A dockerized service providing a REST API interface to leverage WeasyPrint's functionality for generating PDF documents from HTML and CSS.
- 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
To install the latest version of the WeasyPrint Service, run the following command:
docker pull ghcr.io/schweizerischebundesbahnen/weasyprint-service:latestTo start the WeasyPrint service container, execute:
docker run --detach \
--init \
--publish 9080:9080 \
--publish 9180:9180 \
--name weasyprint-service \
ghcr.io/schweizerischebundesbahnen/weasyprint-service:latestThe service will be accessible on port 9080, and Prometheus metrics on port 9180.
Important: The
--initflag enables Docker's built-in init process which handles signal forwarding and zombie process reaping. This is required for proper operation of the service.
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 levelspdf/a-2u,pdf/a-3u,pdf/a-4u- Unicode conformance levelspdf/a-1a,pdf/a-2a,pdf/a-3a- Accessible conformance levelspdf/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.pdfBreaking Change (WeasyPrint 67.0): The
pdf/a-4bvariant is no longer supported. Usepdf/a-4forpdf/a-4einstead.
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:latestNote: Invalid values will fall back to default (1.0) with a warning logged.
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:latestNote: Invalid values will fall back to default (10) with a warning logged.
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:latestHow 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.
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-sizeshould be configured if running many concurrent conversions - Health check: The
/healthendpoint 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:latestNote: Invalid values will fall back to default (2) with a warning logged.
Monitoring:
- Use Docker healthcheck or the
/healthendpoint 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.
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:
- Queue & Active Conversions - Real-time visualization of request queue and concurrent processing
- CPU Usage (%) - CPU consumption tracking with percentage scale
- 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:latestNote: 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
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 generationspdf_generation_failures_total- Total failed PDF conversionspdf_generation_error_rate_percent- PDF generation error ratesvg_conversions_total- Total successful SVG conversionssvg_conversion_failures_total- Total failed SVG conversionssvg_conversion_error_rate_percent- SVG conversion error rate
Performance Metrics:
pdf_generation_duration_seconds- PDF generation time histogramsvg_conversion_duration_seconds- SVG conversion time histogramqueue_time_seconds- Request queue wait time histogramhttp_request_duration_seconds- HTTP request duration histogram
Resource Metrics:
cpu_percent- Current CPU usagesystem_memory_total_bytes- Total system memorysystem_memory_available_bytes- Available system memorychromium_memory_bytes- Current Chromium memory usagequeue_size- Current requests in queueactive_pdf_generations- Active PDF generations processes
Health Metrics:
uptime_seconds- Service uptimechromium_restarts_total- Chromium restart countchromium_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: 10sGrafana 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.shThis 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.
The service includes a robust logging system with the following features:
- Log files are stored in
/opt/weasyprint/logsdirectory - Log level can be configured via
LOG_LEVELenvironment 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:latestAvailable 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
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 droppedorg.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:latestNote: When using --read-only, ensure log volume is mounted if persistent logging is required:
--volume /path/to/logs:/opt/weasyprint/logsTo extend or customize the service, use it as a base image in the Dockerfile:
FROM ghcr.io/schweizerischebundesbahnen/weasyprint-service:latestTo run the service using Docker Compose:
docker-compose up -dThe Docker Compose configuration includes the init: true parameter which enables proper process management for the container.
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:latestThe 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
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>The Docker image uses a multi-stage build with the following components:
- Base image:
debian:trixie-slim(same base aspython:3.14-slim) - Python: Installed via uv from
.tool-versionsfile - 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
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.
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.0To stop the running container, execute:
docker container stop weasyprint-serviceThe 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.yamlGrype 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.0Tox 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 toxPytest 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 -vPre-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 --allThis service provides REST API. OpenAPI Specification can be obtained here.