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:
AWS_PUBLIC_BASE_URL(if set) – used as-is. Example:https://bucket.nyc3.digitaloceanspaces.comAWS_ENDPOINT_URL(derived) – path-style:{endpoint}/{bucket}. Example:https://storage.yandexcloud.net/my-bucket- 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
AllowedOriginsto 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 labelshttp_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 runningGET /health/redis– returns Redis connection status, the pod’s conversionrole, and thependingandprocessingqueue depths
Configuration methods
The application can be configured through multiple methods listed in order of precedence:
- Environment variables
- Kubernetes ConfigMap/Secrets
.envfile (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_LENGTHand 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_URLmatches your actual bucket URL scheme. - Blank or corrupted images: Verify Chromium is installed (
/usr/bin/chromium-browser). In Docker, ensure thePUPPETEER_EXECUTABLE_PATHenvironment 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_URLexplicitly. The default URL generation assumes AWS virtual-hosted bucket style.