Skip to main content
This guide deploys Reactive Resume to a Kubernetes cluster with plain manifests. The result matches the Docker setup: one app Deployment serving everything on port 3000, a separate PostgreSQL database, and persistent storage for uploads. Adapt the storage class and Ingress settings to your cluster.

Before you start

You need:
  • A cluster with kubectl access and a default StorageClass for PersistentVolumeClaims.
  • An Ingress controller and a way to issue TLS certificates, such as cert-manager.
  • A DNS name for your instance, for example resume.example.com.
  • About 250m CPU and 512 MiB of memory for the app Pod, plus room for PostgreSQL if it runs in the cluster.
The image is amruthpillai/reactive-resume on Docker Hub or ghcr.io/reactive-resume/reactive-resume on GitHub Container Registry, built for amd64 and arm64. The examples pin the v6 tag, which gets fixes and new features without moving to the next major version.

Create the namespace and Secret

Save both manifests. The Secret holds every setting and is passed to the app as environment variables.
namespace.yaml
secret.yaml
Generate the secrets and the database password with openssl rand -hex 32, and paste them in. See Environment variables for everything else you can add, such as SMTP, S3 and single sign-on.
stringData keeps the example readable, but a Secret is only base64-encoded, not encrypted. Keep this file out of version control. For GitOps, use Sealed Secrets, SOPS or External Secrets. Never change AUTH_SECRET or ENCRYPTION_SECRET once people use the instance.

Run PostgreSQL

You can use a managed database, an operator such as CloudNativePG, or this minimal single-instance Deployment. If you use your own, skip this file and set DATABASE_URL to it.
postgres.yaml
  • Keep this Service a ClusterIP. Don’t expose PostgreSQL outside the cluster.
  • POSTGRES_PASSWORD only applies when the database is first created. Changing it later doesn’t change the password.
  • Inside the cluster, use the Service name as the host in DATABASE_URL, never localhost.

Deploy the app

This file adds the uploads volume, the Deployment, a Service and an Ingress.
reactive-resume.yaml
Replace resume.example.com, ingressClassName and the cluster-issuer with your own values, and point your DNS at the Ingress controller. The annotations let the Ingress accept Assistant attachments of up to 25 MB and stream replies that take up to 4 minutes; What every reverse proxy needs explains why. That section also covers client IP headers: make sure your controller doesn’t pass on CF-Connecting-IP, CF-Connecting-IPv6 or True-Client-IP headers sent by clients, or they can get around per-IP rate limits.

Apply and verify

1

Apply the manifests

The app Pod becomes Ready once its startup migrations finish and /api/health answers 200. Follow along with kubectl -n reactive-resume logs -f deployment/reactive-resume.
2

Create your account

Open your APP_URL and create an account. You’re signed in right away. Without SMTP, the verification email is written to the app log instead of being sent.
3

Check that data persists

Create a resume from a sample and upload a profile picture. Then replace the Pod:
Reload the page. The resume and picture should still be there. Expect a short outage while a single-replica Deployment restarts.
4

Close sign-ups (optional)

For a private instance, add FLAG_DISABLE_SIGNUPS: "true" to secret.yaml after your account exists, apply it, and restart the Deployment. Pods only read Secret changes when they start.

How the probes work

  • The startup probe gives the first start up to 5 minutes to run migrations before other probes begin.
  • The readiness probe calls /api/health, which checks the database, storage and, when configured, Redis. It answers 503 if any of them fails, and Kubernetes stops sending traffic to that Pod until it recovers.
  • There is deliberately no liveness probe on /api/health. Restarting the app doesn’t fix a database or storage outage. If the server process itself exits, Kubernetes restarts the container anyway.
See Check the health endpoint for the response format. To call it yourself:

Run several replicas

The example runs one replica because uploads live on a ReadWriteOnce volume. To run more:
  1. Switch uploads to S3-compatible storage: add the S3_* variables to the Secret, then remove the PVC, the volume and the /app/data mount. See Store uploads in S3-compatible storage.
  2. Add Redis and set REDIS_URL in the Secret, so rate limits, stopping Assistant replies and live updates work across Pods.
  3. Change strategy to RollingUpdate and raise replicas.
Pods that start together are safe: they take turns on migrations, and only one applies changes.

Update the app

  1. Back up the database and uploads.
  2. Restart the Deployment to pull the newest v6 image:
  3. Check the logs while the new version applies its migrations.
For fully reproducible deployments, pin an exact tag such as v6.0.0 (or an image digest), change it in reactive-resume.yaml, and run kubectl apply -f reactive-resume.yaml. Coming from v5? Read Migrating from v5 first. Back up PostgreSQL and the uploads (the PVC or the S3 bucket) on a schedule, and keep a copy of the Secret. Update PostgreSQL separately from the app, following your operator’s or provider’s upgrade process.

Community Helm chart

A community-maintained Helm chart is available from HelmForge:
The Helm chart is maintained outside the Reactive Resume project. Report chart problems in the HelmForge repository. Check that it supports v6 before using it; the manifests above work without it.

Troubleshooting

Read the logs with kubectl -n reactive-resume logs deployment/reactive-resume --previous. Invalid environment variables names a missing or malformed setting. Database migrations failed usually means a wrong DATABASE_URL or PostgreSQL isn’t ready. Local storage path is not writable means the volume isn’t writable by UID 1000; keep fsGroup: 1000 or fix the volume’s permissions.
Port-forward and call /api/health. The entry marked unhealthy points at the problem: the database, the uploads volume or S3 settings, or Redis.
APP_URL doesn’t match the address in the browser, or it says http:// while you use https://. Fix it in the Secret, apply, and restart the Deployment.
The Ingress is buffering responses, timing out after 60 seconds, or rejecting large bodies. Apply the annotations from the example, or your controller’s equivalents.