docs/self-hosting/docker.mdx
Reactive Resume can be self-hosted with Docker. These are the services you'll need:
The official image runs one application container that serves both the web app and API. PostgreSQL must run as a
separate service and the app connects to it through DATABASE_URL; no all-in-one image with an embedded database is
planned. Follow the Docker Compose quickstart below for the supported setup.
You can pull the latest app image from:
amruthpillai/reactive-resume:latestghcr.io/amruthpillai/reactive-resume:latestpostgres.APP_URL, DATABASE_URL, and AUTH_SECRET in a private .env file. Set the database host in DATABASE_URL
to a name or address reachable from the app container./app/data.reactive-resume app service and PostgreSQL service to the intended private container network. Do not
expose PostgreSQL to the public internet.The repository's full compose.yml also defines optional Redis and S3-compatible storage services. Those services are
not required for the core resume workflow; use the two-service example below when you only need the app and PostgreSQL.
The repository file is a broader source-build stack and publishes administration ports for local use. Before using it
on an internet-facing host, remove those host port mappings, bind them to loopback, or restrict them with a firewall.
Create a new folder (for example reactive-resume/) with:
compose.yml.env./data)The Compose example below reads `.env` directly. If you use the repository's `compose.yml` instead, copy its `.env.example` into the same folder. That file supplies defaults before your `.env` overrides are applied.
# --- Server ---
TZ="Etc/UTC"
APP_URL="http://localhost:3000"
# --- Database (PostgreSQL) ---
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres"
# --- Authentication ---
# Generated using `openssl rand -hex 32`
AUTH_SECRET=""
# Better Auth dashboard API key (optional)
BETTER_AUTH_API_KEY=""
# Social Auth (Google, optional)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Social Auth (GitHub, optional)
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Social Auth (LinkedIn, optional)
LINKEDIN_CLIENT_ID=""
LINKEDIN_CLIENT_SECRET=""
# Custom OAuth Provider
OAUTH_PROVIDER_NAME=""
OAUTH_CLIENT_ID=""
OAUTH_CLIENT_SECRET=""
# Use EITHER discovery URL (preferred for OIDC-compliant providers):
OAUTH_DISCOVERY_URL=""
# OR manual URLs (all three required if not using discovery):
OAUTH_AUTHORIZATION_URL=""
OAUTH_TOKEN_URL=""
OAUTH_USER_INFO_URL=""
# Custom scopes (space-separated, defaults to "openid profile email")
OAUTH_SCOPES=""
# --- Email (optional) ---
# If all keys are disabled, the app logs the email to be sent to the console instead.
SMTP_HOST=""
SMTP_PORT="587"
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM="Reactive Resume <[email protected]>"
SMTP_SECURE="false"
# --- Storage (optional) ---
# If all S3 keys are disabled, the app uses local filesystem storage instead.
# Make sure to mount this directory to a volume or the host filesystem to ensure data integrity.
S3_ACCESS_KEY_ID=""
S3_SECRET_ACCESS_KEY=""
S3_REGION="us-east-1"
S3_ENDPOINT=""
S3_BUCKET=""
# Set to "true" for path-style URLs (https://endpoint/bucket), common with MinIO, SeaweedFS, etc.
# Set to "false" for virtual-hosted-style URLs (https://bucket.endpoint), common with AWS S3, Cloudflare R2, etc.
S3_FORCE_PATH_STYLE="false"
# --- AI features (optional) ---
# ENCRYPTION_SECRET is required for saved AI providers. REDIS_URL is also required for the AI Agent workspace.
# The rest of Reactive Resume can run without these.
REDIS_URL=""
# Generated using `openssl rand -hex 32`
ENCRYPTION_SECRET=""
# --- Feature Flags ---
FLAG_DISABLE_SIGNUPS="false"
FLAG_DISABLE_EMAIL_AUTH="false"
FLAG_DISABLE_IMAGE_PROCESSING="false"
FLAG_DISABLE_API_RATE_LIMIT="false"
# Allows any parseable dynamic OAuth redirect URI. Keep false unless this is a trusted self-hosted deployment.
FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI="false"
# Allows unsafe/private/non-public AI provider base URLs. Keep false unless this is a trusted self-hosted deployment.
FLAG_ALLOW_UNSAFE_AI_BASE_URL="false"
```bash Linux/macOS (alternative)
head -c 32 /dev/urandom | hexdump -v -e '/1 "%02x"'
```
```powershell Windows
[byte[]]$bytes = New-Object byte[] 32; (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($bytes); $bytes | ForEach-Object { "{0:x2}" -f $_ } | Out-String -Stream | ForEach-Object { $_.Trim() } | Write-Host -NoNewline
```
</CodeGroup>
<CodeGroup>
services:
postgres:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 10
reactive-resume:
image: amruthpillai/reactive-resume:latest
# image: ghcr.io/amruthpillai/reactive-resume:latest
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- .env
volumes:
# Used when S3 is not configured; keeps uploads persistent
- ./data:/app/data
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1));"]
interval: 30s
timeout: 10s
retries: 3
volumes:
postgres_data:
</CodeGroup>
<Tip>
Prefer pulling from Docker Hub? Keep <code>amruthpillai/reactive-resume:latest</code>. Prefer GHCR? Swap it to <code>ghcr.io/amruthpillai/reactive-resume:latest</code>.
</Tip>
<Note>
In Docker, the Reactive Resume server listens on <code>PORT</code> and serves both the API and the built web app.
The default image uses <code>PORT=3000</code>, so the example maps <code>3000:3000</code>. If you change
<code>PORT</code>, update the container-side port mapping and health check to match.
</Note>
docker compose up -d
docker compose ps
docker compose logs -f reactive-resume
</CodeGroup>
Reactive Resume should now be available at your `APP_URL` (for the example above: `http://localhost:3000`).
Use your platform's generic container configuration to create two separately managed containers: one for Reactive Resume and one for PostgreSQL. No official Unraid Community Applications template is provided.
amruthpillai/reactive-resume:latest or ghcr.io/amruthpillai/reactive-resume:latest image for the
app container.3000 to the host port you want to use.DATABASE_URL to the PostgreSQL container or
service name reachable on that network.APP_URL, DATABASE_URL, and AUTH_SECRET as private environment variables./app/data.After starting both containers, wait for PostgreSQL to become healthy and check the app logs while automatic migrations run. Open the UI only after the app health check succeeds.
Generate with:
<CodeGroup>
openssl rand -hex 32
</CodeGroup>
**`GOOGLE_CLIENT_ID`** / **`GOOGLE_CLIENT_SECRET`** (optional): Enables Google sign-in.
**`GITHUB_CLIENT_ID`** / **`GITHUB_CLIENT_SECRET`** (optional): Enables GitHub sign-in.
**`LINKEDIN_CLIENT_ID`** / **`LINKEDIN_CLIENT_SECRET`** (optional): Enables LinkedIn sign-in.
**`BETTER_AUTH_API_KEY`** (optional): Enables Better Auth dashboard integrations.
**Custom OAuth provider** (optional):
- **`OAUTH_PROVIDER_NAME`**: Display name in the UI
- **`OAUTH_CLIENT_ID`** / **`OAUTH_CLIENT_SECRET`**: Required for any custom OAuth provider
- **`OAUTH_SCOPES`**: Space-separated scopes (defaults to `openid profile email`)
Configure endpoints using **one** of these methods:
- **Option A (OIDC Discovery, preferred)**: Set `OAUTH_DISCOVERY_URL` to your provider's `.well-known/openid-configuration` URL
- **Option B (manual URLs)**: Set all three: `OAUTH_AUTHORIZATION_URL`, `OAUTH_TOKEN_URL`, and `OAUTH_USER_INFO_URL`
- Email delivery is enabled only when **all** of `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, and `SMTP_FROM` are set.
- **`SMTP_HOST`**: SMTP host (if empty, email sending is disabled).
- **`SMTP_PORT`**: Defaults to `587` in the app.
- **`SMTP_USER`** / **`SMTP_PASS`**: SMTP credentials.
- **`SMTP_FROM`**: Default from address (for example, `Reactive Resume <[email protected]>`).
- **`SMTP_SECURE`**: `"true"` or `"false"` (string). Match your provider settings.
REDIS_URL: Redis connection string used by the AI Agent workspace.ENCRYPTION_SECRET: Secret used to encrypt saved AI provider credentials. Generate with openssl rand -hex 32.If you use the Postgres-only Compose example above and want the AI Agent workspace, add a Redis service or use managed Redis, then set REDIS_URL.
</Accordion>
To update an installation created from the image-based quickstart above to the latest version, follow only the numbered
steps below. If you use the repository's full compose.yml, use the separate source-build path after these steps.
Back up your database and uploads first. Do this before every update.
The database and upload storage are independent resources. Recreating the app container must preserve both the
PostgreSQL data volume or managed database and the /app/data mount or S3 bucket.
Pull the latest app image. Leave the PostgreSQL service unchanged.
docker compose pull reactive-resume
Recreate only the app container to run the new image.
docker compose up -d --no-deps reactive-resume
Check migration/startup logs after deploy.
docker compose logs -f reactive-resume
(Optional) Remove old, unused Docker images to free up disk space.
docker image prune -f
The repository's full compose.yml names its build-only app service reactive_resume. After confirming its dependencies
are healthy, rebuild that service and follow its migration/startup logs with:
docker compose up -d --build --no-deps reactive_resume
docker compose logs -f reactive_resume
Do not run docker compose pull for this build-only service.
This process updates the app container and automatically runs DB migrations on startup. If migration fails, restore from backup and fix configuration before retrying.
Update PostgreSQL separately from the app. Choose a supported, major-pinned PostgreSQL image or select the target version through your managed provider, then follow that image's, host's, or provider's upgrade procedure. Back up the database and verify that the backup can be restored before a major-version upgrade. Pulling a new app image and running app migrations do not upgrade the PostgreSQL server.
Reactive Resume stores data in two places: the PostgreSQL database and file uploads (either local storage or S3). Back up both on a regular schedule.
Test restores for both resources. An app container backup alone does not include the separate database or uploads, and recreating the app container must not replace either persistent resource.
Your PostgreSQL database holds all user accounts, resumes, and application data. Use pg_dump to take periodic backups and store them somewhere secure. Many providers of managed PostgreSQL also offer automated backups that handle scheduling, retention, and restores for you.
If you're using local storage (the ./data directory), include this directory in your regular backup routine. A simple approach is to use rsync or a similar tool to copy the directory to a remote server or cloud storage.
If you're using S3-compatible storage, consider enabling versioning on your bucket to protect against accidental deletions. Most S3 providers also support lifecycle rules for automatic cleanup of old versions and cross-region replication for disaster recovery.
Reactive Resume exposes a health check endpoint at /api/health that verifies the application and its dependencies. It checks database and storage; if either is unhealthy, the endpoint returns HTTP 503.
The Docker Compose configuration includes a health check that periodically calls the /api/health endpoint:
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1));"]
interval: 30s
timeout: 10s
retries: 3
When the health check fails, Docker marks the container as unhealthy. This status is visible when running docker compose ps or docker ps.
Most reverse proxies (such as Traefik, Caddy, or nginx with upstream health checks) can use Docker's health status to make routing decisions:
This is particularly useful in high-availability setups where you have multiple instances of Reactive Resume. If one instance becomes unhealthy (for example, it loses database or storage connectivity), the reverse proxy will stop routing traffic to it until it recovers.
<Tip> If you're using **Traefik**, it automatically respects Docker health checks when using the Docker provider. Unhealthy containers are excluded from routing without any additional configuration. </Tip>To check your instance yourself:
# From outside the container
curl -f http://localhost:3000/api/health
# Check Docker's health status
docker compose ps
A healthy response returns HTTP 200. If you get a different status code, the JSON response body says what failed. If the connection is refused or times out there is no response to read, so check the container and reverse-proxy logs instead.
To display one public resume at / instead of the marketing home, set the optional server environment variable ROOT_RESUME_ID on the application service:
environment:
APP_URL: https://resume.example.com
ROOT_RESUME_ID: your-resume-id
Find the resume ID in its owner's builder URL: /builder/<resume-id>. The resume must already have Allow Public Access enabled in Sharing. This setting does not change its visibility. Password protection and the download-button preference still apply, and the ordinary /<username>/<slug> URL continues to work. Renaming the username or slug does not change the configured ID.
Restart the application after setting or changing ROOT_RESUME_ID. With Docker Compose, run docker compose up -d to recreate the application with the new environment. Unset the variable or leave it blank, then restart, to restore the marketing home. A missing, deleted, or private target shows an unavailable page, including when its owner visits /.
Keep APP_URL set to the public origin and proxy the whole application normally, including API, uploads, fonts, and assets. Root mode uses that configured origin for its canonical URL; it does not infer a domain from request headers. A successful password challenge returns visitors to /.
This is a single-resume setting for one self-hosted instance. It does not register custom domains, manage DNS or TLS, or hide the rest of the application. Login and the dashboard remain available at their usual paths.