Image Converter Configuration Guide

The image converter is a Node.js microservice that renders SVG venue backgrounds into optimized WebP images. It produces three variants per background – full resolution, a 1000px preview, and a 400px blurred placeholder with inline base64 data for progressive loading. Images are stored in S3-compatible object storage or on the local filesystem.

Stack: Node.js 22, Express 5, Puppeteer (headless Chromium for SVG rendering), Sharp (image processing), Redis (conversion queue).

Core settings

Setting Type Default Description
PORT integer 3000 Express server listening port
NODE_ENV string production Node environment
DEBUG boolean false Enable debug mode (sets LOG_LEVEL to debug)
LOG_LEVEL string info Winston log level: error, warn, info, http, verbose, debug

File handling settings

Setting Type Default Description
UPLOAD_FOLDER string /tmp Temporary directory for file uploads (cleaned up after processing)
MAX_CONTENT_LENGTH integer 16777216 Maximum upload file size in bytes (default 16 MB)

Image processing settings

Setting Type Default Description
MAX_IMAGE_WIDTH integer 15000 Maximum allowed output image width in pixels
MAX_IMAGE_HEIGHT integer 15000 Maximum allowed output image height in pixels
DEFAULT_WIDTH integer 440 Default width for basic conversion endpoint
DEFAULT_HEIGHT integer 246 Default height for basic conversion endpoint
PNG_OPTIMIZATION boolean false Enable image compression
PNG_OPTIMIZATION_QUALITY string 0.6-0.8 Quality range for WebP output

Storage settings

Setting Type Default Description
STORAGE_TYPE string s3 Storage backend: s3 or local
LOCAL_STORAGE_PATH string ./storage Filesystem path when using local storage

AWS S3 configuration

These settings are required when STORAGE_TYPE=s3.

Setting Type Default Description
AWS_ACCESS_KEY_ID string (required) IAM access key for S3 authentication
AWS_SECRET_ACCESS_KEY string (required) IAM secret key for S3 authentication
AWS_REGION string us-east-1 AWS region for the S3 bucket
AWS_BUCKET_NAME string (required) S3 bucket name for storing images
AWS_ENDPOINT_URL string Custom S3 endpoint for S3-compatible providers (MinIO, DigitalOcean Spaces, Garage)
AWS_PUBLIC_BASE_URL string Override public URL base for generated image URLs (see below)

ImportantAWS credentials should be provided through secure environment variables or Kubernetes secrets. Never commit credentials to version control.

Public URL resolution

The converter generates public URLs for uploaded images. The URL base is resolved in this priority order:

  1. AWS_PUBLIC_BASE_URL (if set) – used as-is. Example: https://bucket.nyc3.digitaloceanspaces.com
  2. AWS_ENDPOINT_URL (derived) – path-style: {endpoint}/{bucket}. Example: https://storage.yandexcloud.net/my-bucket
  3. Default – AWS virtual-hosted style: https://{bucket}.s3.{region}.amazonaws.com

Self-hosted customers using non-AWS S3 providers (DigitalOcean Spaces, MinIO, Garage, Yandex Cloud) should set AWS_PUBLIC_BASE_URL to ensure correct public URLs.

S3 bucket policy

To allow public read access to images, apply the following bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

CORS configuration

To prevent tainted canvas issues when processing images in web browsers, configure CORS on the bucket:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": []
  }
]

NoteFor production environments, restrict AllowedOrigins to your specific domains instead of *.

Redis settings

Conversions are queued in Redis when REDIS_ENABLED=true (the Helm chart default). Each request is recorded as a job; a converter with a GPU claims it first, a CPU converter claims it after CPU_CLAIM_DELAY_MS, and a job whose worker dies mid-render is returned to the queue and retried. With REDIS_ENABLED=false the converter renders inside the request and needs no Redis. While Redis is enabled but unreachable the converter also renders inside the request, answers 503 on GET /jobs/{id} and logs connection errors; queueing starts on its own once Redis is reachable.

Setting Type Default Description
REDIS_ENABLED boolean false Queue conversions in Redis instead of rendering inside the request
REDIS_HOST string localhost Redis server hostname
REDIS_PORT integer 6379 Redis server port
REDIS_PASSWORD string Redis password (if authentication is required)
CONVERTER_ROLE string cpu gpu claims a queued job immediately; cpu waits CPU_CLAIM_DELAY_MS first
CPU_CLAIM_DELAY_MS integer 5000 Head start a GPU converter gets before a CPU converter claims the job
WORKER_ENABLED boolean true false makes the pod accept submissions without rendering
JOB_TTL_SECONDS integer 3600 Lifetime of a job record in Redis
RESULT_TTL_SECONDS integer 900 Lifetime of a rendered result in Redis
JOB_VISIBILITY_TIMEOUT_MS integer 300000 Render budget before a claimed job is returned to the queue
JOB_MAX_ATTEMPTS integer 3 Attempts before a job is recorded as failed
WORKER_IDLE_POLL_MS integer 500 Queue polling cadence of an idle worker
REAPER_INTERVAL_MS integer 30000 Interval of the sweep that returns lost claims to the queue

Authentication

Setting Type Default Description
API_KEY string Shared secret. When set, every conversion route and GET /jobs/{id} require Authorization: Bearer <value> and answer 401 without it; /health, /metrics and the local-storage asset routes stay open. The Helm chart generates the value and wires it into editor-service

