Skip to content
Afa' Afa'

Take screenshot of an URL using puppeteer

Capture a URL screenshot with Puppeteer inside a small Chromium Docker image.

2 min read Updated

A containerised Puppeteer script that captures a screenshot of a URL with a configurable viewport, wait time and output path. The Dockerfile installs Chromium, and the CLI parses the options. The files are reproduced below.

Usage

sh
# build
docker build . -t screenshot

# execution
docker run -it --rm -v $(pwd):/output screenshot \
  --url https://google.com \
  --wait 3000 \
  --size 1024x768 \
  --output /output/screenshot.png

Dockerfile

dockerfile
FROM alpine:edge

ENV NODE_ENV='production'
ENV NO_COLOR='true'

# Installs latest Chromium (89) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      freetype-dev \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      yarn

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Puppeteer v6.0.0 works with Chromium 89.
RUN yarn add puppeteer@6.0.0

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

COPY . /app/screenshot/
WORKDIR /app/screenshot

# Run everything after as non-privileged user.
USER pptruser

ENTRYPOINT ["node", "/app/screenshot"]

package.json

json
{
  "name": "screenshot",
  "version": "1.0.0",
  "description": "Take screenshot of an URL",
  "main": "screenshot.mjs",
  "type": "module",
  "scripts": {},
  "author": "Sébastien Demanou",
  "license": "MIT"
}

screenshot.mjs

javascript
import puppeteer from "puppeteer";
import commander from "commander";

function parseIntValidator(value) {
  // parseInt takes a string and a radix
  const parsedValue = Number.parseInt(value, 10);

  if (isNaN(parsedValue)) {
    throw new commander.InvalidOptionArgumentError("Not a number.");
  }

  return parsedValue;
}

const program = new commander.Command();

program.name("screenshot");
program.version("1.0.0");
program.description("Take screenshot of an URL");

program
  .requiredOption("-u, --url <url>", "Page URL")
  .option("-s, --size <size>", "Screenshot size", "1024x768")
  .requiredOption("-o, --output <file>", "Output filename")
  .option(
    "-w, --wait <milliseconds>",
    "Pauses execution for the given number of milliseconds before taking screenshot",
    parseIntValidator,
  )
  .action(async () => {
    const { url, size, wait, output, outputVariables } = program.opts();
    const [width, height] = size.toLowerCase().split(/x/);

    const browser = await puppeteer.launch({
      headless: true,
      args: ["--no-sandbox", "--disable-dev-shm-usage"],
    });
    const page = await browser.newPage();

    await page.setViewport({
      width: Number.parseInt(width, 10),
      height: Number.parseInt(height, 10),
      deviceScaleFactor: 1,
    });

    try {
      process.stdout.write(`Opening page ${url}\n`);
      await page.goto(url);

      if (wait) {
        process.stdout.write(`Wait for timeout ${wait}ms\n`);
        await page.waitForTimeout(wait);
      }

      process.stdout.write(`Taking screenshot of ${url}...\n`);
      await page.screenshot({ path: output });
      process.stdout.write(`Screenshot saved to ${output}\n`);
      await page.close();
      await browser.close();
      process.exit(0);
    } catch (err) {
      console.error(err);
      await page.close();
      await browser.close();
      process.exit(1);
    }
  });

program.parse();
Something wrong or want to discuss this article? Get in touch

Search articles and projects

Type to filter articles and projects. Use the arrow keys to move through results and Enter to open one. Press Escape to close.