Skip to content

SchweizerischeBundesbahnen/weasyprint-service

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

WeasyPrint Service

A dockerized service providing a REST API interface to leverage WeasyPrint's functionality for generating PDF documents from HTML and CSS.

Features

  • Simple REST API to access WeasyPrint
  • Real-time monitoring dashboard with metrics visualization
  • Prometheus metrics endpoint on dedicated port (9180) for security and Grafana integration
  • Compatible with amd64 and arm64 architectures
  • Easily deployable via Docker
  • Security-hardened container: Runs as non-root user with OCI security labels

Getting Started

Installation

To install the latest version of the WeasyPrint Service, run the following command:

docker pull ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Running the Service

To start the WeasyPrint service container, execute:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --publish 9180:9180 \
  --name weasyprint-service \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

The service will be accessible on port 9080, and Prometheus metrics on port 9180.

Important: The --init flag enables Docker's built-in init process which handles signal forwarding and zombie process reaping. This is required for proper operation of the service.

PDF Variants

The service supports generating PDFs in various archival and accessibility formats via the pdf_variant query parameter.

Supported PDF/A variants (archival):

  • pdf/a-1b, pdf/a-2b, pdf/a-3b - Basic conformance levels
  • pdf/a-2u, pdf/a-3u, pdf/a-4u - Unicode conformance levels
  • pdf/a-1a, pdf/a-2a, pdf/a-3a - Accessible conformance levels
  • pdf/a-4e, pdf/a-4f - PDF/A-4 variants

Supported PDF/UA variants (universal accessibility):

  • pdf/ua-1, pdf/ua-2

Example usage:

curl -X POST "http://localhost:9080/convert/html?pdf_variant=pdf/a-2b" \
  -H "Content-Type: text/html" \
  -d "<html><body>Hello World</body></html>" \
  --output document.pdf

Breaking Change (WeasyPrint 67.0): The pdf/a-4b variant is no longer supported. Use pdf/a-4f or pdf/a-4e instead.

Device Scaling

Device Scaling can be configured via the DEVICE_SCALE_FACTOR environment variable. This allows you to adjust the scaling factor for the SVG to PNG conversion.

Valid range: 1.0 - 10.0 (default: 1.0)

To customize the device scaling when running the container:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --env DEVICE_SCALE_FACTOR=2.0 \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Note: Invalid values will fall back to default (1.0) with a warning logged.

Concurrency Control

The service limits concurrent SVG to PNG conversions to prevent memory leaks and resource exhaustion.

Valid range: 1 - 100 (default: 10)

To customize the concurrency limit when running the container:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --env MAX_CONCURRENT_CONVERSIONS=20 \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Note: Invalid values will fall back to default (10) with a warning logged.

Automatic Chromium Restart

The service can automatically restart the Chromium browser after a specified number of conversions to prevent memory accumulation and ensure long-term stability.

Valid range: 0 - 10000 (default: 0 = disabled)

To enable automatic restart after every 1000 conversions:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --env CHROMIUM_RESTART_AFTER_N_CONVERSIONS=1000 \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

How it works:

  • When enabled (value > 0), Chromium will automatically restart after reaching the specified conversion count
  • The restart happens transparently before the next conversion begins
  • Conversion counter resets to 0 after each restart
  • Set to 0 (default) to disable automatic restarts
  • Useful for long-running services with high conversion volumes

Note: Invalid values will fall back to default (0) with a warning logged.

Chromium Requirements and Recovery

The service requires a persistent Chromium browser instance for SVG to PNG conversion.

Startup Behavior (Fail-Fast):

  • The service will terminate if Chromium cannot be initialized during startup
  • Common causes: Missing dependencies, insufficient memory, or missing Chromium binaries
  • Docker requirements: --shm-size should be configured if running many concurrent conversions
  • Health check: The /health endpoint verifies Chromium is running and healthy at runtime

