Skip to content

Latest commit

 

History

1,121 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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.

Font Subsetting

By default, WeasyPrint subsets fonts to include only the glyphs used in the document, reducing PDF size. However, some fonts have invalid OS/2 Unicode range bits that cause subsetting to fail with:

ValueError: expected 0 <= int <= 122, found: 123

Use the full_fonts=true query parameter to disable font subsetting and embed complete fonts:

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

Note: This increases PDF file size but avoids font subsetting errors.

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

External resources of a document

A document names its own resources: an image, a font, a stylesheet. WeasyPrint loads what it names, so a document reaching this service can make it read an address of the network it sits in, and <link rel="attachment"> returns the answer inside the produced PDF.

Every load therefore goes through a policy. By default the service refuses every address which is not public: loopback, private ranges, link local including the cloud metadata address 169.254.169.254, and their IPv6 equivalents.

Variable Default Purpose
EXTERNAL_RESOURCES_POLICY BLOCK_INTERNAL BLOCK_INTERNAL, ALLOWLIST_ONLY or ALLOW_ALL
EXTERNAL_RESOURCES_ALLOWED_ORIGINS empty Comma separated origins which are allowed whatever they resolve to
EXTERNAL_RESOURCES_MAX_SIZE_MB 16 Size a single resource may reach
EXTERNAL_RESOURCES_TIMEOUT_SECONDS 10 Time the whole load of one resource may take, its redirects and its address attempts included. Resolving a name is bounded by the resolver of the host rather than by this value, so one lookup may outlast it. No further lookup starts once it has passed

To load resources from an internal host, list its origin:

EXTERNAL_RESOURCES_ALLOWED_ORIGINS=cdn.intranet,https://images.intranet:8443

