add an alternative to browserless, for lightweight servers

This commit is contained in:
Amruth Pillai
2026-01-21 23:24:37 +01:00
parent 4d72a953dd
commit 5d73998f82
9 changed files with 120 additions and 86 deletions
+1 -2
View File
@@ -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"
+16 -4
View File
@@ -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
+24 -22
View File
@@ -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
-1
View File
@@ -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 | — |
+28 -11
View File
@@ -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:
</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.
**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.
</Note>
<Tip>
@@ -283,11 +291,20 @@ 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`.
- **`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`.
<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.
**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.
</Note>
</Accordion>
+3 -3
View File
@@ -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
+46 -40
View File
@@ -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<Browser> {
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<PressureResponse> => {
const printerEndpoint = env.PRINTER_ENDPOINT;
healthcheck: async (): Promise<object> => {
const headers = new Headers({ Accept: "application/json" });
const endpoint = new URL(printerEndpoint);
endpoint.protocol = endpoint.protocol === "wss:" ? "https:" : "http:";
endpoint.pathname = "/pressure";
const endpoint = new URL(env.PRINTER_ENDPOINT);
endpoint.protocol = endpoint.protocol.replace("ws", "http");
endpoint.pathname = "/json/version";
const response = await fetch(endpoint, { headers });
const data = (await response.json()) as { pressure: PressureResponse };
const data = await response.json();
return data.pressure;
return data;
},
chromeDebug: async (): Promise<void> => {
const browser = await getBrowser("pdf");
const page = await browser.newPage();
await page.goto("https://www.google.com");
await page.pdf({ path: `screenshot-${Date.now()}.pdf` });
await browser.disconnect();
},
printResumeAsPDF: async (
@@ -95,16 +112,11 @@ export const printerService = {
marginY = Math.round(data.metadata.page.marginY / 0.75);
}
if (!pdfBrowser || !pdfBrowser.connected) {
pdfBrowser = await puppeteer.connect({
acceptInsecureCerts: true,
browserWSEndpoint: env.PRINTER_ENDPOINT,
});
}
const browser = await getBrowser("pdf");
await pdfBrowser.setCookie({ name: "locale", value: locale, domain });
await browser.setCookie({ name: "locale", value: locale, domain });
const page = await pdfBrowser.newPage();
const page = await browser.newPage();
if (env.APP_URL.includes("localhost")) await interceptLocalhostRequests(page);
@@ -176,17 +188,11 @@ export const printerService = {
const token = generatePrinterToken(id);
const url = `${baseUrl}/printer/${id}?token=${token}`;
if (!screenshotBrowser || !screenshotBrowser.connected) {
screenshotBrowser = await puppeteer.connect({
acceptInsecureCerts: true,
defaultViewport: { width: 794, height: 1123 },
browserWSEndpoint: env.PRINTER_ENDPOINT,
});
}
const browser = await getBrowser("screenshot");
await screenshotBrowser.setCookie({ name: "locale", value: locale, domain });
await browser.setCookie({ name: "locale", value: locale, domain });
const page = await screenshotBrowser.newPage();
const page = await browser.newPage();
if (env.APP_URL.includes("localhost")) await interceptLocalhostRequests(page);
+1 -2
View File
@@ -68,8 +68,7 @@ async function checkPrinter() {
async function checkStorage() {
try {
const storageService = getStorageService();
const result = await storageService.healthcheck();
return result;
return await storageService.healthcheck();
} catch (error) {
return {
status: "unhealthy",
+1 -1
View File
@@ -15,7 +15,7 @@ export const env = createEnv({
PRINTER_APP_URL: z.url({ protocol: /https?/ }).optional(),
// Printer
PRINTER_ENDPOINT: z.url({ protocol: /wss?/ }),
PRINTER_ENDPOINT: z.url({ protocol: /^(wss?|https?)$/ }),
// Database
DATABASE_URL: z.url({ protocol: /postgres(ql)?/ }),