mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-13 14:35:33 +10:00
add an alternative to browserless, for lightweight servers
This commit is contained in:
+1
-2
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 | — |
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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)?/ }),
|
||||
|
||||
Reference in New Issue
Block a user