Automatic Recovery:

  • If a conversion fails due to Chromium crash or error, the service automatically restarts Chromium and retries
  • Valid range: 1 - 10 (default: 2)
  • This provides resilience against transient Chromium failures during operation
  • If restart fails or all retry attempts are exhausted, the conversion request will fail with an error
  • Recovery attempts are logged for monitoring and troubleshooting

To customize the number of retry attempts:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --env CHROMIUM_MAX_CONVERSION_RETRIES=3 \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Note: Invalid values will fall back to default (2) with a warning logged.

Monitoring:

  • Use Docker healthcheck or the /health endpoint to monitor service availability
  • Check service logs for automatic recovery events and conversion failures
  • Failed conversions are logged with WARNING level, recovery attempts with INFO level

To diagnose Chromium startup issues, check the service logs for error messages during initialization. The container will exit if Chromium fails to start.

Monitoring Dashboard

The service includes an interactive web-based monitoring dashboard accessible at /dashboard:

Dashboard Features:

Key Performance Indicators:

  • Service health status with real-time indicator
  • Total conversions (HTML→PDF and SVG→PNG)
  • Error rate percentage
  • Current queue size and active conversions
  • Average response time
  • System uptime and browser restarts

Interactive Charts:

  1. Queue & Active Conversions - Real-time visualization of request queue and concurrent processing
  2. CPU Usage (%) - CPU consumption tracking with percentage scale
  3. Memory Usage (MB) - Memory tracking showing Chromium memory, total system memory, and available memory

Technical Details:

  • Auto-refresh: Updates every 5 seconds
  • Data retention: Last 20 data points on charts
  • Technology: Chart.js 4.4.0 (bundled locally) for visualizations
  • Design: Light or dark theme support via environment variable
  • API endpoint: Fetches data from /health?detailed=true
  • Version information: Service, WeasyPrint, and Chromium versions displayed in the header

Theme Configuration:

The dashboard theme can be configured via the DASHBOARD_THEME environment variable:

Valid values: light, dark (case-insensitive, default: light)

To use dark theme:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --env DASHBOARD_THEME=dark \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Note: Invalid values will fall back to light theme with a warning logged.

Production Considerations:

  • Consider restricting dashboard access via reverse proxy (nginx, Traefik)
  • Use authentication middleware for sensitive environments
  • Monitor dashboard endpoint metrics separately

Prometheus & Grafana Integration

The service exposes Prometheus-compatible metrics for comprehensive monitoring and observability through Grafana dashboards.

Metrics Endpoint: /metrics on port 9180 (dedicated metrics port)

Security: The metrics endpoint is served on a separate port (9180) from the main API (9080). This allows network-level isolation using security groups or firewall rules to restrict metrics access to your Prometheus server only.

Available Metrics:

Conversion Metrics:

  • pdf_generations_total - Total successful PDF generations
  • pdf_generation_failures_total - Total failed PDF conversions
  • pdf_generation_error_rate_percent - PDF generation error rate
  • svg_conversions_total - Total successful SVG conversions
  • svg_conversion_failures_total - Total failed SVG conversions
  • svg_conversion_error_rate_percent - SVG conversion error rate

Performance Metrics:

  • pdf_generation_duration_seconds - PDF generation time histogram
  • svg_conversion_duration_seconds - SVG conversion time histogram
  • queue_time_seconds - Request queue wait time histogram
  • http_request_duration_seconds - HTTP request duration histogram

Resource Metrics:

  • cpu_percent - Current CPU usage
  • system_memory_total_bytes - Total system memory
  • system_memory_available_bytes - Available system memory
  • chromium_memory_bytes - Current Chromium memory usage
  • queue_size - Current requests in queue
  • active_pdf_generations - Active PDF generations processes

Health Metrics:

  • uptime_seconds - Service uptime
  • chromium_restarts_total - Chromium restart count
  • chromium_consecutive_failures - Health check failure streak

Prometheus Configuration Example:

scrape_configs:
  - job_name: 'weasyprint-service'
    static_configs:
      - targets: ['weasyprint-service:9180']  # Metrics on dedicated port
    metrics_path: '/metrics'
    scrape_interval: 15s
    scrape_timeout: 10s

