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.
| Service | Required | Purpose |
|---|---|---|
| PostgreSQL | Yes | Stores accounts, resumes, cover letters and applications. Runs as its own container or managed database. |
| Upload storage | Yes | A folder mounted at /app/data, or an S3-compatible bucket instead. |
| SMTP server | No | Sends verification and password reset emails. Without it, emails are written to the app log. |
| Redis | No | Shares state between several app processes and lets Assistant replies survive a page reload. |
| Firecrawl and an AI provider | No | Enable keyword job search, read posting URLs and extract application details. See Job search and AI. |
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
| Tag | Contents |
|---|---|
latest | The newest release. |
v6, v6.0, v6.0.0 | A major, minor or exact release. Pin v6 to get fixes without jumping to the next major version. |
nightly | The current main branch. For testing only. |
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#
Create a folder for your instance
The next steps create two files in it: .env for settings and compose.yml for the containers.
Create the .env file
Create .env with the three required settings:
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.
Create 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.
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:
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.envand rundocker compose up -d. Nobody else can sign up; existing accounts keep working. - Mirror Computer Modern fonts for server PDFs. See Font mirroring for configuration and current browser and Google-font limitations.
- 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_URLto the publichttps://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:
- Applies any new database migrations. Several containers starting at once take turns, so only one migrates.
- Compares the database schema with what the migrations expect. A mismatch is logged; set
STRICT_SCHEMA_CHECK=trueto stop instead. - With local storage, checks that
/app/data(orLOCAL_STORAGE_PATH) is writable, and stops if it isn’t. - 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#
Back up first
Back up the database and uploads before every update. See Back up your data.
Pull the new image
bash docker compose pull reactive-resume 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_datavolume (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:
PostgreSQL and SeaweedFS use default passwords in this file, so it keeps them on the Docker network and doesn’t
publish their ports. To reach them from the host, uncomment their ports entries, which bind to 127.0.0.1 only. In
production, the server refuses to start while AUTH_SECRET or ENCRYPTION_SECRET still has its example value from
.env.example.
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:latestand one frompostgres: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
3000to any free host port. - Map a persistent folder to
/app/data. The app runs as thenodeuser (UID 1000), so that user must be able to write to it. - Set
APP_URL,DATABASE_URLandAUTH_SECRETas 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#
The app container keeps restarting
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 failedorECONNREFUSED:DATABASE_URLis 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/dataisn’t writable by UID 1000. Fix its owner, or use a named volume.
Sign-in loops back to the sign-in page
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.
Verification or password reset emails never arrive
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.
/api/health returns 503
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.
Uploaded pictures disappear after an update
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.
S3 error: getaddrinfo ENOTFOUND bucket.endpoint
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".
The Assistant says it isn't set up on this server
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.
PDF download fails in the browser
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.
Related pages#
- Environment variables: every setting, with defaults.
- Deployment examples: reverse proxies, S3 storage, Redis and local AI.
- Job search and AI: Firecrawl, SearXNG and shared or personal AI providers.
- Self-hosting with Kubernetes: the same setup as Kubernetes manifests.
- Self-hosting on Hostinger: a 1-click deploy of the same stack on a Hostinger VPS.
- Checking service status: the status of the hosted instance.