API endpoints

Method Path Description
POST / Basic SVG to image conversion. Returns the image file directly. Query params: width, height
POST /background/ Full background conversion. Returns JSON with three variant URLs (full, preview, blurred). Query params: filename, debug
POST /thumbnail/ Thumbnail conversion. Returns JSON with three variant URLs. Query params: filename, debug
POST /photo/ Section photo conversion. Returns JSON with the photo and thumbnail URLs
GET /jobs/{id} A queued conversion: { requestId, kind, status, attempts, enqueuedAt, finishedAt, result }. status is pending, running, succeeded or failed; result is null until the job finishes, then the JSON the synchronous route would have returned. 404 for an unknown or expired id (results are kept for RESULT_TTL_SECONDS), 503 when queueing is off
GET /health Service health check. Returns { "status": "healthy" }
GET /health/redis Redis connection status, the pod’s conversion role, and the pending and processing queue depths
GET /metrics Prometheus metrics in text format

POST /, /background/ and /thumbnail/ accept multipart/form-data with an SVG file (image/svg+xml). POST /photo/ accepts a JPEG, PNG or WebP file up to 10 MB and answers with url and thumbUrl.

With queueing enabled, a conversion route answers 202 Accepted with a job id and the result is read from GET /jobs/{id}; editor-service does this polling itself. With queueing disabled the routes answer with the result directly.

Rate limiting: RATE_LIMIT_MAX requests (default 600) per RATE_LIMIT_WINDOW_MS (default 60000) per IP address on the conversion routes, /photo/ included. GET /jobs/{id} has its own budget, JOBS_RATE_LIMIT_MAX requests per minute (default 5000). /health and /metrics are not throttled.

Request timeout: 5 minutes (300 seconds).

Output format

The converter generates WebP images (not PNG), providing 30-50% smaller file sizes with alpha channel support.

Three variants are produced for each background:

Variant Width Quality Purpose
full Original 60-80% Full-resolution venue background
preview 1000px 60-80% Quick-loading preview for initial render
blurred 400px 30-50% Placeholder with inline base64 data for progressive loading

Files are stored at backgrounds/{filename}/{variant}.webp in the configured storage backend.

Docker

The service runs in a Docker container based on node:22.14.0-alpine with Chromium installed for Puppeteer.

Port: 3000

Health check: GET /health every 30 seconds (3 retries, 5-second start period)

Environment variables for Puppeteer:

  • PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true (uses system Chromium)
  • PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

Security: Runs as non-root user (uid 10001). Uses tini as init process to prevent zombie processes.

Monitoring

Prometheus metrics

The /metrics endpoint exposes:

  • http_request_duration_seconds – histogram with method, route, status_code labels
  • http_requests_total – counter with method, route, status_code labels
  • Default Node.js process metrics (memory, CPU, event loop lag, GC)

Metric buckets are optimized for image processing workloads: 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30 seconds.

Health endpoints

  • GET /health – returns 200 if the service is running
  • GET /health/redis – returns Redis connection status, the pod’s conversion role, and the pending and processing queue depths

Configuration methods

The application can be configured through multiple methods listed in order of precedence:

  1. Environment variables
  2. Kubernetes ConfigMap/Secrets
  3. .env file (development only)

Kubernetes deployment

When deploying to Kubernetes, configuration is managed through Helm values and secrets:

converter:
  image:
    repository: registry.gitlab.com/seatmap.pro/seatmap/converter-service
    tag: latest
  env:
    PORT: '3000'
    LOG_LEVEL: 'info'
    STORAGE_TYPE: 's3'
    AWS_REGION: 'us-east-1'
    AWS_BUCKET_NAME: 'your-bucket'
    # AWS_PUBLIC_BASE_URL: "https://your-cdn.example.com"  # if using non-AWS S3

Secrets management

AWS credentials and Redis passwords should be managed securely using one of these methods:

  • Kubernetes Secrets (injected via seatmap-helm-secrets)
  • External Secrets Operator
  • IAM roles for Service Accounts (IRSA)

Development setup

For local development using Docker Compose:

cd products/converter-service
cp .env.sample .env
# Edit .env with your settings (use STORAGE_TYPE=local for development)
docker compose up

This starts the converter service on port 3000 and a Redis instance on port 6379.

Never commit sensitive credentials to version control.

Troubleshooting

Common configuration issues:

  • Image upload fails: Check MAX_CONTENT_LENGTH and ingress body-size configuration. The default 16 MB limit may need increasing for large SVG files.
  • S3 access denied: Verify AWS credentials and bucket permissions. Check that AWS_PUBLIC_BASE_URL matches your actual bucket URL scheme.
  • Blank or corrupted images: Verify Chromium is installed (/usr/bin/chromium-browser). In Docker, ensure the PUPPETEER_EXECUTABLE_PATH environment variable is set correctly.
  • High memory usage: Sharp and Puppeteer can consume significant memory with large SVG files. Set container memory limits to at least 512 MB, with 2 GB recommended for production.
  • WebP not loading in renderer: Ensure CORS headers are configured on the S3 bucket. The renderer needs cross-origin access to load background images.
  • Wrong public URLs: If using a non-AWS S3 provider, set AWS_PUBLIC_BASE_URL explicitly. The default URL generation assumes AWS virtual-hosted bucket style.