Grafana Dashboard Queries:

# PDF generation rate (requests per second)
rate(pdf_generations_total[5m])

# Error rate percentage
(rate(pdf_generation_failures_total[5m]) + rate(svg_conversion_failures_total[5m]))
/ (rate(pdf_generations_total[5m]) + rate(svg_conversions_total[5m])) * 100

# 95th percentile conversion duration
histogram_quantile(0.95, rate(pdf_generation_duration_seconds_bucket[5m]))

# Memory usage (MB)
chromium_memory_bytes / 1024 / 1024

Docker Compose Example with Prometheus & Grafana:

version: '3.8'
services:
  weasyprint-service:
    image: ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest
    init: true
    ports:
      - "9080:9080"   # Main API
      - "9180:9180"   # Metrics endpoint

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

volumes:
  prometheus-data:
  grafana-data:

Pre-configured Monitoring Stack:

For a complete, production-ready monitoring setup with pre-configured Prometheus, Grafana, and dashboards:

cd monitoring
./start-monitoring.sh

This will start the WeasyPrint service, Prometheus, and Grafana with a pre-built dashboard. Access Grafana at http://localhost:3000 (admin/admin) and view the dashboard at http://localhost:3000/d/weasyprint-service.

For detailed setup instructions and configuration options, see monitoring/README.md.

For complete metric descriptions and alerting examples, see CLAUDE.md.

Logging Configuration

The service includes a robust logging system with the following features:

  • Log files are stored in /opt/weasyprint/logs directory
  • Log level can be configured via LOG_LEVEL environment variable (default: INFO)
  • Log format: timestamp - logger name - log level - message
  • Each service start creates a new timestamped log file

To customize logging when running the container:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --env LOG_LEVEL=DEBUG \
  --volume /path/to/local/logs:/opt/weasyprint/logs \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Available log levels:

  • DEBUG: Detailed information for debugging
  • INFO: General operational information (default)
  • WARNING: Warning messages for potential issues
  • ERROR: Error messages for failed operations
  • CRITICAL: Critical issues that require immediate attention

Container Security

The container is built with security best practices:

Non-root User:

  • Container runs as appuser (uid 1000) instead of root
  • Reduces attack surface and follows principle of least privilege
  • Compatible with Kubernetes pod security policies requiring non-root containers

OCI Security Labels: The image includes security metadata labels for container scanning and policy enforcement:

  • org.opencontainers.image.security.caps.drop="ALL" - Indicates all capabilities should be dropped
  • org.opencontainers.image.security.no-new-privileges="true" - Prevents privilege escalation

Recommended Runtime Security:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --security-opt no-new-privileges:true \
  --cap-drop ALL \
  --read-only \
  --tmpfs /tmp \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Note: When using --read-only, ensure log volume is mounted if persistent logging is required:

--volume /path/to/logs:/opt/weasyprint/logs

Using as a Base Image

To extend or customize the service, use it as a base image in the Dockerfile:

FROM ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Using Docker Compose

To run the service using Docker Compose:

docker-compose up -d

The Docker Compose configuration includes the init: true parameter which enables proper process management for the container.

Multipart form limits (environment variables)

The endpoint /convert/html-with-attachments parses multipart/form-data and supports configuring Starlette's form parsing limits via environment variables:

  • FORM_MAX_FIELDS: Maximum number of non-file form fields to accept. Default: 1000.
  • FORM_MAX_FILES: Maximum number of file parts to accept. Default: 1000.
  • FORM_MAX_PART_SIZE: Maximum allowed size in bytes for any single part (file or field). Default: 10485760 (10 MiB).

Notes:

  • Values are parsed as integers. Invalid or negative values fall back to the defaults (negative values are clamped to 0 internally).
  • These limits only affect the /convert/html-with-attachments endpoint. The endpoint requires Content-Type: multipart/form-data and will return 400 Bad Request otherwise.

