Skip to main content
From v5.1.0 onwards — the builder generates PDFs in the browser via @react-pdf/renderer. New deployments no longer require Browserless, Chromium, or any external print service as a dependency. The PRINTER_* and BROWSERLESS_* environment variables are no longer read and can be removed from your configuration.

Overview

Reactive Resume runs on Kubernetes as a single Deployment that serves both the web app and the API on port 3000, the same way the official Docker image does. The rest of the stack matches the Self-hosting with Docker guide:
  • PostgreSQL must run as a separate service. The app connects to it through DATABASE_URL; no all-in-one image with an embedded database is planned.
  • Persistent storage for uploads. Without S3, uploads live under /app/data, so a PersistentVolumeClaim must be mounted there.
  • Secrets for APP_URL, DATABASE_URL, and AUTH_SECRET. Optional features (SMTP, S3, OAuth, AI) use the same environment variables as the Docker guide’s environment variable reference.
Everything below uses plain Kubernetes manifests for a Linux cluster. Adapt the storage and Ingress settings to your cluster. A community Helm chart is linked at the end of the page; it is maintained outside this repository.

Image

Use ghcr.io/reactive-resume/reactive-resume:latest or amruthpillai/reactive-resume:latest.

PostgreSQL

Stores accounts, resumes, and application data. Runs separately, never embedded in the app image.

Minimum requirements

Kubernetes cluster

A running cluster with kubectl access and a default StorageClass for PersistentVolumeClaims.

Ingress + TLS

An Ingress controller (nginx, Traefik, …) and a way to issue TLS certificates, for example cert-manager.

Compute

1 vCPU / 1 GB RAM minimum for the app Pod (2 GB recommended when PostgreSQL runs in the same cluster).

Create the namespace

Save this as namespace.yaml. Apply it before any of the namespaced resources below.
namespace.yaml

Required Secrets

Configuration is passed to the Pod as environment variables. Store the values in a Secret and reference it from the Deployment with envFrom:
secret.yaml
1

Generate AUTH_SECRET

Generate a strong secret and paste it into AUTH_SECRET.
2

Set APP_URL

Set APP_URL to the public HTTPS URL you will reach through the Ingress. If it does not match the URL you actually use, sign-in redirects and cookies will misbehave.
3

Set DATABASE_URL

Point DATABASE_URL at your PostgreSQL instance. Inside the cluster the host is the Service DNS name (for example postgres in the same namespace) — never localhost, which resolves to the app Pod itself. For the PostgreSQL example below, generate a separate password with openssl rand -hex 32 and use it in both POSTGRES_PASSWORD and DATABASE_URL. URL-encode special characters in connection-string passwords.
stringData keeps the example readable. Base64-encoded data is not encryption. Keep files containing real secrets out of version control; for GitOps, use encrypted Secrets or an External Secrets mapping. Retain AUTH_SECRET across Pod restarts and upgrades.

PostgreSQL dependency

PostgreSQL is the only required service next to the app. You can use a managed database outside the cluster, an operator such as CloudNativePG, or a chart such as the HelmForge or Bitnami PostgreSQL charts. The minimal example below is enough for a small single-node cluster:
postgres.yaml
  • Keep the PostgreSQL Service a ClusterIP. Do not expose PostgreSQL to the public internet.
  • Keep the image pinned to a PostgreSQL major version. PGDATA uses a subdirectory so filesystem entries such as lost+found at the volume root do not prevent initialization.
  • POSTGRES_PASSWORD initializes a new database only. Changing the Secret does not change an existing database’s password.
  • The app runs database migrations automatically on every start, and needs to reach PostgreSQL before it becomes ready.

Deploy the application

With the namespace and Secret above, this file adds the uploads PersistentVolumeClaim, Deployment, Service, and Ingress.
reactive-resume.yaml
Replace resume.example.com, ingressClassName, and the cluster-issuer name with your own host, controller class, and configured issuer. Point your hostname’s DNS at the Ingress controller. The app listens on PORT and serves both the API and the built web app; the default image uses PORT=3000, so the example targets container port 3000. If you change PORT, update the container port, Service targetPort, and readiness probe to match.
Apply the four files in order and wait for PostgreSQL before starting the app:
If you use an external database, skip postgres.yaml and its rollout check, and ensure the database is reachable first. The app Pod becomes Ready only after automatic migrations succeed and the /api/health endpoint reports the database and storage healthy. If the Pod exits or stays in CrashLoopBackOff, check the logs:

Storage: uploads and persistence

Uploads are stored in one of two ways, exactly as in the Docker guide:
  • Local storage (default). Unless all three of S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and S3_BUCKET are set, the app writes uploads under /app/data. The reactive-resume-data PVC is mounted there; fsGroup: 1000 requests group write access from storage drivers that support it. Otherwise, configure volume permissions for UID/GID 1000. Without that mount, uploads are lost when the Pod is replaced.
  • S3-compatible storage. Set S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and S3_BUCKET in the Secret. Set S3_REGION for your bucket (default: us-east-1) and S3_ENDPOINT for non-AWS services. Set S3_FORCE_PATH_STYLE: "true" for path-style services such as MinIO or SeaweedFS. You can then omit the app’s uploads PVC, volume, and volume mount. Private AI Agent attachments require S3-compatible storage.
Switching between local storage and S3 does not move existing uploads. Export or back them up before changing the storage driver.
Back up the PostgreSQL database and the upload storage (the reactive-resume-data PVC or the S3 bucket) together, on a regular schedule. Recreating the Deployment must preserve both.

Ingress and the public URL

The Ingress above routes resume.example.com to the Service and terminates TLS with cert-manager. Two rules apply:
  • APP_URL must equal the public HTTPS URL users visit. A mismatch (or serving HTTPS while APP_URL says http://…) causes sign-in redirects and cookies that do not stick.
  • The app serves the web app, the API, uploads, and assets from one origin. Proxy the whole application; do not rewrite or filter paths such as /api/.
HTTPS is strongly recommended. Authentication cookies and the first-user signup flow depend on a correct public origin.

Health checks and startup

Reactive Resume exposes a health endpoint at /api/health that verifies the database and storage; if either is unhealthy it returns HTTP 503, and 200 when both are healthy. The Deployment uses this endpoint for readiness, keeping the Pod out of Service rotation until both dependencies are healthy. It deliberately omits a liveness probe against this dependency check: restarting the app does not repair a database or storage outage, and a slow migration should not be interrupted by a probe. Kubernetes restarts the container if the server process exits. To check the endpoint manually:
On every start the server automatically runs database migrations before serving traffic. If migrations fail (usually a database connection issue), the container exits with an error — check kubectl logs.

Verify the installation

1

Create the first account

Open APP_URL and sign up for the first account. Without SMTP configured, verification emails are logged to the server console instead of being sent: kubectl -n reactive-resume logs -f deployment/reactive-resume.
2

Create a resume

Create a resume from the dashboard, add a few sections, and upload a profile picture. Reload the page and confirm the saved content and picture are present.
3

Export a PDF

Open Download in the builder header and choose PDF. Builder PDF rendering happens in the browser via @react-pdf/renderer. Open the downloaded file and check its text, fonts, and picture.
4

Check persistence

Replace the Pod and verify nothing is lost:
After the new Pod is Ready, sign in again and confirm the resume and any uploaded picture are still there. If you deployed the example PostgreSQL Deployment, also restart it with kubectl -n reactive-resume rollout restart deployment/postgres, wait for its rollout to complete, and confirm the same data remains. Expect downtime during these single-replica restarts.
5

Close signups (optional)

For a private single-user instance, add FLAG_DISABLE_SIGNUPS: "true" under stringData in secret.yaml, apply it with kubectl apply -f secret.yaml, and restart the app Deployment after your account exists.

Community Helm chart (HelmForge)

A community-maintained Helm chart for Reactive Resume is available in the HelmForge charts repository:
This chart is community-maintained and lives outside this repository. It is not part of the Reactive Resume project, and chart support is handled in the HelmForge repository, not here. The manifests above work without it.

Updating

  1. Back up the database and uploads first. Do this before every update.
  2. Restart the app to pull the current latest image. The example explicitly sets imagePullPolicy: Always; setting the image to the same latest string does not trigger a rollout.
  3. Wait for the rollout, then check the startup logs while migrations run:
For reproducible deployments, pin a specific version tag or digest instead of latest, and update PostgreSQL separately from the app, following your operator’s or provider’s upgrade procedure. For a pinned app image, change image in reactive-resume.yaml and run kubectl apply -f reactive-resume.yaml to deploy the new version.

Troubleshooting

  • Common cause: database migrations failed (often a bad DATABASE_URL).
  • What to do: check logs with kubectl -n reactive-resume logs -f deployment/reactive-resume and confirm the PostgreSQL Pod is running and the Service is reachable. URL-encode special characters in the password.
  • Common cause: APP_URL does not match the URL you actually use, or you serve HTTPS while APP_URL says http://….
  • Fix: set APP_URL to the canonical public HTTPS URL in the Secret, then restart the Deployment.
  • Common cause: storage health failed (not only the database).
  • Fix: inspect the endpoint response payload and check the storage field; confirm the PVC is mounted and not full, and that the S3 settings (if used) are valid.
  • Cause: local upload storage was not mounted to a persistent volume.
  • Fix: add the reactive-resume-data PVC mount at /app/data (with fsGroup: 1000) and redeploy.
  • Checks: for builder exports, inspect the browser console and failed network requests, including fonts and images. Check download permissions, browser memory limits, extensions, and custom CSP rules.
  • No external Browserless or Chromium service is needed. API PDF downloads and the public viewer’s server fallback render in the app process; inspect the app logs if one of those requests fails.