mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 18:23:47 +10:00
Add managed credentials, job posting search and scraping, database migration, translations, and self-hosting documentation. Fix empty resume-copy recovery and ensure every sheet popup renders inside a drawer viewport.
372 lines
15 KiB
Plaintext
372 lines
15 KiB
Plaintext
---
|
|
title: "Self-hosting with Docker"
|
|
description: "Run your own Reactive Resume server with Docker Compose and PostgreSQL: set up, create the first account, check health, update and back up."
|
|
---
|
|
|
|
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](/self-hosting/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](/self-hosting/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
|
|
|
|
<Steps>
|
|
<Step title="Create a folder for your instance">
|
|
```bash
|
|
mkdir reactive-resume && cd reactive-resume
|
|
```
|
|
|
|
The next steps create two files in it: `.env` for settings and `compose.yml` for the containers.
|
|
</Step>
|
|
|
|
<Step title="Create the .env file">
|
|
Create `.env` with the three required settings:
|
|
|
|
```bash .env
|
|
# The address people will use to reach your instance.
|
|
APP_URL="http://localhost:3000"
|
|
|
|
# "postgres" is the database service name in compose.yml.
|
|
POSTGRES_PASSWORD="replace-with-a-database-password"
|
|
DATABASE_URL="postgresql://postgres:replace-with-a-database-password@postgres:5432/postgres"
|
|
|
|
# Generate with: openssl rand -hex 32
|
|
AUTH_SECRET=""
|
|
```
|
|
|
|
Generate a value for `AUTH_SECRET` and a database password, and paste them in. Use the same password in
|
|
`POSTGRES_PASSWORD` and `DATABASE_URL`.
|
|
|
|
<CodeGroup>
|
|
```bash Linux and macOS
|
|
openssl rand -hex 32
|
|
```
|
|
|
|
```powershell Windows
|
|
# PowerShell 7 or newer
|
|
[Convert]::ToHexString([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)).ToLower()
|
|
```
|
|
</CodeGroup>
|
|
|
|
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](/self-hosting/environment-variables) lists them all.
|
|
</Step>
|
|
|
|
<Step title="Create compose.yml">
|
|
```yaml compose.yml
|
|
services:
|
|
postgres:
|
|
image: postgres:18
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: postgres
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
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
|
|
restart: unless-stopped
|
|
ports:
|
|
- "3000:3000"
|
|
env_file: .env
|
|
volumes:
|
|
- reactive_resume_data:/app/data
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
|
|
volumes:
|
|
postgres_data:
|
|
reactive_resume_data:
|
|
```
|
|
|
|
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.
|
|
|
|
<Tip>
|
|
To use GitHub Container Registry instead of Docker Hub, change the image to
|
|
`ghcr.io/reactive-resume/reactive-resume:latest`.
|
|
</Tip>
|
|
</Step>
|
|
|
|
<Step title="Start the containers">
|
|
```bash
|
|
docker compose up -d
|
|
docker compose logs -f reactive-resume
|
|
```
|
|
|
|
On the first start the app creates its database tables. When the log shows a line containing
|
|
`Up and running on`, press <kbd>Ctrl</kbd> <kbd>C</kbd> to stop following the log. Within a minute, `docker compose ps` shows
|
|
the app as `healthy`.
|
|
</Step>
|
|
|
|
<Step title="Create your account">
|
|
Open your `APP_URL` in a browser and create an account; see [Creating an account](/guides/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:
|
|
|
|
```bash
|
|
docker compose logs reactive-resume | grep verify-email
|
|
```
|
|
</Step>
|
|
</Steps>
|
|
|
|
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](/self-hosting/environment-variables#email).
|
|
- **Turn on job search and AI.** Connect shared Firecrawl and AI services, or enable personal keys with
|
|
`ENCRYPTION_SECRET`. See [Job search and AI](/self-hosting/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](/self-hosting/examples).
|
|
|
|
<Warning>
|
|
`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.
|
|
</Warning>
|
|
|
|
## 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.
|
|
|
|
```bash
|
|
curl http://localhost:3000/api/health
|
|
```
|
|
|
|
```json
|
|
{
|
|
"service": "reactive-resume",
|
|
"version": "6.0.0",
|
|
"status": "healthy",
|
|
"timestamp": "2026-09-30T09:00:00.000Z",
|
|
"uptime": "3600.12s",
|
|
"database": { "status": "healthy", "latencyMs": 2 },
|
|
"storage": {
|
|
"type": "local",
|
|
"status": "healthy",
|
|
"message": "Local filesystem storage is accessible and has read/write permission.",
|
|
"latencyMs": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
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
|
|
|
|
<Steps>
|
|
<Step title="Back up first">
|
|
Back up the database and uploads before every update. See [Back up your data](#back-up-your-data).
|
|
</Step>
|
|
|
|
<Step title="Pull the new image">
|
|
```bash
|
|
docker compose pull reactive-resume
|
|
```
|
|
</Step>
|
|
|
|
<Step title="Recreate the app container">
|
|
```bash
|
|
docker compose up -d reactive-resume
|
|
docker compose logs -f reactive-resume
|
|
```
|
|
|
|
The new version applies its migrations on startup. PostgreSQL keeps running.
|
|
</Step>
|
|
</Steps>
|
|
|
|
Updating from v5? Read [Upgrading to v6](/self-hosting/upgrading-to-v6) 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`:
|
|
|
|
```bash
|
|
docker compose exec -T postgres pg_dump -U postgres -d postgres --format=custom > reactive-resume.dump
|
|
```
|
|
|
|
Restore into an empty database with `pg_restore`:
|
|
|
|
```bash
|
|
docker compose exec -T postgres pg_restore -U postgres -d postgres --clean --if-exists < reactive-resume.dump
|
|
```
|
|
|
|
- **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`.
|
|
|
|
```bash
|
|
git clone https://github.com/reactive-resume/reactive-resume.git
|
|
cd reactive-resume
|
|
cp .env.example .env # then set APP_URL, AUTH_SECRET and ENCRYPTION_SECRET
|
|
docker compose up -d --build
|
|
```
|
|
|
|
To update, pull the latest code and rebuild only the app service:
|
|
|
|
```bash
|
|
git pull
|
|
docker compose up -d --build --no-deps reactive_resume
|
|
```
|
|
|
|
<Warning>
|
|
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.
|
|
</Warning>
|
|
|
|
## 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.
|
|
|
|
<Note>
|
|
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`.
|
|
</Note>
|
|
|
|
## Troubleshooting
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="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 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.
|
|
</Accordion>
|
|
|
|
<Accordion title="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`.
|
|
</Accordion>
|
|
|
|
<Accordion title="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.
|
|
</Accordion>
|
|
|
|
<Accordion title="/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.
|
|
</Accordion>
|
|
|
|
<Accordion title="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.
|
|
</Accordion>
|
|
|
|
<Accordion title="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"`.
|
|
</Accordion>
|
|
|
|
<Accordion title="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](/self-hosting/job-search-and-ai).
|
|
</Accordion>
|
|
|
|
<Accordion title="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.
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
## Related pages
|
|
|
|
- [Environment variables](/self-hosting/environment-variables): every setting, with defaults.
|
|
- [Deployment examples](/self-hosting/examples): reverse proxies, S3 storage, Redis and local AI.
|
|
- [Job search and AI](/self-hosting/job-search-and-ai): Firecrawl, SearXNG and shared or personal AI providers.
|
|
- [Self-hosting with Kubernetes](/self-hosting/kubernetes): the same setup as Kubernetes manifests.
|
|
- [Checking service status](/guides/checking-service-status): the status of the hosted instance.
|