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 port3000, 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, andAUTH_SECRET. Optional features (SMTP, S3, OAuth, AI) use the same environment variables as the Docker guide’s environment variable reference.
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 asnamespace.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 withenvFrom:
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.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.
PGDATAuses a subdirectory so filesystem entries such aslost+foundat the volume root do not prevent initialization. POSTGRES_PASSWORDinitializes 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.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, andS3_BUCKETare set, the app writes uploads under/app/data. Thereactive-resume-dataPVC is mounted there;fsGroup: 1000requests group write access from storage drivers that support it. Otherwise, configure volume permissions for UID/GID1000. Without that mount, uploads are lost when the Pod is replaced. - S3-compatible storage. Set
S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY, andS3_BUCKETin the Secret. SetS3_REGIONfor your bucket (default:us-east-1) andS3_ENDPOINTfor non-AWS services. SetS3_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.
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 routesresume.example.com to the Service and terminates TLS with cert-manager. Two rules apply:
APP_URLmust equal the public HTTPS URL users visit. A mismatch (or serving HTTPS whileAPP_URLsayshttp://…) 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/.
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:- Chart source: helmforgedev/charts — charts/reactive-resume
- Chart documentation: helmforge.dev — Reactive Resume
Updating
- Back up the database and uploads first. Do this before every update.
-
Restart the app to pull the current
latestimage. The example explicitly setsimagePullPolicy: Always; setting the image to the samelateststring does not trigger a rollout. -
Wait for the rollout, then check the startup logs while migrations run:
Troubleshooting
The app Pod is in CrashLoopBackOff
The app Pod is in CrashLoopBackOff
- Common cause: database migrations failed (often a bad
DATABASE_URL). - What to do: check logs with
kubectl -n reactive-resume logs -f deployment/reactive-resumeand confirm the PostgreSQL Pod is running and the Service is reachable. URL-encode special characters in the password.
/api/health returns 503 even though Postgres is up
/api/health returns 503 even though Postgres is up
- Common cause: storage health failed (not only the database).
- Fix: inspect the endpoint response payload and check the
storagefield; confirm the PVC is mounted and not full, and that the S3 settings (if used) are valid.
Uploads disappear after a restart
Uploads disappear after a restart
- Cause: local upload storage was not mounted to a persistent volume.
- Fix: add the
reactive-resume-dataPVC mount at/app/data(withfsGroup: 1000) and redeploy.
PDF export fails or downloads an empty file
PDF export fails or downloads an empty file
- 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.