An entry is written [scheme://]host[:port], and what it leaves out is not compared:

Entry What it allows
cdn.intranet that host under either scheme, on any port
cdn.intranet:8443 that host on port 8443, under either scheme
https://cdn.intranet that host under https, on port 443
https://cdn.intranet:8443 that host under https, on port 8443

The three policies:

# BLOCK_INTERNAL (default) - public addresses and the allowed origins
# ALLOWLIST_ONLY           - only the allowed origins
# ALLOW_ALL                - no restriction, this exposes the network of the service to whoever writes a document
-e EXTERNAL_RESOURCES_POLICY=ALLOWLIST_ONLY

How a request is made. The name is resolved, every address it answers with is checked, and the request is bound to what was checked, so a second lookup cannot answer differently. Every redirect hop is checked again, at most five of them. A loaded resource has to be an image, a font or a stylesheet, by the declared type and, where a server declares none, by the content itself. That last rule is what closes <link rel="attachment"> as a way to read a body back, and it costs one thing: a stylesheet served without a content type is refused, because a stylesheet cannot be told from any other text by its first bytes, and it is text bodies which the rule is there to keep out. A server which declares text/css is loaded as before.

Schemes. data: passes, it carries its own content. file: is refused, except for the files uploaded with a request to /convert/html-with-attachments, which the service itself put in a temporary directory. ftp: and everything else are refused.

Behind a proxy. A proxy resolves the name itself, so such a request cannot be bound to a checked address. It is made only for a host listed in EXTERNAL_RESOURCES_ALLOWED_ORIGINS. Where a proxy is configured for every destination, BLOCK_INTERNAL therefore behaves as ALLOWLIST_ONLY.

Blocked resources are reported in the log and the document is rendered without them, which is how WeasyPrint treats a resource it cannot load.

HTTPS

Both servers speak plain HTTP by default, which is what a deployment behind a reverse proxy or an ingress expects: TLS terminates there and nothing has to be configured here. That remains the recommended setup where such a component is already in place.

Where the service is reached directly across a network, each server can serve TLS itself.

The API server:

Variable Purpose
TLS_CERT_FILE Certificate chain in PEM format
TLS_KEY_FILE Private key in PEM format
TLS_KEY_PASSWORD Password of the key, where it has one
TLS_CLIENT_CA_FILE CA that client certificates are verified against
TLS_CLIENT_AUTH none (default), optional or required

The metrics server, configured on its own:

Variable Purpose
METRICS_TLS_CERT_FILE Certificate chain in PEM format
METRICS_TLS_KEY_FILE Private key in PEM format
METRICS_TLS_KEY_PASSWORD Password of the key, where it has one
METRICS_TLS_CLIENT_CA_FILE CA that client certificates are verified against
METRICS_TLS_CLIENT_AUTH none (default), optional or required

The two sets are independent and neither inherits from the other. The metrics port can stay plain behind a network rule while the API serves TLS, or the other way round.

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --volume /path/to/tls:/opt/weasyprint/tls:ro \
  -e TLS_CERT_FILE=/opt/weasyprint/tls/server.pem \
  -e TLS_KEY_FILE=/opt/weasyprint/tls/server.key \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

An incomplete configuration stops the start with a message naming the variable. The service never falls back to plain HTTP, because serving in the clear while an operator believes otherwise is worse than not starting.

The material is loaded before either server listens, so a key which does not match its certificate, a wrong TLS_KEY_PASSWORD or an unusable CA also stop the start, rather than surfacing at the first connection.

Mutual TLS. With TLS_CLIENT_AUTH=required the service accepts only clients presenting a certificate signed by TLS_CLIENT_CA_FILE. That authenticates the caller, which the API key below does not: the certificate says who connected, the key says who may convert. The two combine.

The healthcheck of the container follows the configured scheme. It talks to its own process over loopback, so it does not verify the certificate. Where client certificates are required, give the probe one with TLS_HEALTHCHECK_CERT_FILE and TLS_HEALTHCHECK_KEY_FILE, otherwise the container reports itself unhealthy. The two go together: one without the other fails the probe with a message naming them, rather than an opaque handshake error.

Certificate renewal. The certificate is read once, at startup. A renewed certificate takes effect when the container restarts.

Prometheus. Once the metrics port serves TLS, its scrape configuration needs scheme: https next to the target.

API Key Authentication

The conversion endpoints can be protected with an API key. The feature is optional and disabled by default.

Configuration:

  • API_KEY: One or more API keys. Authentication is disabled when the variable is unset or empty.
  • Several keys are configured as a comma-separated list, for example API_KEY=key-a,key-b. This allows key rotation without downtime.

Protected endpoints:

  • POST /convert/html
  • POST /convert/html-with-attachments

/health, /version, /dashboard, /static and /api/docs stay open. This keeps the Docker healthcheck and the monitoring dashboard working. The Prometheus endpoint on port 9180 is not affected; isolate it at the network level as described in Prometheus & Grafana Integration.

Start the service with authentication:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  -e API_KEY=your-secret-key \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Send the key in the X-API-Key header:

export API_KEY=your-secret-key

curl -X POST http://localhost:9080/convert/html \
  -H "X-API-Key: ${API_KEY}" \
  -H "Content-Type: text/html" \
  --data-binary @index.html \
  --output document.pdf

Or as a bearer token:

curl -X POST http://localhost:9080/convert/html \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: text/html" \
  --data-binary @index.html \
  --output document.pdf

Requests with a missing or invalid key return 401 Unauthorized with the header WWW-Authenticate: Bearer. The key value is never written to the log.

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>

Memory Requirements

The minimum recommended memory for the weasyprint-service container is 2 GB.

Memory consumption depends on the size and complexity of documents being converted. For large documents (1000+ pages with images), peak memory usage can reach 3 GB or more. When running multiple concurrent conversions, memory requirements increase proportionally — plan for approximately 1 GB of additional memory per concurrent conversion of a large document.

To limit memory usage in Docker:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --memory=2g \
  --name weasyprint-service \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Note: After a conversion completes, the container's reported memory (RSS) may not decrease immediately. This is normal behavior — Python's memory allocator and glibc malloc retain freed memory as a cache for future allocations rather than returning it to the OS. This memory will be reused by subsequent conversions and does not indicate a memory leak.

Post-Conversion Memory Reclamation

By default, Python and glibc do not return freed memory to the OS after a conversion completes. This can be problematic when occasional traffic spikes (e.g. 10 parallel conversions) cause high memory consumption that persists long after the spike ends.

To enable automatic memory reclamation after each conversion, set the RECLAIM_MEMORY_AFTER_CONVERSION environment variable:

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

When enabled, the service runs gc.collect() followed by glibc's malloc_trim(0) after each PDF generation, which forces Python to collect unreachable objects and asks the C allocator to return free memory pages to the OS.

For even better memory reclamation, combine with MALLOC_ARENA_MAX=2 to reduce glibc arena fragmentation:

docker run --detach \
  --init \
  --publish 9080:9080 \
  --name weasyprint-service \
  --env RECLAIM_MEMORY_AFTER_CONVERSION=true \
  --env MALLOC_ARENA_MAX=2 \
  ghcr.io/schweizerischebundesbahnen/weasyprint-service:latest

Trade-offs:

Enabled Disabled (default)
Memory after spike Returns to near-baseline Stays at peak level
Performance Small overhead per conversion (~1–5 ms) No overhead
Subsequent conversions May be slightly slower (re-allocation) Faster (memory reused from cache)

Recommendation: Enable this option in memory-constrained environments or when traffic is bursty. Leave disabled if conversions are frequent and steady, as cached memory improves performance.

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.

VSDX Support

To enable VSDX (Microsoft Visio) file conversion support, add the ENABLE_VSDX_SUPPORT=true build argument:

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

This installs LibreOffice (~200MB additional size) for VSDX to PNG conversion. Without this flag, VSDX images will remain unchanged in the output.

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

19 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages