- Use browserless over gotenberg

- Implement functionality to move items between sections or pages
- Enhance custom sections to have a `type` property
- Update the v4 importer to account for custom sections
- Update healthcheck to be a simple curl command
- Update dependencies to latest
and a lot more changes
This commit is contained in:
Amruth Pillai
2026-01-21 18:49:54 +01:00
parent b3c342b029
commit 70064be7de
54 changed files with 2153 additions and 822 deletions
+36 -25
View File
@@ -1,6 +1,6 @@
---
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."
description: "A comprehensive guide to self-host Reactive Resume with Docker (Postgres + Printer), including a detailed environment variable reference and troubleshooting tips."
---
## Overview
@@ -11,8 +11,8 @@ Reactive Resume can be self-hosted using Docker in a matter of minutes, and this
<Card title="PostgreSQL">
Stores accounts, resumes, and application data.
</Card>
<Card title="Gotenberg">
Generates PDFs by rendering a special print route.
<Card title="Printer">
Generates PDFs and screenshots using a headless Chromium browser.
</Card>
<Card title="Email (optional)">
SMTP for verification emails, password reset, etc. If not configured, emails are logged to the server console.
@@ -34,7 +34,7 @@ You can pull the latest app image from:
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).
2 vCPU / 2 GB RAM minimum (4 GB recommended if Postgres + Printer run on the same host).
</Card>
<Card title="Storage">
Enough for Postgres + uploads (start with 10-20 GB and scale as needed).
@@ -64,11 +64,11 @@ APP_URL="http://localhost:3000"
PRINTER_APP_URL="http://host.docker.internal:3000"
# --- Printer ---
GOTENBERG_ENDPOINT="http://gotenberg:3000"
PRINTER_ENDPOINT="http://printer:3000"
# Gotenberg Authentication (Optional)
# GOTENBERG_USERNAME=""
# GOTENBERG_PASSWORD=""
# Printer Authentication (Optional)
# If using browserless with a token, set PRINTER_TOKEN
# PRINTER_TOKEN=""
# --- Database (PostgreSQL) ---
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres"
@@ -150,7 +150,7 @@ FLAG_DISABLE_SIGNUP="false"
</Step>
<Step title="Create compose.yml">
This setup runs Postgres + Gotenberg + Reactive Resume on a private Docker network.
This setup runs Postgres + Printer + Reactive Resume on a private Docker network.
<CodeGroup>
@@ -171,18 +171,21 @@ services:
timeout: 5s
retries: 10
gotenberg:
image: gotenberg/gotenberg:edge
printer:
image: ghcr.io/browserless/chromium:latest
restart: unless-stopped
ports:
- "4000:3000"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- CHROMIUM_AUTO_START=true
- LIBREOFFICE_DISABLE_ROUTES=true
- TIMEOUT=120000
- CONCURRENT=10
- HEALTH=true
# Optional: Set a token for authentication
# - TOKEN=your-secret-token
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 10s
timeout: 5s
retries: 10
@@ -201,10 +204,10 @@ services:
depends_on:
postgres:
condition: service_healthy
gotenberg:
printer:
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))\""]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3
@@ -215,6 +218,10 @@ volumes:
</CodeGroup>
<Note>
**Alternative Printer Options**: If you don't want to use browserless, you can also use any headless Chrome/Chromium instance with its remote debugging port open. For example, you could run `chromium --remote-debugging-port=9222` and point `PRINTER_ENDPOINT` to that instance.
</Note>
<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>
@@ -254,7 +261,7 @@ docker compose logs -f reactive-resume
<ul>
<li><code>APP_URL</code></li>
<li><code>DATABASE_URL</code></li>
<li><code>GOTENBERG_ENDPOINT</code></li>
<li><code>PRINTER_ENDPOINT</code></li>
<li><code>AUTH_SECRET</code></li>
</ul>
</Card>
@@ -272,12 +279,16 @@ docker compose logs -f reactive-resume
<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`).
- **`PRINTER_APP_URL`** (optional): Overrides the base URL used when rendering the print route for the printer. Defaults to `APP_URL`. Useful when the printer 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 title="Printer">
- **`PRINTER_ENDPOINT`**: Base URL where Reactive Resume reaches the printer service. In Compose: `http://printer:3000`.
- **`PRINTER_TOKEN`** (optional): Authentication token for the printer service. Required if your browserless instance is configured with token authentication.
<Note>
**Alternative to browserless**: You can use any headless Chrome/Chromium instance with its remote debugging port open. For example, run `chromium --remote-debugging-port=9222` and set `PRINTER_ENDPOINT` to point to that instance.
</Note>
</Accordion>
<Accordion title="Database (PostgreSQL)">
@@ -386,7 +397,7 @@ The Docker Compose configuration includes a health check that periodically calls
```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))\""]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3
@@ -439,10 +450,10 @@ A healthy response returns HTTP 200. Any other response (or a connection failure
</Accordion>
<Accordion title="PDF export fails / printing is stuck">
- **Common cause**: Reactive Resume can't reach Gotenberg or Gotenberg can't reach your app.
- **Common cause**: Reactive Resume can't reach the printer or the printer 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.
- `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.
</Accordion>
<Accordion title="Uploads disappear after restart">
+33 -40
View File
@@ -17,7 +17,7 @@ These examples go beyond the basic setup in the [Self-Hosting with Docker](/self
## 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.
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 the printer 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.
@@ -75,19 +75,19 @@ services:
timeout: 5s
retries: 5
gotenberg:
image: gotenberg/gotenberg:8
printer:
image: ghcr.io/browserless/chromium:latest
restart: unless-stopped
command:
- "gotenberg"
- "--chromium-auto-start"
- "--api-timeout=120s"
environment:
- TIMEOUT=120000
- CONCURRENT=10
- HEALTH=true
networks:
- reactive_resume_network
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
@@ -99,7 +99,7 @@ services:
- 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
- PRINTER_ENDPOINT=http://printer:3000
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
@@ -109,7 +109,7 @@ services:
depends_on:
postgres:
condition: service_healthy
gotenberg:
printer:
condition: service_healthy
labels:
- "traefik.enable=true"
@@ -118,7 +118,7 @@ services:
- "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))\""]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3
@@ -183,19 +183,19 @@ services:
timeout: 5s
retries: 5
gotenberg:
image: gotenberg/gotenberg:8
printer:
image: ghcr.io/browserless/chromium:latest
restart: unless-stopped
command:
- "gotenberg"
- "--chromium-auto-start"
- "--api-timeout=120s"
environment:
- TIMEOUT=120000
- CONCURRENT=10
- HEALTH=true
networks:
- reactive_resume_network
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
@@ -207,7 +207,7 @@ services:
- 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
- PRINTER_ENDPOINT=http://printer:3000
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
@@ -217,10 +217,10 @@ services:
depends_on:
postgres:
condition: service_healthy
gotenberg:
printer:
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))\""]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3
@@ -347,19 +347,18 @@ services:
max_attempts: 3
window: 120s
gotenberg:
image: gotenberg/gotenberg:8
printer:
image: ghcr.io/browserless/chromium:latest
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
- TIMEOUT=120000
- CONCURRENT=10
- HEALTH=true
# Optional: Set a token for authentication
# - TOKEN=$PRINTER_TOKEN
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
@@ -438,9 +437,9 @@ services:
- reactive_resume_app_data:/app/data
environment:
- APP_URL=$APP_URL
- GOTENBERG_ENDPOINT=$GOTENBERG_ENDPOINT
- GOTENBERG_USERNAME=$GOTENBERG_USERNAME
- GOTENBERG_PASSWORD=$GOTENBERG_PASSWORD
- PRINTER_ENDPOINT=$PRINTER_ENDPOINT
# Optional: Set PRINTER_TOKEN if using browserless with token authentication
# - PRINTER_TOKEN=$PRINTER_TOKEN
- DATABASE_URL=$DATABASE_URL
- AUTH_SECRET=$AUTH_SECRET
- GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
@@ -459,13 +458,7 @@ services:
- 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))",
]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3