add more guides on migration and self-hosting docker compose examples, change launch date for banner, update dependencies

This commit is contained in:
Amruth Pillai
2026-01-20 10:35:37 +01:00
parent 7e5597271b
commit b87e5dd023
11 changed files with 901 additions and 416 deletions
+463
View File
@@ -0,0 +1,463 @@
---
title: "Self-Hosting with Docker"
description: "A comprehensive guide to self-host Reactive Resume with Docker (Postgres + Gotenberg), including a detailed environment variable reference and troubleshooting tips."
---
## Overview
Reactive Resume can be self-hosted using Docker in a matter of minutes, and this guide will walk you through the process. Here are some of the services you'll need to get started:
<CardGroup cols={2}>
<Card title="PostgreSQL">
Stores accounts, resumes, and application data.
</Card>
<Card title="Gotenberg">
Generates PDFs by rendering a special print route.
</Card>
<Card title="Email (optional)">
SMTP for verification emails, password reset, etc. If not configured, emails are logged to the server console.
</Card>
<Card title="Storage (optional)">
Use S3-compatible storage, or local persistent storage via <code>/app/data</code>.
</Card>
</CardGroup>
You can pull the latest app image from:
- Docker Hub: `amruthpillai/reactive-resume:latest`
- GitHub Container Registry: `ghcr.io/amruthpillai/reactive-resume:latest`
## Minimum requirements
<CardGroup cols={1}>
<Card title="Docker + Docker Compose">
Docker Engine + Docker Compose plugin (or Docker Desktop).
</Card>
<Card title="Compute">
2 vCPU / 2 GB RAM minimum (4 GB recommended if Postgres + Gotenberg run on the same host).
</Card>
<Card title="Storage">
Enough for Postgres + uploads (start with 10-20 GB and scale as needed).
</Card>
</CardGroup>
## Quickstart using Docker Compose
Create a new folder (for example `reactive-resume/`) with:
- `compose.yml`
- `.env`
- a persistent data directory for uploads (for example `./data`)
<Steps>
<Step title="Create your .env">
Start by creating a `.env` file next to your `compose.yml`.
```bash .env
# --- Server ---
TZ="Etc/UTC"
APP_URL="http://localhost:3000"
# Optional, uses APP_URL by default
# This can be set to a different URL (like http://host.docker.internal:3000 or http://{docker_service}:3000)
# to let the browser navigate to a non-public instance of Reactive Resume
PRINTER_APP_URL="http://host.docker.internal:3000"
# --- Printer ---
GOTENBERG_ENDPOINT="http://gotenberg:3000"
# Gotenberg Authentication (Optional)
# GOTENBERG_USERNAME=""
# GOTENBERG_PASSWORD=""
# --- Database (PostgreSQL) ---
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres"
# --- Authentication ---
# Generated using `openssl rand -hex 32`
AUTH_SECRET=""
# Social Auth (Google, optional)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Social Auth (GitHub, optional)
GITHUB_CLIENT_ID=""
GITHUB_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="465"
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM="Reactive Resume <noreply@rxresu.me>"
SMTP_SECURE="false"
# --- Storage (optional) ---
# If all keys are disabled, the app uses local filesystem (/data) to store uploads 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"
# --- Feature Flags ---
FLAG_DEBUG_PRINTER="false"
FLAG_DISABLE_SIGNUP="false"
# --- Others ---
# GOOGLE_CLOUD_API_KEY=""
```
</Step>
<Step title="Generate AUTH_SECRET">
Generate a strong secret and paste it into `AUTH_SECRET`.
<CodeGroup>
```bash Linux/macOS
openssl rand -hex 32
```
```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
```
```cmd Windows (alternative)
certutil -generateSRS 32 | findstr /r /v "^$" | findstr /v ":" | findstr /v " " | findstr /v "-" | findstr /v "certutil"
```
</CodeGroup>
</Step>
<Step title="Create compose.yml">
This setup runs Postgres + Gotenberg + Reactive Resume on a private Docker network.
<CodeGroup>
```yaml compose.yml
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 10
gotenberg:
image: gotenberg/gotenberg:edge
restart: unless-stopped
ports:
- "4000:3000"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- CHROMIUM_AUTO_START=true
- LIBREOFFICE_DISABLE_ROUTES=true
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 10
app:
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
gotenberg:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 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>
</Step>
<Step title="Start the stack">
<CodeGroup>
```bash
docker compose up -d
```
```bash
docker compose ps
```
```bash
docker compose logs -f reactive-resume
```
</CodeGroup>
Reactive Resume should now be available at your `APP_URL` (for the example above: `http://localhost:3000`).
</Step>
</Steps>
## How startup works (database migrations)
<Info>
On every start, the server <b>automatically runs database migrations</b> before serving traffic. If migrations fail (usually due to a DB connection issue), the container will exit with an error.
</Info>
## Environment variables
<CardGroup cols={2}>
<Card title="Required">
<ul>
<li><code>APP_URL</code></li>
<li><code>DATABASE_URL</code></li>
<li><code>GOTENBERG_ENDPOINT</code></li>
<li><code>AUTH_SECRET</code></li>
</ul>
</Card>
<Card title="Optional">
<ul>
<li>SMTP (<code>SMTP_&#42;</code>)</li>
<li>Social auth (<code>GOOGLE_&#42;</code>, <code>GITHUB_&#42;</code>)</li>
<li>S3 storage (<code>S3_&#42;</code>)</li>
<li>Feature flags (<code>FLAG_&#42;</code>)</li>
</ul>
</Card>
</CardGroup>
<AccordionGroup>
<Accordion title="Server">
- **`TZ`**: Sets the container timezone (affects logs and server-side timestamps). Recommended: `Etc/UTC`.
- **`APP_URL`**: Canonical/public URL for your instance (used for absolute URLs, redirects, and auth flows). If behind a reverse proxy, set this to your public HTTPS URL (for example, `https://resume.example.com`).
- **`PRINTER_APP_URL`** (optional): Overrides the base URL used when rendering the print route for Gotenberg. Defaults to `APP_URL`. Useful when Gotenberg must access the app via a different internal URL (for example, `http://host.docker.internal:3000`).
</Accordion>
<Accordion title="Printer (Gotenberg)">
- **`GOTENBERG_ENDPOINT`**: Base URL where Reactive Resume reaches Gotenberg. In Compose: `http://gotenberg:3000`.
- **`GOTENBERG_USERNAME`** / **`GOTENBERG_PASSWORD`** (optional): Use if Gotenberg is configured with Basic Auth (recommended only if Gotenberg is reachable outside your private network).
</Accordion>
<Accordion title="Database (PostgreSQL)">
- **`DATABASE_URL`**: Postgres connection string in the format `postgresql://USER:PASSWORD@HOST:PORT/DATABASE`.
- In Docker Compose, set `HOST` to the Postgres service name (e.g. `postgres`), not `localhost`.
</Accordion>
<Accordion title="Authentication">
**`AUTH_SECRET`**: Secret used to secure authentication. Changing it invalidates existing sessions.
Generate with:
<CodeGroup>
```bash
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.
**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`
</Accordion>
<Accordion title="Email (SMTP, optional)">
If SMTP is not configured, the app logs emails to the server console instead of sending them.
- **`SMTP_HOST`**: SMTP host (if empty, email sending is disabled).
- **`SMTP_PORT`**: Usually `465` (implicit TLS) or `587` (STARTTLS).
- **`SMTP_USER`** / **`SMTP_PASS`**: SMTP credentials.
- **`SMTP_FROM`**: Default from address (for example, `Reactive Resume <noreply@rxresu.me>`).
- **`SMTP_SECURE`**: `"true"` or `"false"` (string). Match your provider settings.
</Accordion>
<Accordion title="Storage (S3 or local)">
- **Default (local)**: If all `S3_*` values are empty, uploads are stored under `/app/data`. Mount it to persistent storage (for example `./data:/app/data`) or uploads can be lost on container recreation.
- **S3/S3-compatible**: Configure these to store uploads in an S3-compatible service (SeaweedFS, MinIO, AWS S3, etc.):
- **`S3_ACCESS_KEY_ID`**
- **`S3_SECRET_ACCESS_KEY`**
- **`S3_REGION`**
- **`S3_ENDPOINT`** (for S3-compatible providers; may be blank for AWS depending on your setup)
- **`S3_BUCKET`**
- **`S3_FORCE_PATH_STYLE`**: Controls how the bucket is addressed in URLs. Defaults to `"false"`.
- Set to `"true"` for **path-style** URLs (`https://s3-server.com/bucket`). Common with **MinIO**, **SeaweedFS**, and other self-hosted S3-compatible services.
- Set to `"false"` for **virtual-hosted-style** URLs (`https://bucket.s3-server.com`). Common with **AWS S3**, **Cloudflare R2**, and most cloud providers.
</Accordion>
<Accordion title="Feature Flags">
- **`FLAG_DEBUG_PRINTER`**: Bypasses the printer-only access restriction (useful when debugging `/printer/{resumeId}`). Recommended: keep `"false"` in production.
- **`FLAG_DISABLE_SIGNUP`**: Disables new signups (web app and server). Useful for private instances.
</Accordion>
</AccordionGroup>
## Updating your installation
To update your Reactive Resume installation to the latest available version, follow these steps:
1. **Pull the latest images** for all services defined in your Docker Compose file.
```bash
docker compose pull
```
2. **Restart the containers** to run the new images.
```bash
docker compose up -d
```
3. **(Optional) Remove old, unused Docker images** to free up disk space.
```bash
docker image prune -f
```
This process ensures your app, database, and printer are all up-to-date while keeping your data and configuration intact.
## Backups (recommended)
Regular backups are essential to protect your data. Reactive Resume stores data in two places: the PostgreSQL database and file uploads (either local storage or S3).
### Database backups
Your PostgreSQL database contains all user accounts, resumes, and application data. For self-hosted deployments, you can use `pg_dump` to create periodic backups of your database and store them in a secure location. Many hosting providers also offer automated backup solutions for managed PostgreSQL instances, which handle scheduling, retention, and restoration for you.
### Upload backups
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.
## Health Checks
Reactive Resume exposes a health check endpoint at `/api/health` that verifies the application and its dependencies are functioning correctly. If any critical service (such as the database connection) fails, the health check will return an unhealthy status.
### How it works
The Docker Compose configuration includes a health check that periodically calls the `/api/health` endpoint:
```yaml
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 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`.
### Reverse proxy integration
Most reverse proxies (such as **Traefik**, **Caddy**, or **nginx** with upstream health checks) can use Docker's health status to make routing decisions:
- **Healthy containers** receive traffic as normal
- **Unhealthy containers** are automatically removed from the load balancer pool
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 its database connection), 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>
### Manually checking health
You can manually verify the health of your Reactive Resume instance:
```bash
# 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. Any other response (or a connection failure) indicates a problem that should be investigated in the container logs.
## Troubleshooting
<AccordionGroup>
<Accordion title="The app container exits immediately">
- **Common cause**: database migrations failed (often a bad `DATABASE_URL`).
- **What to do**:
When the app container exits right away, you'll want to check the logs for more information about the error. Run the following command to view real-time logs from the Reactive Resume container:
```bash
docker compose logs -f reactive-resume
```
</Accordion>
<Accordion title="Can't sign in / redirects loop / cookies don't stick">
- **Common cause**: `APP_URL` doesn't match the URL you're actually using (especially behind a reverse proxy).
- **Fix**: set `APP_URL` to the public URL (preferably HTTPS) and restart the container.
</Accordion>
<Accordion title="PDF export fails / printing is stuck">
- **Common cause**: Reactive Resume can't reach Gotenberg or Gotenberg can't reach your app.
- **Checks**:
- `GOTENBERG_ENDPOINT` should usually be `http://gotenberg:3000` in Compose.
- If you use `PRINTER_APP_URL="http://host.docker.internal:3000"`, ensure `extra_hosts: host-gateway` is present for Gotenberg.
</Accordion>
<Accordion title="Uploads disappear after restart">
- **Cause**: you didn't mount persistent storage for `/app/data` (when not using S3).
- **Fix**: add a volume mount like `./data:/app/data` and redeploy.
</Accordion>
<Accordion title="Emails aren't being delivered">
- **Expected behavior**: if SMTP vars are empty, the app logs emails to the console instead.
- **Fix**: configure SMTP and verify your provider's TLS/port settings.
</Accordion>
<Accordion title="S3 storage error: ENOTFOUND bucket.endpoint">
- **Common cause**: The S3 client is using virtual-hosted-style addressing (prepending the bucket name to the endpoint), but your S3-compatible storage expects path-style addressing.
- **Symptom**: Error message like `getaddrinfo ENOTFOUND mybucket.s3-server.com` when your endpoint is `s3-server.com`.
- **Fix**: Set `S3_FORCE_PATH_STYLE="true"` in your environment. This is required for most self-hosted S3-compatible services like MinIO, SeaweedFS, etc.
</Accordion>
</AccordionGroup>
+567
View File
@@ -0,0 +1,567 @@
---
title: "Docker Compose Examples"
description: "A collection of Docker Compose examples for different deployment scenarios. If you have a different setup that works for you, please share it by opening a pull request on GitHub."
---
## Overview
Every self-hosted setup is unique. You might be running on a single VPS, a Kubernetes cluster, behind Cloudflare Tunnel, or using a specific reverse proxy like Traefik or nginx. This page provides real-world Docker Compose configurations for various deployment scenarios to help you get started faster.
These examples go beyond the basic setup in the [Self-Hosting with Docker](/self-hosting/docker) guide, showing production-ready configurations with reverse proxies, SSL termination, and other common patterns.
<Info>
**Help others by sharing your setup!** If you have a working configuration that isn't covered here, I'd love to include it. Simply [open a pull request](https://github.com/amruthpillai/reactive-resume) with your example added to this page. Your contribution helps the community and makes self-hosting easier for everyone.
</Info>
---
## Docker with Traefik
This example uses [Traefik](https://traefik.io/) as a reverse proxy with automatic SSL certificate management via Let's Encrypt. Only the Reactive Resume app is exposed through Traefik—Postgres and Gotenberg remain on an internal network.
<Tip>
Traefik automatically discovers services via Docker labels and handles SSL certificates, making it ideal for setups where you want minimal configuration.
</Tip>
```yaml compose-traefik.yml lines expandable
services:
traefik:
image: traefik:v3.2
restart: unless-stopped
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
networks:
- reactive_resume_network
labels:
- "traefik.enable=true"
# Dashboard (optional, remove if not needed)
- "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- reactive_resume_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 5
gotenberg:
image: gotenberg/gotenberg:8
restart: unless-stopped
command:
- "gotenberg"
- "--chromium-auto-start"
- "--api-timeout=120s"
networks:
- reactive_resume_network
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
app:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
environment:
- APP_URL=https://resume.${DOMAIN}
- PRINTER_APP_URL=http://app:3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- GOTENBERG_ENDPOINT=http://gotenberg:3000
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
- app_data:/app/data
networks:
- reactive_resume_network
depends_on:
postgres:
condition: service_healthy
gotenberg:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.reactive-resume.rule=Host(`resume.${DOMAIN}`)"
- "traefik.http.routers.reactive-resume.entrypoints=websecure"
- "traefik.http.routers.reactive-resume.tls.certresolver=letsencrypt"
- "traefik.http.services.reactive-resume.loadbalancer.server.port=3000"
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""]
interval: 30s
timeout: 10s
retries: 3
networks:
reactive_resume_network:
driver: bridge
volumes:
traefik_letsencrypt:
postgres_data:
app_data:
```
**Environment variables (`.env`):**
```bash .env
DOMAIN="example.com"
ACME_EMAIL="admin@example.com"
POSTGRES_PASSWORD="your-secure-postgres-password"
AUTH_SECRET="your-auth-secret-from-openssl-rand-hex-32"
# Optional: Traefik dashboard auth (generate with: htpasswd -nb admin password)
TRAEFIK_DASHBOARD_AUTH="admin:$$apr1$$..."
```
---
## Docker with nginx
This example uses [nginx](https://nginx.org/) as a reverse proxy with SSL certificates (you'll need to provide your own certificates or use certbot separately).
```yaml compose-nginx.yml lines expandable
services:
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
networks:
- reactive_resume_network
depends_on:
- app
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- reactive_resume_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 5
gotenberg:
image: gotenberg/gotenberg:8
restart: unless-stopped
command:
- "gotenberg"
- "--chromium-auto-start"
- "--api-timeout=120s"
networks:
- reactive_resume_network
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
app:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
environment:
- APP_URL=https://resume.${DOMAIN}
- PRINTER_APP_URL=http://app:3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- GOTENBERG_ENDPOINT=http://gotenberg:3000
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
- app_data:/app/data
networks:
- reactive_resume_network
depends_on:
postgres:
condition: service_healthy
gotenberg:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""]
interval: 30s
timeout: 10s
retries: 3
networks:
reactive_resume_network:
driver: bridge
volumes:
postgres_data:
app_data:
```
**nginx configuration (`nginx.conf`):**
```nginx nginx.conf lines expandable
events {
worker_connections 1024;
}
http {
upstream reactive_resume {
server app:3000;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name resume.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Proxy settings
location / {
proxy_pass http://reactive_resume;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts for long-running requests (PDF generation)
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Increase max body size for resume uploads
client_max_body_size 10M;
}
}
```
<Tip>
For automatic SSL certificates with nginx, consider using [certbot](https://certbot.eff.org/) with the `--nginx` plugin, or a companion container like [nginx-proxy-acme](https://github.com/nginx-proxy/acme-companion).
</Tip>
---
## Docker Swarm
This example demonstrates a production-grade Docker Swarm deployment with multiple replicas, health checks, rolling updates, and Traefik integration. It includes SeaweedFS for S3-compatible storage and a PostgreSQL database with custom configuration.
<Tip>
Docker Swarm is great for multi-node deployments where you need high availability and easy scaling. The app service is configured with 2 replicas and rolling update strategy.
</Tip>
```yaml compose-swarm.yml lines expandable
services:
postgres:
image: postgres:18
command: postgres -c config_file=/etc/postgresql.conf
networks:
- reactive_resume_network
volumes:
- reactive_resume_postgres_data:/var/lib/postgresql
environment:
- POSTGRES_DB=$POSTGRES_DB
- POSTGRES_USER=$POSTGRES_USER
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
configs:
- source: reactive_resume_postgres_config
target: /etc/postgresql.conf
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
mode: replicated
replicas: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
gotenberg:
image: gotenberg/gotenberg:8
networks:
- reactive_resume_network
environment:
- WEBHOOK_DISABLE=true
- CHROMIUM_AUTO_START=true
- API_ENABLE_BASIC_AUTH=true
- PROMETHEUS_DISABLE_COLLECT=true
- GOTENBERG_API_BASIC_AUTH_USERNAME=$GOTENBERG_USERNAME
- GOTENBERG_API_BASIC_AUTH_PASSWORD=$GOTENBERG_PASSWORD
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
mode: replicated
replicas: 2
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
seaweedfs:
image: chrislusf/seaweedfs:latest
command: server -s3 -filer -dir=/data -ip=0.0.0.0
networks:
- reactive_resume_network
volumes:
- reactive_resume_seaweedfs_data:/data
environment:
- AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8888"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
mode: replicated
replicas: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
seaweedfs_create_bucket:
image: quay.io/minio/mc:latest
entrypoint: >
/bin/sh -c "
until mc alias set seaweedfs http://seaweedfs:8333 $S3_ACCESS_KEY_ID $S3_SECRET_ACCESS_KEY; do
echo 'Waiting for SeaweedFS...';
sleep 2;
done;
mc mb seaweedfs/$S3_BUCKET --ignore-existing;
"
networks:
- reactive_resume_network
deploy:
mode: replicated
replicas: 1
restart_policy:
condition: on-failure
delay: 10s
max_attempts: 5
window: 120s
app:
image: ghcr.io/amruthpillai/reactive-resume:latest
networks:
- traefik_network
- reactive_resume_network
volumes:
- reactive_resume_app_data:/app/data
environment:
- APP_URL=$APP_URL
- GOTENBERG_ENDPOINT=$GOTENBERG_ENDPOINT
- GOTENBERG_USERNAME=$GOTENBERG_USERNAME
- GOTENBERG_PASSWORD=$GOTENBERG_PASSWORD
- DATABASE_URL=$DATABASE_URL
- AUTH_SECRET=$AUTH_SECRET
- GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
- GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET
- GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID
- GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET
- SMTP_HOST=$SMTP_HOST
- SMTP_PORT=$SMTP_PORT
- SMTP_USER=$SMTP_USER
- SMTP_PASS=$SMTP_PASS
- SMTP_FROM=$SMTP_FROM
- SMTP_SECURE=$SMTP_SECURE
- S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
- S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
- S3_REGION=$S3_REGION
- S3_ENDPOINT=$S3_ENDPOINT
- S3_BUCKET=$S3_BUCKET
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
mode: replicated
replicas: 2
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
order: start-first
rollback_config:
parallelism: 1
delay: 10s
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`rxresu.me`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls=true"
- "traefik.http.services.app.loadbalancer.server.port=3000"
configs:
reactive_resume_postgres_config:
name: reactive_resume_postgres_config
external: true
networks:
traefik_network:
external: true
reactive_resume_network:
name: reactive_resume_network
driver: overlay
attachable: true
volumes:
reactive_resume_postgres_data:
name: reactive_resume_postgres_data
reactive_resume_seaweedfs_data:
name: reactive_resume_seaweedfs_data
reactive_resume_app_data:
name: reactive_resume_app_data
```
**Deploy the stack:**
```bash
docker stack deploy -c compose-swarm.yml reactive_resume
```
**Useful commands:**
```bash
# Check service status
docker stack services reactive_resume
# View logs for the app
docker service logs -f reactive_resume_app
# Scale the app
docker service scale reactive_resume_app=3
# Remove the stack
docker stack rm reactive_resume
```
<Note>
This example assumes you have an external Traefik network already set up. Adjust the `traefik_network` reference and labels based on your Traefik configuration.
</Note>
---
## Contributing Your Setup
Have a different deployment setup that works well? Consider contributing it here. Some examples include:
- Kubernetes / Helm charts
- Cloudflare Tunnel
- Caddy reverse proxy
- Docker with Portainer
- Podman configurations
- Cloud-specific deployments (AWS ECS, Google Cloud Run, Azure Container Apps)
To contribute, [open a pull request](https://github.com/amruthpillai/reactive-resume) with your example added to this page. Include:
1. A brief description of when/why someone would use this setup
2. The complete Docker Compose (or equivalent) configuration
3. Any additional configuration files (nginx.conf, etc.)
4. Required environment variables
+305
View File
@@ -0,0 +1,305 @@
---
title: "Migrating from v4 to v5"
description: "A step-by-step guide to migrate your Reactive Resume instance from v4 to v5, including manual and automated migration options."
---
## Overview
This guide walks you through migrating your Reactive Resume installation from **v4 to v5**. The migration process involves setting up a new v5 instance alongside your existing v4 instance, then transferring your users and resumes to the new system.
<Warning>
**Keep your v4 instance running** until you have successfully migrated all data to v5 and verified everything works correctly. This ensures you have a fallback in case anything goes wrong during the migration.
</Warning>
## Prerequisites
Before starting the migration, ensure you have:
<CardGroup cols={2}>
<Card title="Running v4 Instance">
Your existing Reactive Resume v4 instance should be running and accessible.
</Card>
<Card title="New v5 Instance">
A fresh Reactive Resume v5 instance set up and running. Follow the [Self-Hosting with Docker](/self-hosting/docker) guide if you haven't done this yet.
</Card>
<Card title="Database Access">
Access to both your v4 PostgreSQL database (source) and v5 PostgreSQL database (target).
</Card>
<Card title="Backup">
A recent backup of your v4 database. Always backup before any migration.
</Card>
</CardGroup>
## Choosing a Migration Method
The best migration approach depends on the size of your instance:
<CardGroup cols={2}>
<Card title="Manual Migration" icon="hand">
**Best for**: Small instances with a handful of resumes.
Uses the built-in Import Dialog to manually convert resumes one at a time.
</Card>
<Card title="Automated Migration" icon="robot">
**Best for**: Large instances with many users and resumes.
Uses migration scripts to batch-process all users and resumes automatically.
</Card>
</CardGroup>
## Manual Migration (Small Instances)
If you have only a few resumes to migrate, the simplest approach is to use the **Import Dialog** feature in v5.
<Steps>
<Step title="Export from v4">
In your v4 instance, go to each resume and export it as JSON. This creates a portable file containing all your resume data.
</Step>
<Step title="Import into v5">
In your new v5 instance:
1. Log in or create a new account
2. Click **Create Resume** or use the **Import** option
3. Select the **Reactive Resume v4** format
4. Upload your exported JSON file
The import process automatically converts the v4 format to v5.
</Step>
<Step title="Verify and repeat">
Review the imported resume to ensure all data transferred correctly. Repeat for each resume you need to migrate.
</Step>
</Steps>
<Tip>
The Import Dialog handles the schema conversion automatically, so you don't need to worry about format differences between v4 and v5.
</Tip>
## Automated Migration (Large Instances)
For instances with many users and resumes, use the migration scripts to automate the process. The migration happens in two phases: first users, then resumes.
### Requirements
To run the migration scripts, you need the following installed on your host machine:
<CardGroup cols={1}>
<Card title="Node.js Runtime">
**tsx** - TypeScript execution environment. Install globally with:
```bash
npm install -g tsx
```
</Card>
<Card title="Environment Loader">
**dotenvx** (or any tool to load `.env` files). Install globally with:
```bash
npm install -g @dotenvx/dotenvx
```
Alternatively, you can use `dotenv`, `direnv`, or export the variables manually.
</Card>
<Card title="Reactive Resume Source Code">
Clone the Reactive Resume repository to access the migration scripts:
```bash
git clone https://github.com/amruthpillai/reactive-resume.git
cd reactive-resume
pnpm install
```
</Card>
</CardGroup>
### Environment Setup
Create a `.env` file in the root of the repository with the following variables:
```bash .env
# Connection string to your NEW v5 PostgreSQL database (target)
DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v5"
# Connection string to your OLD v4 PostgreSQL database (source)
PRODUCTION_DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v4"
```
<Warning>
Double-check your connection strings! `DATABASE_URL` should point to your **new v5 database** and `PRODUCTION_DATABASE_URL` should point to your **old v4 database**. Mixing these up could cause data loss.
</Warning>
### Step 1: Migrate Users
The user migration script transfers all user accounts, authentication data, and two-factor settings from v4 to v5.
```bash
dotenvx run -- tsx scripts/migration/user.ts
```
**What this script does:**
- Fetches users in batches from the v4 database
- Creates corresponding user accounts in the v5 database
- Migrates authentication providers (email, Google, GitHub, custom OAuth)
- Preserves two-factor authentication settings and backup codes
- Creates a mapping file (`scripts/migration/user-id-map.json`) that links old user IDs to new ones
<Info>
The script saves progress automatically. If interrupted (Ctrl+C), you can run it again and it will resume from where it left off.
</Info>
**Expected output:**
```
⌛ Starting user migration...
📥 Fetching users batch from production database (OFFSET 0)...
📋 Found 1000 users in this batch.
📝 Preparing to bulk insert 1000 users...
✅ Bulk inserted 1000 users in 245.3 ms (avg 0.2 ms/user)
💾 Progress saved at offset 1000
📦 Processed 1000 users so far...
📊 Migration Summary:
Users created: 1000
Accounts created: 1000
Two-factor entries created: 50
Skipped (already exist): 0
⏱️ Total migration time: 1234.5 ms (1.23 seconds)
✅ User migration complete!
```
### Step 2: Migrate Resumes
After users are migrated, run the resume migration script. This script depends on the user ID mapping created in the previous step.
```bash
dotenvx run -- tsx scripts/migration/resume.ts
```
**What this script does:**
- Fetches resumes in batches from the v4 database
- Converts each resume from v4 format to v5 format automatically
- Links resumes to the correct users using the ID mapping
- Migrates resume statistics (views, downloads)
- Preserves visibility settings (public/private) and lock status
<Info>
Like the user script, the resume migration also saves progress and can be resumed if interrupted.
</Info>
**Expected output:**
```
⌛ Starting resume migration...
📥 Fetching resumes batch from production database (OFFSET 0)...
📋 Found 2500 resumes in this batch.
📝 Preparing to bulk insert 2500 resumes...
✅ Bulk inserted 2500 resumes in 892.1 ms (avg 0.4 ms/resume)
💾 Progress saved at offset 2500
📦 Processed 2500 resumes so far...
📊 Migration Summary:
Resumes created: 2500
Statistics created: 2500
Skipped (userId not found or already exist): 0
Errors: 0
⏱️ Total migration time: 5678.9 ms (5.68 seconds)
✅ Resume migration complete!
```
### Progress and Recovery
Both migration scripts support graceful shutdown and resume:
- **Progress files**: `scripts/migration/user-progress.json` and `scripts/migration/resume-progress.json` track the current migration state
- **User ID mapping**: `scripts/migration/user-id-map.json` maps v4 user IDs to v5 user IDs
- **Graceful shutdown**: Press `Ctrl+C` to stop the migration safely. Progress is saved before exit.
- **Resume migration**: Run the script again to continue from where you left off
<Tip>
If you need to restart the migration from scratch, delete the progress files and the user ID mapping file before running the scripts again.
</Tip>
## Post-Migration Steps
After completing the migration:
<Steps>
<Step title="Verify data integrity">
Log into your v5 instance and spot-check several user accounts and resumes to ensure data transferred correctly.
</Step>
<Step title="Test functionality">
- Create a test resume and export it as PDF
- Verify social logins work (if configured)
- Check that two-factor authentication works for migrated users
</Step>
<Step title="Update DNS/Proxy">
Once verified, update your DNS records or reverse proxy to point to the new v5 instance.
</Step>
<Step title="Decommission v4">
After confirming everything works and allowing a grace period, you can safely shut down your v4 instance.
</Step>
</Steps>
## Important Notes
<AccordionGroup>
<Accordion title="User passwords are preserved">
Users who signed up with email/password can continue using their existing passwords. No password reset is required after migration.
</Accordion>
<Accordion title="Profile pictures are not migrated">
User profile pictures (avatars) are stored as references in the database. If you were using S3 storage, ensure your v5 instance has access to the same bucket, or users may need to re-upload their avatars.
</Accordion>
<Accordion title="Resume images and uploads">
Similar to profile pictures, any images embedded in resumes need to be accessible from your v5 instance. Consider migrating your storage bucket or updating references as needed.
</Accordion>
<Accordion title="OAuth provider changes">
If you're using custom OAuth providers, ensure the same providers are configured in v5 with matching client IDs. Users authenticate with the same provider ID, so mismatched configurations will cause login failures.
</Accordion>
<Accordion title="Schema differences">
The v5 schema has some changes from v4:
- `visibility` (public/private) is now `isPublic` (boolean)
- Resume `title` is now `name`
- Some resume data fields have been reorganized
The migration scripts handle these conversions automatically.
</Accordion>
</AccordionGroup>
## Troubleshooting
<AccordionGroup>
<Accordion title="Script fails with 'PRODUCTION_DATABASE_URL is not set'">
Ensure your `.env` file contains both `DATABASE_URL` and `PRODUCTION_DATABASE_URL`, and that you're using a tool like `dotenvx` to load them before running the script.
</Accordion>
<Accordion title="Users are skipped during migration">
Users are skipped if:
- Their email already exists in the v5 database
- Their username already exists in the v5 database
- They were already migrated in a previous run
Check the console output for skip reasons.
</Accordion>
<Accordion title="Resumes are skipped during migration">
Resumes are skipped if:
- The associated user wasn't migrated (user ID not in mapping file)
- A resume with the same slug already exists for that user
- They were already migrated in a previous run
</Accordion>
<Accordion title="Resume data parsing fails">
If a resume can't be parsed from v4 format, it will be created with default empty data. Check the console output for warnings about specific resumes, and consider manually importing those using the Import Dialog.
</Accordion>
<Accordion title="Migration is slow">
The scripts process data in batches to avoid overwhelming the database. For very large instances:
- Consider running the migration during off-peak hours
- Ensure both databases have adequate resources
- The batch size can be adjusted in the script files if needed
</Accordion>
</AccordionGroup>