Skip to main content
This guide sets up your own Reactive Resume server with Docker Compose. You end up with two containers, the app and a PostgreSQL database, reachable at an address you choose. It’s the same app that runs on https://rxresu.me, and your data stays on your hardware.

Before you start

You need:
  • A Linux, macOS or Windows host with Docker Engine and the Docker Compose plugin (or Docker Desktop).
  • At least 1 vCPU and 1 GB of memory for the app. Allow 2 GB when PostgreSQL runs on the same host.
  • Disk space for the database and uploaded pictures. 10 GB is plenty to start.
  • For anything beyond a test on your own machine: a domain name and a reverse proxy that serves it over HTTPS. See Deployment examples for Traefik, Caddy and nginx.

How the pieces fit

The official image runs a single Node.js process on port 3000. It serves the web app, the API, the MCP server and uploaded files from one address. PDFs are rendered by the app itself, in the browser or on the server, so there is no separate printer service. The image is published to two registries with the same tags:
  • Docker Hub: amruthpillai/reactive-resume
  • GitHub Container Registry: ghcr.io/reactive-resume/reactive-resume
Images are built for linux/amd64 and linux/arm64, so they run on most servers, Apple silicon, and a Raspberry Pi 4 or newer running a 64-bit operating system.

Set up with Docker Compose

1

Create a folder for your instance

The next steps create two files in it: .env for settings and compose.yml for the containers.
2

Create the .env file

Create .env with the three required settings:
.env
Generate a value for AUTH_SECRET and a database password, and paste them in. Use the same password in POSTGRES_PASSWORD and DATABASE_URL.
If you will serve the instance from a domain, set APP_URL to that address now, for example https://resume.example.com. Every other setting is optional; the environment variable reference lists them all.
3

Create compose.yml

compose.yml
PostgreSQL is only reachable from the app container, never from outside. Uploads live in the reactive_resume_data volume, so they survive when the container is recreated. The image has a built-in health check, so Compose reports the app as healthy once it’s ready.
To use GitHub Container Registry instead of Docker Hub, change the image to ghcr.io/reactive-resume/reactive-resume:latest.
4

Start the containers

