diff --git a/.env.example b/.env.example
index 1ec281313..f22604344 100644
--- a/.env.example
+++ b/.env.example
@@ -5,10 +5,9 @@ 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
+# Note: set this to `http://host.docker.internal:3000` if your printer service is running on docker, but Reactive Resume is running outside of Docker.
PRINTER_APP_URL="http://host.docker.internal:3000"
-# Note: set this to `http://host.docker.internal:3000` if your `gotenberg` service is running on docker, but Reactive Resume is running outside of Docker.
-
# --- Printer ---
PRINTER_ENDPOINT="ws://localhost:4000?token=1234567890"
diff --git a/compose.dev.yml b/compose.dev.yml
index 4e6ac5bd1..04c1af3c7 100644
--- a/compose.dev.yml
+++ b/compose.dev.yml
@@ -27,12 +27,24 @@ services:
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
+ - QUEUED=10
- HEALTH=true
- - MAX_CPU_PERCENT=90
- - MAX_MEMORY_PERCENT=90
- - QUEUED=5
- - CONCURRENT=5
+ - CONCURRENT=20
- TOKEN=1234567890
+ healthcheck:
+ test:
+ ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=1234567890"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+
+ # As an alternative to browserless, you can also use a lightweight image like chromedp/headless-shell:latest
+ # See https://docs.rxresu.me/self-hosting/docker#alternative-printer-options for more information.
+ # chrome:
+ # image: chromedp/headless-shell:latest
+ # restart: unless-stopped
+ # ports:
+ # - "9222:9222"
seaweedfs:
image: chrislusf/seaweedfs:latest
diff --git a/compose.yml b/compose.yml
index 4bf2306fb..03cae4a7f 100644
--- a/compose.yml
+++ b/compose.yml
@@ -17,22 +17,32 @@ services:
timeout: 10s
retries: 3
- gotenberg:
- image: gotenberg/gotenberg:8
+ browserless:
+ image: ghcr.io/browserless/chromium:latest
restart: unless-stopped
+ ports:
+ - "4000:3000"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- - WEBHOOK_DISABLE=true
- - CHROMIUM_AUTO_START=true
- - LIBREOFFICE_DISABLE_ROUTES=true
- - PROMETHEUS_DISABLE_COLLECT=true
+ - QUEUED=10
+ - HEALTH=true
+ - CONCURRENT=20
+ - TOKEN=1234567890
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
- start_period: 10s
- interval: 30s
- timeout: 10s
- retries: 3
+ test:
+ ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=1234567890"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+
+ # As an alternative to browserless, you can also use a lightweight image like chromedp/headless-shell:latest
+ # See https://docs.rxresu.me/self-hosting/docker#alternative-printer-options for more information.
+ # chrome:
+ # image: chromedp/headless-shell:latest
+ # restart: unless-stopped
+ # ports:
+ # - "9222:9222"
seaweedfs:
image: chrislusf/seaweedfs:latest
@@ -74,7 +84,8 @@ services:
- APP_URL=http://localhost:3000
- PRINTER_APP_URL=http://app:3000
# Printer
- - GOTENBERG_ENDPOINT=http://gotenberg:3000
+ - PRINTER_ENDPOINT=ws://browserless:3000?token=1234567890
+ # - PRINTER_ENDPOINT=http://chrome:9222 # Or, if you're using chromedp/headless-shell
# Database
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres
# Authentication
@@ -97,16 +108,7 @@ services:
seaweedfs-create-bucket:
condition: service_completed_successfully
healthcheck:
- test:
- [
- "CMD",
- "curl",
- "-f",
- "http://localhost:3000/api/health",
- "||",
- "exit",
- "1",
- ]
+ test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
start_period: 10s
interval: 30s
timeout: 10s
diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx
index b0afdc35d..3270938f5 100644
--- a/docs/getting-started/quickstart.mdx
+++ b/docs/getting-started/quickstart.mdx
@@ -166,7 +166,6 @@ Here's a complete list of environment variables you can configure:
| Variable | Description | Default |
|----------|-------------|---------|
| `PRINTER_APP_URL` | Public URL for the printer to access the Application | — |
-| `PRINTER_TOKEN` | Printer authentication token (if using browserless with token auth) | — |
| `GOOGLE_CLIENT_ID` | Google OAuth Client ID | — |
| `GOOGLE_CLIENT_SECRET` | Google OAuth Client Secret | — |
| `GITHUB_CLIENT_ID` | GitHub OAuth Client ID | — |
diff --git a/docs/self-hosting/docker.mdx b/docs/self-hosting/docker.mdx
index 3ab6d8cf7..4ac5bb4c8 100644
--- a/docs/self-hosting/docker.mdx
+++ b/docs/self-hosting/docker.mdx
@@ -64,11 +64,9 @@ APP_URL="http://localhost:3000"
PRINTER_APP_URL="http://host.docker.internal:3000"
# --- Printer ---
-PRINTER_ENDPOINT="http://printer:3000"
-
-# Printer Authentication (Optional)
-# If using browserless with a token, set PRINTER_TOKEN
-# PRINTER_TOKEN=""
+# 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"
# --- Database (PostgreSQL) ---
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres"
@@ -179,9 +177,9 @@ services:
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- - TIMEOUT=120000
- - CONCURRENT=10
- HEALTH=true
+ - CONCURRENT=20
+ - QUEUED=10
# Optional: Set a token for authentication
# - TOKEN=your-secret-token
healthcheck:
@@ -219,7 +217,17 @@ volumes:
- **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.
+ **Alternative Printer Options**: If you don't want to use browserless, you can also use a lightweight headless Chrome Docker image like `chromedp/headless-shell`:
+
+ ```yaml
+ chrome:
+ image: chromedp/headless-shell:latest
+ restart: unless-stopped
+ ports:
+ - "9222:9222"
+ ```
+
+ Then set `PRINTER_ENDPOINT` to `http://chrome:9222` (or `http://localhost:9222` if running outside Docker Compose). This provides the same PDF/screenshot generation functionality with a smaller image footprint.
@@ -283,11 +291,20 @@ docker compose logs -f reactive-resume
- - **`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.
+ - **`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`.
- **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.
+ **Alternative to browserless**: You can use a lightweight headless Chrome Docker image like `chromedp/headless-shell`:
+
+ ```yaml
+ chrome:
+ image: chromedp/headless-shell:latest
+ restart: unless-stopped
+ ports:
+ - "9222:9222"
+ ```
+
+ Set `PRINTER_ENDPOINT` to `http://chrome:9222` (in Docker Compose) or `http://localhost:9222` (if running externally). This provides the same PDF/screenshot generation with a smaller image footprint.
diff --git a/docs/self-hosting/examples.mdx b/docs/self-hosting/examples.mdx
index d5efecf2c..920c1163b 100644
--- a/docs/self-hosting/examples.mdx
+++ b/docs/self-hosting/examples.mdx
@@ -356,7 +356,7 @@ services:
- CONCURRENT=10
- HEALTH=true
# Optional: Set a token for authentication
- # - TOKEN=$PRINTER_TOKEN
+ # - TOKEN=your-secret-token
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s
@@ -437,9 +437,9 @@ services:
- reactive_resume_app_data:/app/data
environment:
- APP_URL=$APP_URL
+ # If using browserless with token auth, include the token in the URL:
+ # PRINTER_ENDPOINT=ws://printer:3000?token=your-secret-token
- 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
diff --git a/src/integrations/orpc/services/printer.ts b/src/integrations/orpc/services/printer.ts
index 57f07bae0..d6f9483b8 100644
--- a/src/integrations/orpc/services/printer.ts
+++ b/src/integrations/orpc/services/printer.ts
@@ -1,25 +1,11 @@
import type { InferSelectModel } from "drizzle-orm";
-import puppeteer, { type Browser, type Page } from "puppeteer-core";
+import puppeteer, { type Browser, type ConnectOptions, type Page } from "puppeteer-core";
import type { schema } from "@/integrations/drizzle";
import { printMarginTemplates } from "@/schema/templates";
import { env } from "@/utils/env";
import { generatePrinterToken } from "@/utils/printer-token";
import { getStorageService, uploadFile } from "./storage";
-type PressureResponse = {
- cpu: number;
- date: number;
- isAvailable: boolean;
- maxConcurrent: number;
- maxQueued: number;
- memory: number;
- message: string;
- queued: number;
- reason: string;
- recentlyRejected: number;
- running: number;
-};
-
const pageDimensions = {
a4: {
width: "210mm",
@@ -36,6 +22,26 @@ const SCREENSHOT_TTL = 1000 * 60 * 60; // 1 hour
let pdfBrowser: Browser | null = null;
let screenshotBrowser: Browser | null = null;
+async function getBrowser(type: "pdf" | "screenshot"): Promise {
+ const endpoint = new URL(env.PRINTER_ENDPOINT);
+ const isWebSocket = endpoint.protocol.startsWith("ws");
+
+ const connectOptions: ConnectOptions = { acceptInsecureCerts: true };
+
+ if (isWebSocket) connectOptions.browserWSEndpoint = env.PRINTER_ENDPOINT;
+ else connectOptions.browserURL = env.PRINTER_ENDPOINT;
+
+ if (type === "screenshot") {
+ if (screenshotBrowser?.connected) return screenshotBrowser;
+ screenshotBrowser = await puppeteer.connect({ ...connectOptions, defaultViewport: { width: 794, height: 1123 } });
+ return screenshotBrowser;
+ }
+
+ if (pdfBrowser?.connected) return pdfBrowser;
+ pdfBrowser = await puppeteer.connect(connectOptions);
+ return pdfBrowser;
+}
+
async function interceptLocalhostRequests(page: Page) {
await page.setRequestInterception(true);
@@ -53,18 +59,29 @@ async function interceptLocalhostRequests(page: Page) {
}
export const printerService = {
- healthcheck: async (): Promise => {
- const printerEndpoint = env.PRINTER_ENDPOINT;
-
+ healthcheck: async (): Promise