docs(self-hosting): refresh docker and setup guidance

Clarify self-hosting, migration, and quickstart instructions to reflect the new secure defaults and deployment flow.

Made-with: Cursor
This commit is contained in:
Amruth Pillai
2026-04-25 15:30:47 +02:00
parent 9e1e593dc4
commit c0387298c5
6 changed files with 249 additions and 119 deletions
+10 -3
View File
@@ -4,12 +4,19 @@ description: "List of all notable changes and updates to Reactive Resume"
rss: true
---
<Update label="v5.0.19" description="22nd April 2026">
<Update label="v5.0.19" description="25th April 2026">
## Security
- Pinned some packages to specific versions to avoid potential security vulnerabilities through transitive dependencies. (through running `pnpm audit`)
- Hardened resume export and printer access controls to prevent unauthorized data access.
- PDF export is now owner-only; the public share-page download action has been removed.
- Strengthened protections for password verification, OAuth client registration, and redirect URI validation, including tighter rate limits.
- Blocked network-loading custom CSS patterns and improved CSS sanitization/scoping safeguards.
- Reduced SSRF risk by tightening URL validation for AI provider base URLs and user picture URLs.
- Disabled public auth API reference/schema exposure in production-facing auth routes.
## Maintenance
- Updated dependencies and lockfile.
- Updated dependencies and lockfile (including security-focused packages and typings).
- Added targeted tests for auth, printer/export, URL validation, and CSS sanitization hardening.
- Improved Docker Compose security defaults (safer Browserless token/image/network configuration).
- Synced translation catalogs from Crowdin.
</Update>
+95 -56
View File
@@ -67,28 +67,60 @@ Before you begin, ensure you have the following installed:
<Steps>
<Step title="Clone the Repository">
```bash git clone https://github.com/amruthpillai/reactive-resume.git cd Reactive-Resume ```
```bash
git clone https://github.com/amruthpillai/reactive-resume.git
cd Reactive-Resume
```
</Step>
<Step title="Configure Environment Variables">
Create a `.env` file in the root directory with the following variables: ```bash # Application
APP_URL=http://localhost:3000 # Database DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres #
Authentication (generate a secure secret) AUTH_SECRET=your-secure-secret-key-here # Storage (S3-compatible)
S3_ACCESS_KEY_ID=seaweedfs S3_SECRET_ACCESS_KEY=seaweedfs S3_ENDPOINT=http://seaweedfs:8333
S3_BUCKET=reactive-resume S3_FORCE_PATH_STYLE=true ```
Create a `.env` file in the root directory with the following variables:
```bash .env
# Application
APP_URL=http://localhost:3000
# Printer (Browserless)
BROWSERLESS_TOKEN=change-me
PRINTER_ENDPOINT=ws://browserless:3000?token=change-me
# Database
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres
# Authentication (generate a secure secret)
AUTH_SECRET=your-secure-secret-key-here
# Storage (S3-compatible via SeaweedFS)
S3_ACCESS_KEY_ID=seaweedfs
S3_SECRET_ACCESS_KEY=seaweedfs
S3_ENDPOINT=http://seaweedfs:8333
S3_BUCKET=reactive-resume
S3_FORCE_PATH_STYLE=true
```
<Warning>
For production deployments, always use strong, unique values for `AUTH_SECRET` and database credentials.
For production deployments, always use strong, unique values for `AUTH_SECRET`, `BROWSERLESS_TOKEN`, and database credentials.
</Warning>
</Step>
<Step title="Start the Services">
```bash docker compose up -d ``` This will start: - **PostgreSQL** — Database for storing user data and resumes -
**SeaweedFS** — S3-compatible storage for file uploads - **Printer** — PDF and screenshot generation service
(browserless/chromium) - **Reactive Resume** — The main application
```bash
docker compose up -d
```
This starts:
- **PostgreSQL** — Database for storing user data and resumes
- **SeaweedFS** — S3-compatible storage for file uploads
- **Browserless** — Headless Chromium service for PDF/screenshot generation
- **Reactive Resume** — The main application
</Step>
<Step title="Access Your Instance">
Once all services are running, access your Reactive Resume instance at: ``` http://localhost:3000 ```
Once all services are running, access your Reactive Resume instance at:
```text
http://localhost:3000
```
</Step>
</Steps>
@@ -97,12 +129,12 @@ Before you begin, ensure you have the following installed:
Here's what each service in the stack does:
| Service | Port | Description |
| ----------- | ---- | ----------------------------------------------------------- |
| `postgres` | 5432 | PostgreSQL database for storing all application data |
| `seaweedfs` | 8333 | S3-compatible object storage for file uploads |
| `printer` | 4000 | Headless Chromium service for PDF and screenshot generation |
| `app` | 3000 | The main Reactive Resume application |
| Service | Port | Description |
| ----------------- | ---------------------------------- | ----------------------------------------------------------- |
| `postgres` | 5432 | PostgreSQL database for storing all application data |
| `seaweedfs` | 8333 | S3-compatible object storage for file uploads |
| `browserless` | 3000 (internal) / 4000 (published) | Headless Chromium service for PDF and screenshot generation |
| `reactive_resume` | 3000 | The main Reactive Resume application |
### Health Checks
@@ -122,51 +154,58 @@ Here's a complete list of environment variables you can configure:
### Required Variables
| Variable | Description | Example |
| ------------------ | ------------------------------ | --------------------------------------- |
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` |
| `AUTH_SECRET` | Secret key for authentication | Generate with `openssl rand -base64 32` |
| `APP_URL` | Public URL of your Application | `https://rxresu.me` |
| `PRINTER_ENDPOINT` | URL of the printer service | `http://printer:3000` |
| Variable | Description | Example |
| ------------------ | ------------------------------ | ------------------------------------- |
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` |
| `AUTH_SECRET` | Secret key for authentication | Generate with `openssl rand -hex 32` |
| `APP_URL` | Public URL of your Application | `https://rxresu.me` |
| `PRINTER_ENDPOINT` | URL of the printer service | `ws://browserless:3000?token=...` |
### Optional Variables
| Variable | Description | Default |
| ------------------------------- | ----------------------------------------------------------- | ---------------------- |
| `PRINTER_APP_URL` | Public URL for the printer to access the Application | — |
| `GOOGLE_CLIENT_ID` | Google OAuth Client ID | — |
| `GOOGLE_CLIENT_SECRET` | Google OAuth Client Secret | — |
| `GITHUB_CLIENT_ID` | GitHub OAuth Client ID | — |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth Client Secret | — |
| `LINKEDIN_CLIENT_ID` | LinkedIn OAuth Client ID | — |
| `LINKEDIN_CLIENT_SECRET` | LinkedIn OAuth Client Secret | — |
| `OAUTH_PROVIDER_NAME` | Custom OAuth Provider Name | — |
| `OAUTH_CLIENT_ID` | Custom OAuth Client ID | — |
| `OAUTH_CLIENT_SECRET` | Custom OAuth Client Secret | — |
| `OAUTH_DISCOVERY_URL` | OIDC Discovery URL (use this OR manual URLs below) | — |
| `OAUTH_AUTHORIZATION_URL` | OAuth Authorization URL (manual config) | — |
| `OAUTH_TOKEN_URL` | OAuth Token URL (manual config) | — |
| `OAUTH_USER_INFO_URL` | OAuth User Info URL (manual config) | — |
| `OAUTH_SCOPES` | OAuth Scopes (space-separated) | `openid profile email` |
| `SMTP_HOST` | SMTP Server Host (for email features) | — |
| `SMTP_PORT` | SMTP Server Port | `587` |
| `SMTP_USER` | SMTP Username | |
| `SMTP_PASS` | SMTP Password | |
| `SMTP_FROM` | Default FROM address for emails | — |
| `SMTP_SECURE` | Use secure SMTP connection (`true` or `false`) | `false` |
| `S3_ACCESS_KEY_ID` | S3 Access Key | |
| `S3_SECRET_ACCESS_KEY` | S3 Secret Key | — |
| `S3_REGION` | S3 Region | `us-east-1` |
| `S3_ENDPOINT` | S3-compatible Endpoint URL | — |
| `S3_BUCKET` | S3 Bucket Name | |
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 (set `true` for MinIO/SeaweedFS) | `false` |
| `FLAG_DEBUG_PRINTER` | Used for debugging the printer route | `false` |
| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` |
| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` |
| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` |
| Variable | Description | Default |
| ------------------------------------- | ----------------------------------------------------------- | ---------------------- |
| `PRINTER_APP_URL` | Public URL for the printer to access the Application | — |
| `GOOGLE_CLIENT_ID` | Google OAuth Client ID | — |
| `GOOGLE_CLIENT_SECRET` | Google OAuth Client Secret | — |
| `GITHUB_CLIENT_ID` | GitHub OAuth Client ID | — |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth Client Secret | — |
| `LINKEDIN_CLIENT_ID` | LinkedIn OAuth Client ID | — |
| `LINKEDIN_CLIENT_SECRET` | LinkedIn OAuth Client Secret | — |
| `OAUTH_PROVIDER_NAME` | Custom OAuth Provider Name | — |
| `OAUTH_CLIENT_ID` | Custom OAuth Client ID | — |
| `OAUTH_CLIENT_SECRET` | Custom OAuth Client Secret | — |
| `OAUTH_DISCOVERY_URL` | OIDC Discovery URL (use this OR manual URLs below) | — |
| `OAUTH_AUTHORIZATION_URL` | OAuth Authorization URL (manual config) | — |
| `OAUTH_TOKEN_URL` | OAuth Token URL (manual config) | — |
| `OAUTH_USER_INFO_URL` | OAuth User Info URL (manual config) | — |
| `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` | Trusted HTTPS hosts/origins for dynamic OAuth redirects | — |
| `OAUTH_SCOPES` | OAuth Scopes (space-separated) | `openid profile email` |
| `BETTER_AUTH_API_KEY` | Better Auth dashboard API key | |
| `BETTER_AUTH_URL` | Better Auth base URL override (advanced) | `APP_URL` |
| `BETTER_AUTH_SECRET` | Better Auth secret override (advanced) | `AUTH_SECRET` |
| `AI_ALLOWED_BASE_URLS` | Allowlist for custom AI provider base URLs | — |
| `SMTP_HOST` | SMTP Server Host (for email features) | — |
| `SMTP_PORT` | SMTP Server Port | `587` |
| `SMTP_USER` | SMTP Username | — |
| `SMTP_PASS` | SMTP Password | |
| `SMTP_FROM` | Default FROM address for emails | — |
| `SMTP_SECURE` | Use secure SMTP connection (`true` or `false`) | `false` |
| `S3_ACCESS_KEY_ID` | S3 Access Key | — |
| `S3_SECRET_ACCESS_KEY` | S3 Secret Key | — |
| `S3_REGION` | S3 Region | `us-east-1` |
| `S3_ENDPOINT` | S3-compatible Endpoint URL | |
| `S3_BUCKET` | S3 Bucket Name | |
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 (set `true` for MinIO/SeaweedFS) | `false` |
| `FLAG_DEBUG_PRINTER` | Used for debugging the printer route | `false` |
| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` |
| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` |
| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` |
> **Note:** Some variables are only required for using related features (OAuth, SMTP, S3, etc.) and can be left unset if unused.
> **Health check behavior:** `/api/health` reports status for database, printer, and storage. A failure in any dependency returns HTTP `503`.
<Note>
**Hybrid Setup Note**: The `PRINTER_APP_URL` variable is required when running Reactive Resume outside of Docker while
the printer service is running inside Docker. In this scenario, the printer needs to reach your local app to render
+89 -43
View File
@@ -54,9 +54,9 @@ APP_URL="http://localhost:3000"
PRINTER_APP_URL="http://host.docker.internal:3000"
# --- Printer ---
# If using browserless with token authentication, include the token as a query parameter:
# PRINTER_ENDPOINT="ws://printer:3000?token=your-secret-token"
PRINTER_ENDPOINT="ws://printer:3000"
# Keep this token in sync with the Browserless TOKEN value.
BROWSERLESS_TOKEN="change-me"
PRINTER_ENDPOINT="ws://printer:3000?token=change-me"
# --- Database (PostgreSQL) ---
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres"
@@ -64,6 +64,8 @@ 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=""
@@ -87,20 +89,30 @@ OAUTH_DISCOVERY_URL=""
OAUTH_AUTHORIZATION_URL=""
OAUTH_TOKEN_URL=""
OAUTH_USER_INFO_URL=""
OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS=""
# Custom scopes (space-separated, defaults to "openid profile email")
OAUTH_SCOPES=""
# Optional Better Auth runtime overrides for advanced deployments:
# BETTER_AUTH_URL="https://auth.example.com"
# BETTER_AUTH_SECRET=""
# --- AI (optional) ---
# Comma-separated hostnames/origins for custom AI base URLs
# Example: api.openai.com,https://gateway.ai.vercel.com
AI_ALLOWED_BASE_URLS=""
# --- 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_PORT="587"
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.
# If all keys are disabled, the app uses local filesystem (usually /app/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=""
@@ -115,6 +127,7 @@ S3_FORCE_PATH_STYLE="false"
FLAG_DEBUG_PRINTER="false"
FLAG_DISABLE_SIGNUPS="false"
FLAG_DISABLE_EMAIL_AUTH="false"
FLAG_DISABLE_IMAGE_PROCESSING="false"
```
</Step>
@@ -134,9 +147,6 @@ FLAG_DISABLE_EMAIL_AUTH="false"
[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>
@@ -172,10 +182,9 @@ services:
- HEALTH=true
- CONCURRENT=20
- QUEUED=10
# Optional: Set a token for authentication
# - TOKEN=your-secret-token
- TOKEN=${BROWSERLESS_TOKEN}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"]
test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null']
interval: 10s
timeout: 5s
retries: 10
@@ -282,11 +291,15 @@ docker compose logs -f reactive-resume
SMTP (<code>SMTP_&#42;</code>)
</li>
<li>
Social auth (<code>GOOGLE_&#42;</code>, <code>GITHUB_&#42;</code>)
Social auth (<code>GOOGLE_&#42;</code>, <code>GITHUB_&#42;</code>, <code>LINKEDIN_&#42;</code>,{" "}
<code>OAUTH_&#42;</code>)
</li>
<li>
S3 storage (<code>S3_&#42;</code>)
</li>
<li>
AI URL allowlist (<code>AI_ALLOWED_BASE_URLS</code>)
</li>
<li>
Feature flags (<code>FLAG_&#42;</code>)
</li>
@@ -302,7 +315,9 @@ docker compose logs -f reactive-resume
</Accordion>
<Accordion title="Printer">
- **`PRINTER_ENDPOINT`**: Base URL where Reactive Resume reaches the printer service. In Compose: `http://printer:3000`. If using browserless with token authentication, include the token as a query parameter: `ws://printer:3000?token=your-secret-token`.
- **`PRINTER_ENDPOINT`**: Endpoint where Reactive Resume connects to the printer browser.
- **Recommended (Browserless)**: `ws://printer:3000?token=...` and keep the token value in sync with Browserless `TOKEN`.
- **Also supported**: `http://chrome:9222` for Chrome DevTools Protocol endpoints.
<Note>
**Alternative to browserless**: You can use a lightweight headless Chrome Docker image like `chromedp/headless-shell`:
@@ -322,7 +337,9 @@ docker compose logs -f reactive-resume
<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`.
Docker Compose, set `HOST` to the Postgres service name (e.g. `postgres`), not `localhost`. - If your password
contains special characters (`@`, `#`, `:`), URL-encode it. - For managed Postgres, add provider-specific params (for
example `?sslmode=require`) when needed.
</Accordion>
<Accordion title="Authentication">
@@ -344,9 +361,16 @@ openssl rand -hex 32
**`LINKEDIN_CLIENT_ID`** / **`LINKEDIN_CLIENT_SECRET`** (optional): Enables LinkedIn sign-in.
**`BETTER_AUTH_API_KEY`** (optional): Enables Better Auth dashboard integrations.
**`BETTER_AUTH_URL`** (optional, advanced): Overrides auth base URL if it must differ from `APP_URL` (for split-host deployments).
**`BETTER_AUTH_SECRET`** (optional, advanced): Overrides `AUTH_SECRET` for Better Auth internals.
**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_DYNAMIC_CLIENT_REDIRECT_HOSTS`**: Comma-separated allowlist for extra dynamic OAuth redirect hosts/origins (HTTPS only, non-private hosts).
- **`OAUTH_SCOPES`**: Space-separated scopes (defaults to `openid profile email`)
Configure endpoints using **one** of these methods:
@@ -358,8 +382,9 @@ openssl rand -hex 32
<Accordion title="Email (SMTP, optional)">
If SMTP is not configured, the app logs emails to the server console instead of sending them.
- 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`**: Usually `465` (implicit TLS) or `587` (STARTTLS).
- **`SMTP_PORT`**: Defaults to `587` in the app.
- **`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.
@@ -367,15 +392,17 @@ openssl rand -hex 32
</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.
- **Default (local)**: If all `S3_*` values are empty, uploads are stored under `<cwd>/data` (usually `/app/data` in the official image).
- Mount local uploads to persistent storage (for example `./data:/app/data`) or uploads can be lost on container recreation.
- **S3/S3-compatible**: Configure `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`, `S3_ENDPOINT`, and `S3_BUCKET`.
- **`S3_FORCE_PATH_STYLE`** controls bucket addressing (defaults to `"false"`):
- `"true"` for path-style URLs (`https://endpoint/bucket`) common with MinIO/SeaweedFS.
- `"false"` for virtual-hosted-style URLs (`https://bucket.endpoint`) common with AWS S3 / Cloudflare R2.
</Accordion>
<Accordion title="AI (optional)">
- **`AI_ALLOWED_BASE_URLS`**: Comma-separated hosts or origins allowed as custom AI API base URLs. - Use this when
routing AI requests through your own gateway/proxy. - Example: `api.openai.com,https://gateway.ai.vercel.com`
</Accordion>
<Accordion title="Feature Flags">
@@ -390,24 +417,32 @@ openssl rand -hex 32
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.
1. **Back up your database and uploads first** (highly recommended before every update).
2. **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.
3. **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.
4. **Check migration/startup logs** after deploy.
```bash
docker compose logs -f reactive-resume
```
5. **(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.
This process updates app services and automatically runs DB migrations on startup. If migration fails, restore from backup and fix configuration before retrying.
## Backups (recommended)
@@ -425,7 +460,7 @@ If you're using S3-compatible storage, consider enabling versioning on your buck
## 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.
Reactive Resume exposes a health check endpoint at `/api/health` that verifies the application and its dependencies. It checks **database**, **printer**, and **storage**; if any one is unhealthy, the endpoint returns HTTP `503`.
### How it works
@@ -448,7 +483,7 @@ Most reverse proxies (such as **Traefik**, **Caddy**, or **nginx** with upstream
- **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.
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, printer, 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
@@ -467,40 +502,51 @@ curl -f http://localhost:3000/api/health
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.
A healthy response returns HTTP 200. Any other response (or a connection failure) indicates a problem that should be investigated in the JSON response body and 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:
- **What to do**:
Check logs for migration errors and database connectivity details:
```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.
- **Common cause**: `APP_URL` doesn't match the URL you're actually using (especially behind a reverse proxy), or
you're serving HTTPS while `APP_URL` is `http://...`. - **Fix**: set `APP_URL` to your canonical public HTTPS URL and
restart the container.
</Accordion>
<Accordion title="PDF export fails / printing is stuck">
- **Common cause**: Reactive Resume can't reach the printer or the printer can't reach your app. - **Checks**: -
`PRINTER_ENDPOINT` should usually be `http://printer:3000` in Compose. - If you use
`PRINTER_APP_URL="http://host.docker.internal:3000"`, ensure `extra_hosts: host-gateway` is present for the printer
service.
- **Common cause**: Reactive Resume can't reach the printer, token mismatch, or the printer can't reach your app. -
**Checks**: - `PRINTER_ENDPOINT` should usually be `ws://printer:3000?token=...` in Compose. - Browserless `TOKEN` and
the token in `PRINTER_ENDPOINT` must match. - If you use `PRINTER_APP_URL="http://host.docker.internal:3000"` on
Linux, set `extra_hosts: ["host.docker.internal:host-gateway"]` for the printer service.
</Accordion>
<Accordion title="/api/health returns 503 even though Postgres is up">
- **Common cause**: printer or storage health failed (not only database). - **Fix**: inspect the endpoint response
payload and check `printer` / `storage` fields: ```bash curl -s http://localhost:3000/api/health ```
</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.
- **Cause**: local upload storage wasn't mounted to a persistent volume. - **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.
- **Expected behavior**: if SMTP isn't fully configured, the app logs emails to the console. - **Fix**: set
`SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, and `SMTP_FROM`, then verify `SMTP_PORT` and `SMTP_SECURE`.
</Accordion>
<Accordion title="Dynamic OAuth redirect URI is rejected">
- **Common cause**: redirect host is not trusted for dynamic client registration. - **Fix**: add trusted HTTPS
hosts/origins to `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS`.
</Accordion>
<Accordion title="S3 storage error: ENOTFOUND bucket.endpoint">
+15 -12
View File
@@ -85,12 +85,11 @@ services:
- QUEUED=10
- HEALTH=true
- CONCURRENT=5
# Optional: Set a token for authentication
# - TOKEN=your-secret-token
- TOKEN=${BROWSERLESS_TOKEN}
networks:
- reactive_resume_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"]
test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null']
interval: 30s
timeout: 10s
retries: 3
@@ -102,7 +101,7 @@ services:
- APP_URL=https://resume.${DOMAIN}
- PRINTER_APP_URL=http://reactive_resume:3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- PRINTER_ENDPOINT=http://printer:3000
- PRINTER_ENDPOINT=ws://printer:3000?token=${BROWSERLESS_TOKEN}
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
@@ -143,6 +142,7 @@ DOMAIN="example.com"
ACME_EMAIL="admin@example.com"
POSTGRES_PASSWORD="your-secure-postgres-password"
AUTH_SECRET="your-auth-secret-from-openssl-rand-hex-32"
BROWSERLESS_TOKEN="your-browserless-token"
# Optional: Traefik dashboard auth (generate with: htpasswd -nb admin password)
TRAEFIK_DASHBOARD_AUTH="admin:$$apr1$$..."
```
@@ -191,10 +191,14 @@ services:
- QUEUED=10
- HEALTH=true
- CONCURRENT=5
# Optional: Set a token for authentication
# - TOKEN=your-secret-token
- TOKEN=${BROWSERLESS_TOKEN}
networks:
- reactive_resume_network
healthcheck:
test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null']
interval: 30s
timeout: 10s
retries: 3
reactive_resume:
image: amruthpillai/reactive-resume:latest
@@ -203,7 +207,7 @@ services:
- APP_URL=https://resume.${DOMAIN}
- PRINTER_APP_URL=http://reactive_resume:3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- PRINTER_ENDPOINT=http://printer:3000
- PRINTER_ENDPOINT=ws://printer:3000?token=${BROWSERLESS_TOKEN}
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
@@ -339,10 +343,9 @@ services:
- QUEUED=10
- HEALTH=true
- CONCURRENT=5
# Optional: Set a token for authentication
# - TOKEN=your-secret-token
- TOKEN=$BROWSERLESS_TOKEN
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"]
test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=$BROWSERLESS_TOKEN" > /dev/null']
interval: 30s
timeout: 10s
retries: 3
@@ -469,10 +472,10 @@ docker stack deploy -c compose-swarm.yml reactive_resume
docker stack services reactive_resume
# View logs for the app
docker service logs -f reactive_resume_app
docker service logs -f reactive_resume_reactive_resume
# Scale the app
docker service scale reactive_resume_app=3
docker service scale reactive_resume_reactive_resume=3
# Remove the stack
docker stack rm reactive_resume
+8 -1
View File
@@ -7,6 +7,11 @@ description: "A step-by-step guide to migrate your Reactive Resume instance from
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.
<Info>
This page is for **v4 → v5 data migration** only. For normal v5 upgrades, use the [Self-Hosting with
Docker](/self-hosting/docker) guide. v5 schema migrations run automatically on app startup.
</Info>
<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.
@@ -86,7 +91,7 @@ To run the migration scripts, you need the following installed on your host mach
</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 ```
https://github.com/amruthpillai/reactive-resume.git cd reactive-resume vp install ```
</Card>
</CardGroup>
@@ -107,6 +112,8 @@ PRODUCTION_DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resu
`PRODUCTION_DATABASE_URL` should point to your **old v4 database**. Mixing these up could cause data loss.
</Warning>
<Warning>`PRODUCTION_DATABASE_URL` is used only by these migration scripts. It is not a runtime app variable.</Warning>
### Step 1: Migrate Users
The user migration script transfers all user accounts, authentication data, and two-factor settings from v4 to v5.
+32 -4
View File
@@ -62,10 +62,13 @@ You must configure endpoints using **one** of these two methods:
### Optional Variables
| Variable | Description | Default |
| --------------------- | ---------------------------------------- | ---------------------- |
| `OAUTH_PROVIDER_NAME` | Display name shown on the sign-in button | `Custom OAuth` |
| `OAUTH_SCOPES` | Space-separated list of OAuth scopes | `openid profile email` |
| Variable | Description | Default |
| ------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------- |
| `OAUTH_PROVIDER_NAME` | Display name shown on the sign-in button | `Custom OAuth` |
| `OAUTH_SCOPES` | Space-separated list of OAuth scopes | `openid profile email` |
| `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` | Comma-separated allowlist for dynamic OAuth client redirect hosts/origins (HTTPS only) | _empty_ |
| `BETTER_AUTH_URL` | Optional auth base URL override for split-host setups | `APP_URL` |
| `BETTER_AUTH_SECRET` | Optional Better Auth secret override | `AUTH_SECRET` |
## Callback URL
@@ -86,6 +89,18 @@ https://resume.example.com/api/auth/oauth2/callback/custom
authentication to fail.
</Warning>
<Info>
Built-in providers (Google, GitHub, LinkedIn) use callback URLs in this format: `{APP_URL}/api/auth/callback/
{provider}` (for example `.../google`, `.../github`, `.../linkedin`).
</Info>
## URL and Proxy Requirements
- Set `APP_URL` to the exact public URL users access (prefer HTTPS in production).
- If auth metadata/JWKS must be served from a different public host, set `BETTER_AUTH_URL`.
- Behind a reverse proxy, forward `Host` and `X-Forwarded-Proto` correctly, or cookie/session behavior may break.
- `trustedOrigins` are derived from `APP_URL`, so alternate domains are not automatically trusted.
## Profile Mapping
Reactive Resume automatically maps user profile data from the OAuth provider. The following fields are used:
@@ -280,6 +295,11 @@ OAUTH_DISCOVERY_URL="https://auth.company.com/application/o/reactive-resume/.wel
number differences - Path case sensitivity
</Accordion>
<Accordion title="Session/cookie issues after successful OAuth login">
Common cause: `APP_URL` does not match the real HTTPS public origin (for example, app is behind TLS but `APP_URL` is
`http://...`). Fix: set `APP_URL` to the canonical HTTPS URL and restart the app.
</Accordion>
<Accordion title="Custom OAuth button not appearing">
The custom OAuth option only appears if both `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are set, **and** either:
- `OAUTH_DISCOVERY_URL` is set, **or**
@@ -295,6 +315,11 @@ OAUTH_DISCOVERY_URL="https://auth.company.com/application/o/reactive-resume/.wel
domain
</Accordion>
<Accordion title="Dynamic client redirect URI is rejected">
Dynamic OAuth client registration only allows HTTPS redirect URIs on trusted hosts. Add allowed hosts/origins to
`OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` (comma-separated).
</Accordion>
<Accordion title="User profile data is missing or incorrect">
The profile mapping depends on your provider returning standard claims:
- `email` (required)
@@ -319,6 +344,9 @@ OAUTH_DISCOVERY_URL="https://auth.company.com/application/o/reactive-resume/.wel
<Card title="Verify redirect URIs" icon="shield-check">
Configure your OAuth provider to only allow the exact redirect URI. Avoid wildcards in redirect URI configurations.
</Card>
<Card title="Protect auth internals" icon="shield">
Keep `BETTER_AUTH_SECRET` and `BETTER_AUTH_API_KEY` private. Rotating these values may invalidate active sessions.
</Card>
<Card title="Review scopes" icon="list-check">
Only request the scopes you need. The default (`openid profile email`) is sufficient for Reactive Resume.
</Card>