On the first start the app creates its database tables. The --wait option waits for PostgreSQL and the app to become healthy, with a timeout of 180 seconds. When it succeeds, open your APP_URL (in this example, http://localhost:3000).If startup fails or times out, check the service status and recent app logs:
5

Create your account

Open your APP_URL in a browser and create an account; see Creating an account.You’re signed in right away. Reactive Resume also sends a verification email, but you don’t have to open it to use the app. Without SMTP settings the email isn’t sent; its link is written to the app log instead:
Your instance is running. Next, you might want to:
  • Keep it private. After your own account exists, add FLAG_DISABLE_SIGNUPS="true" to .env and run docker compose up -d. Nobody else can sign up; existing accounts keep working.
  • Send real emails. Add the SMTP_* settings from the reference.
  • Turn on job search and AI. Connect shared Firecrawl and AI services, or enable personal keys with ENCRYPTION_SECRET. See Job search and AI.
  • Serve it over HTTPS. Put a reverse proxy in front and set APP_URL to the public https:// address. See Deployment examples.
APP_URL must match the address in the browser’s address bar exactly, including https://. If it doesn’t, sign-in redirects go to the wrong place and session cookies don’t stick.

What happens at startup

Each time the app container starts, it:
  1. Applies any new database migrations. Several containers starting at once take turns, so only one migrates.
  2. Compares the database schema with what the migrations expect. A mismatch is logged; set STRICT_SCHEMA_CHECK=true to stop instead.
  3. With local storage, checks that /app/data (or LOCAL_STORAGE_PATH) is writable, and stops if it isn’t.
  4. Starts serving on PORT.
If step 1 or 3 fails, the container exits with the reason in its log. This is almost always a wrong DATABASE_URL, a database that isn’t reachable yet, or a storage folder the container can’t write to.

Check the health endpoint

GET /api/health reports whether the app can reach its database, storage and, when configured, Redis. It answers 200 when everything is healthy and 503 when any check fails. The image’s built-in health check calls it every 30 seconds.
The redis field appears only when REDIS_URL is set. When a check fails, its entry shows "status": "unhealthy" and the app log has the details. Each check times out after 1.5 seconds. Use this endpoint for load balancer or orchestrator readiness checks. Restarting the app doesn’t fix a database or storage outage, so avoid using it to kill containers.

Update your instance

1

Back up first

Back up the database and uploads before every update. See Back up your data.
2

Pull the new image

bash docker compose pull reactive-resume
3

Recreate the app container

The new version applies its migrations on startup. PostgreSQL keeps running.
Updating from v5? Read Migrating from v5 first. It covers a one-time command for resumes that still use the old style editor. PostgreSQL is updated separately. Pulling a new app image never changes the database server’s major version. To move to a new major PostgreSQL version, dump the database, start the new version with an empty volume, and restore.

Back up your data

Your data lives in two places. Back up both, on a schedule, and test that you can restore them.
  • Database. Dump it with pg_dump:
    Restore into an empty database with pg_restore:
  • Uploads. With local storage, copy the contents of the reactive_resume_data volume (or your bind-mounted folder). With S3, turn on bucket versioning or replication in your provider.
Keep a copy of your .env too. Without the same AUTH_SECRET and ENCRYPTION_SECRET, a restored instance signs everyone out, breaks two-factor authentication and loses saved AI and Firecrawl keys.

Build from the repository

The repository’s own compose.yml builds the image from source and also starts Redis and SeaweedFS (S3-compatible storage). It reads .env.example first and then your .env.
To update, pull the latest code and rebuild only the app service:
The repository file publishes PostgreSQL (5432) and SeaweedFS (8333) on the host, with default passwords. Remove those ports entries or bind them to 127.0.0.1 before using it on a server reachable from the internet.

Unraid, Synology and other homelab platforms

There is no official app template. Use your platform’s generic container settings:
  • Create two containers: one from amruthpillai/reactive-resume:latest and one from postgres:18. Give PostgreSQL its own persistent volume at /var/lib/postgresql.
  • Put both containers on the same private network, and use the PostgreSQL container’s name as the host in DATABASE_URL.
  • Map the app’s container port 3000 to any free host port.
  • Map a persistent folder to /app/data. The app runs as the node user (UID 1000), so that user must be able to write to it.
  • Set APP_URL, DATABASE_URL and AUTH_SECRET as environment variables.
Inside the app container, localhost means the app container itself. It never reaches a database in another container, so don’t use localhost in DATABASE_URL.

Troubleshooting

Read the log with docker compose logs reactive-resume. The last lines name the problem:
  • Invalid environment variables: a required variable is missing or malformed. The message names it.
  • Database migrations failed or ECONNREFUSED: DATABASE_URL is wrong or PostgreSQL isn’t reachable. Check the host name, password (URL-encode special characters) and that PostgreSQL is healthy.
  • Local storage path is not writable: the folder mounted at /app/data isn’t writable by UID 1000. Fix its owner, or use a named volume.
APP_URL doesn’t match the address you’re using, or you’re on https:// while APP_URL says http://. Set APP_URL to the exact public address and run docker compose up -d.
Emails are only sent when SMTP_HOST, SMTP_USER, SMTP_PASS and SMTP_FROM are all set. Until then they’re written to the app log. If they’re set, check SMTP_PORT and SMTP_SECURE against your provider’s settings and look for SMTP errors in the log.
Look at which entry says unhealthy. For storage, check the /app/data mount or your S3 settings. For redis, check that Redis is running and REDIS_URL is correct.
Nothing persistent was mounted at /app/data, so uploads lived inside the old container. Mount a volume there as in the example above. Pictures uploaded before the change are gone.
The app is using virtual-hosted addresses, which put the bucket name in front of the endpoint. MinIO, SeaweedFS and most self-hosted services need path-style addresses. Set S3_FORCE_PATH_STYLE="true".
Configure a shared AI provider, or set ENCRYPTION_SECRET to at least 32 characters so users can save personal providers. Recreate the container, then check Job search and AI.
The editor renders PDFs in the browser with WebAssembly. If you add your own Content Security Policy at the proxy, allow 'wasm-unsafe-eval' in script-src. Also check for browser extensions that block downloads.