Skip to main content
This guide covers how to deploy Reader in production environments.

Server Usage Pattern

For server applications, create one ReaderClient instance and reuse it:
import { ReaderClient } from "@vakra-dev/reader";
import express from "express";

// Create client once at startup
const reader = new ReaderClient({
  browserPool: {
    size: 5,
    retireAfterPages: 50,
    retireAfterMinutes: 15,
  },
});

const app = express();
app.use(express.json());

// Reuse client for all requests
app.post("/scrape", async (req, res) => {
  try {
    const result = await reader.scrape({
      urls: req.body.urls,
      formats: ["markdown"],
    });
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Graceful shutdown
process.on("SIGTERM", async () => {
  console.log("Shutting down...");
  await reader.close();
  process.exit(0);
});

app.listen(3000, () => {
  console.log("Server running on port 3000");
});

Docker Deployment

Docker deployment only works on x86_64 Linux. Apple Silicon Macs require native Node.js or a remote x86_64 server.

Dockerfile

FROM node:20-slim

# Install Chrome dependencies
RUN apt-get update && apt-get install -y \
    chromium \
    fonts-liberation \
    libasound2 \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libcups2 \
    libdbus-1-3 \
    libdrm2 \
    libgbm1 \
    libgtk-3-0 \
    libnspr4 \
    libnss3 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxrandr2 \
    xdg-utils \
    --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

ENV NODE_ENV=production
ENV CHROME_PATH=/usr/bin/chromium

EXPOSE 3000

CMD ["node", "server.js"]

docker-compose.yml

version: "3.8"

services:
  reader:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    deploy:
      resources:
        limits:
          memory: 4G
        reservations:
          memory: 2G
    restart: unless-stopped

Production Best Practices

Browser Pool Configuration

const reader = new ReaderClient({
  browserPool: {
    size: 5,              // Balance concurrency vs memory
    retireAfterPages: 50, // Prevent memory leaks
    retireAfterMinutes: 15,
    maxQueueSize: 100,    // Prevent unbounded queue growth
  },
});

Error Handling

import {
  ReaderClient,
  TimeoutError,
  NetworkError,
  CloudflareError
} from "@vakra-dev/reader";

app.post("/scrape", async (req, res) => {
  try {
    const result = await reader.scrape({
      urls: req.body.urls,
      timeoutMs: 30000,
    });
    res.json(result);
  } catch (error) {
    if (error instanceof TimeoutError) {
      res.status(504).json({ error: "Request timed out" });
    } else if (error instanceof NetworkError) {
      res.status(502).json({ error: "Network error" });
    } else if (error instanceof CloudflareError) {
      res.status(403).json({ error: "Access denied" });
    } else {
      res.status(500).json({ error: "Internal error" });
    }
  }
});

Rate Limiting

import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
});

app.use("/scrape", limiter);

Health Check

app.get("/health", (req, res) => {
  const ready = reader.isReady();
  res.status(ready ? 200 : 503).json({
    status: ready ? "healthy" : "unhealthy",
    ready,
  });
});

Graceful Shutdown

let isShuttingDown = false;

const shutdown = async () => {
  if (isShuttingDown) return;
  isShuttingDown = true;

  console.log("Shutting down gracefully...");

  // Stop accepting new requests
  server.close();

  // Close reader
  await reader.close();

  process.exit(0);
};

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

Memory Considerations

Each browser instance uses ~200-500MB of memory. Plan accordingly:
Pool SizeRecommended RAM
1-22GB
3-54GB
5-108GB
10+16GB+

Process Exit Note

Node.js may not auto-exit after reader.close() due to Hero internals. For scripts, add process.exit(0) after close. For servers, this is expected behavior since the process stays alive to handle requests.

Next Steps