Examples:

Docker run:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  -e FORM_MAX_FIELDS=2000 \
  -e FORM_MAX_FILES=2000 \
  -e FORM_MAX_PART_SIZE=20971520 \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Mount a custom fonts folder

The following entry may be added to the run command:

docker run --init -v /path/to/host/fonts:/usr/share/fonts/custom ...

Replace /path/to/host/fonts with the folder containing custom fonts

Insert native sticky notes into final PDF document

You can insert native PDF sticky note annotations at specific positions in the resulting document by using the following HTML structure (nested notes are supported for replies):

  <span class="sticky-note">
    <span class="sticky-note-time">2025-04-30T07:24:55.000+02:00</span>
    <span class="sticky-note-username">Test User 1</span>
    <span class="sticky-note-title">Test Title</span>
    <span class="sticky-note-text">Test sticky note text</span>

    <span class="sticky-note">
      <span class="sticky-note-time">2020-05-12T08:17:02.000+02:00</span>
      <span class="sticky-note-username">Test User 2</span>
      <span class="sticky-note-title">Reply Title</span>
      <span class="sticky-note-text">Reply text</span>
    </span>

  </span>

Development

Docker Image Architecture

The Docker image uses a multi-stage build with the following components:

  • Base image: debian:trixie-slim (same base as python:3.14-slim)
  • Python: Installed via uv from .tool-versions file
  • Package manager: uv for fast, reproducible dependency management
  • Runtime user: Non-root appuser (uid 1000)

Key benefits:

  • Single source of truth for Python version (.tool-versions)
  • Faster builds with uv cache mounts
  • Smaller attack surface with non-root execution
  • Consistent with local development environment

Building the Docker Image

To build the Docker image from the source with a custom version, use:

docker build \
  --build-arg APP_IMAGE_VERSION=0.0.0 \
  --file Dockerfile \
  --tag weasyprint-service:0.0.0 .

Replace 0.0.0 with the desired version number.

Running the Development Container

To start the Docker container with your custom-built image:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --publish 9180:9180 \
  --name weasyprint-service \
  weasyprint-service:0.0.0

Stopping the Container

To stop the running container, execute:

docker container stop weasyprint-service

Testing

container-structure-test

The container-structure-test tool is used to verify that the Docker image meets expected standards and specifications. It validates the container structure, ensuring proper file paths, permissions, and commands are available, which helps maintain consistency and reliability of the containerized application.

Before running the following command, ensure that the container-structure-test tool is installed. You can find installation instructions in the official documentation.

container-structure-test test --image weasyprint-service:0.0.0 --config ./tests/container/container-structure-test.yaml

grype

Grype is used for vulnerability scanning of the Docker image. This tool helps identify known security vulnerabilities in the dependencies and packages included in the container, ensuring the deployed application meets security standards and doesn't contain known exploitable components.

To scan the Docker image for vulnerabilities, you can use Grype. First, ensure that Grype is installed by following the installation instructions.

Then run the vulnerability scan on your image:

grype weasyprint-service:0.0.0

tox

Tox automates testing in different Python environments, ensuring that the application works correctly across various Python versions and configurations. It helps maintain compatibility and provides a standardized way to run test suites, formatting checks, and other quality assurance processes.

uv run tox

pytest (for debugging)

Pytest is used for unit and integration testing of the application code. These tests verify that individual components and the entire application function correctly according to specifications. Running pytest during development helps catch bugs early and ensures code quality.

# all tests
uv run pytest
# a specific test
uv run pytest tests/test_svg_processor.py -v

pre-commit

Pre-commit hooks run automated checks on code before it's committed to the repository. This ensures consistent code style, formatting, and quality across the project. It helps catch common issues early in the development process, maintaining high code standards and reducing the need for style-related revisions during code reviews.

uv run pre-commit run --all

REST API

This service provides REST API. OpenAPI Specification can be obtained here.

About

Rest API service providing WeasyPrint functionality

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors 9

Languages