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

Browser Pool

Configure browser pool

API Reference

Full API documentation