Deployment¶
Architecture¶
A production Encore deployment consists of three components:
graph TB
LB[Load Balancer] --> Web1[encore-web]
LB --> Web2[encore-web]
Web1 --> Redis[(Redis)]
Web2 --> Redis
Redis --> W1[encore-worker]
Redis --> W2[encore-worker]
Redis --> W3[encore-worker]
W1 --> FFmpeg1[FFmpeg]
W2 --> FFmpeg2[FFmpeg]
W3 --> FFmpeg3[FFmpeg]
- encore-web — the REST API. Can run behind a load balancer for high availability. Can optionally process jobs itself (polling is enabled by default).
- encore-worker — a headless worker that polls one job from the queue, transcodes it, and exits. Set
encore-settings.worker-drain-queue=trueto keep polling until the queue is empty. Scale horizontally by running multiple workers. See Scaling with KEDA for an example of autoscaling workers in Kubernetes. - Redis 8+ — job storage and queue. Requires the JSON and Search modules. Should have persistence enabled for production.
Tip
If all transcoding should be done by workers, set encore-settings.poll-disabled=true on encore-web so it only serves the API.
Upgrading from v0.2.x to v1.0.0¶
v1.0.0 replaces Spring Data Redis with a direct Lettuce integration backed by the Redis JSON and Search modules. The Redis data model has changed, so existing jobs and queues from a v0.2.x instance are not readable by v1.0.0 and may be lost if you upgrade in place.
Things to check before upgrading:
- Redis version and modules. v1.0.0 requires Redis 8+ with the JSON and Search modules enabled. May work with Valkey or other Redis-compatible servers, but this has not been tested. Managed Redis offerings may need to be reconfigured or replaced.
- Audio codec defaults. The
AudioEncode/SimpleAudioEncodedefault codec changed fromlibfdk_aactoaacin v1.0.0. Profiles that still needlibfdk_aacmust setcodec: libfdk_aacexplicitly. - Redis connection config changed. The
spring.data.redis.*properties (host, port, password) are no longer used. Configure Redis via a single Lettuce URI and theprofile.locationproperty has moved: - Replace
spring.data.redis.host/spring.data.redis.portwithencore-settings.redis.uri(env:ENCORE_SETTINGS_REDIS_URI), e.g.redis://redis.example.com:6379 - Replace
profile.locationwithencore-settings.profile.location(env:ENCORE_SETTINGS_PROFILE_LOCATION) - See the Lettuce URI syntax for passwords and TLS.
- Security configuration. The
encore-settings.security.user-passwordandencore-settings.security.admin-passwordproperties have been replaced with ausersmap. See Security for the new format.
Recommended upgrade path:
- Stand up a Redis 8+ instance with the JSON and Search modules.
- Deploy v1.0.0 alongside the existing v0.2.x instance, pointing it at the new Redis — or at the same server with a distinct
encore-settings.redis.prefixso the two installations don't share keys. - Route newly submitted jobs to the v1.0.0 instance.
- Let the v0.2.x instance finish its outstanding queue, then shut it down.
Docker images¶
Pre-built Docker images are published to the GitHub Container Registry on each release:
| Image | Tags |
|---|---|
ghcr.io/svt/encore-web |
latest, 1.0.0, … |
ghcr.io/svt/encore-worker |
latest, 1.0.0, … |
These images bundle FFmpeg from mwader/static-ffmpeg and MediaInfo. They include libx264, libx265, aac, and most common codecs, but not libfdk_aac.
To build your own image with a custom FFmpeg (e.g. with libfdk_aac), Encore provides two Dockerfile variants in the encore-web and encore-worker modules:
- Native image (
Dockerfile) — GraalVM AOT-compiled binary. Faster startup, lower memory usage. - JAR image (
Dockerfile-jar, encore-web only) — standard JVM. Requires Java 25+.
Both require a base image with FFmpeg and MediaInfo installed.
Note
SVT uses an internal base image with FFmpeg compiled with the non-free codec libfdk_aac. Due to licensing restrictions, this image is not publicly available. If you need libfdk_aac or other non-free codecs, you need to build your own base image.
Build the images:
# Build artifacts (requires GraalVM 25)
./gradlew build nativeCompile -x test
# Build Docker images
docker build --build-arg DOCKER_BASE_IMAGE=your-ffmpeg-base:latest \
-t encore-web:latest encore-web/
docker build --build-arg DOCKER_BASE_IMAGE=your-ffmpeg-base:latest \
-t encore-worker:latest encore-worker/
Artifacts (JARs and native binaries) are also published to GitHub Releases on each tagged release.
Redis setup¶
Encore requires Redis 8+ with the JSON and Search modules for job storage, queuing, and pub/sub messaging. Redis 8 (redis:8.6-alpine) includes these modules by default. May work with Valkey or other Redis-compatible servers that support these modules, but this has not been tested.
| Property | Default | Description |
|---|---|---|
encore-settings.redis.uri |
redis://localhost:6379 |
Redis connection URI (Lettuce URI format) |
encore-settings.redis.prefix |
encore |
Key prefix for all Redis keys |
encore-settings.redis.job-expire-time |
7d |
TTL for completed jobs |
The prefix is applied consistently to all Redis keys, making it safe to run multiple Encore instances against the same Redis by giving each a unique prefix.
Example URIs:
redis://localhost:6379
redis://password@redis.example.com:6379
rediss://password@redis.example.com:6380
redis-sentinel://password@sentinel1:26379,sentinel2:26379/mymaster
Scaling workers¶
Why priority queues?¶
Transcoding jobs can run for many hours, and hardware is always finite. Without prioritisation, a small number of long-running low-priority jobs could occupy every available worker, forcing urgent jobs to wait for hours. Priority queues solve this by isolating job classes so that low-priority transcodes can never block high-priority ones — each priority class has its own queue and its own pool of workers.
How concurrency is wired up¶
The encore-settings.concurrency setting defines how many priority queues exist in Redis. Each queue represents a priority class, and a job is routed to a queue based on its priority when it is submitted. Each queue is itself a priority queue — jobs within a queue are pulled in priority order, so a higher-priority job in a given class is always picked up before a lower-priority one in the same class.
How that concurrency value is consumed depends on the component:
encore-workeralways uses a single thread and polls a single queue. Pin each worker to a specific queue viaencore-settings.poll-queue. To increase throughput for a priority class, run more workers pinned to that queue.encore-webwith polling enabled starts one thread per queue (=concurrencythreads in total) so a single web instance can serve every priority class by itself. For production deployments, prefer runningencore-webwithpoll-disabled: trueand dedicatingencore-workerpods to each priority class.
Within each thread, jobs are processed one at a time — a thread is blocked for the full duration of its current transcode.
The right value for concurrency depends entirely on how many priority classes your workload needs. There is no universally correct default: pick the number of priority classes required to keep urgent work unblocked by routine work, and size each worker pool independently from there.
Example deployment¶
The examples below use concurrency: 2 — one high-priority queue and one low-priority queue — with dedicated workers for each class. This is intentionally minimal to keep the example readable; real deployments commonly use more classes.
graph LR
API[encore-web<br/>poll-disabled: true] --> Redis[(Redis)]
Redis --> HP[high-priority-worker<br/>poll-queue: 0]
Redis --> HP2[high-priority-worker<br/>poll-queue: 0]
Redis --> LP[low-priority-worker<br/>poll-queue: 1]
Redis --> LP2[low-priority-worker<br/>poll-queue: 1]
Each worker pool scales independently: if a long low-priority job occupies every low-priority worker, the high-priority workers remain free and continue picking up urgent work.
Priority-to-queue mapping¶
Job priorities range from 0 (default, lowest) to 100 (highest). The priority determines which queue a job lands in; within that queue, jobs are still ordered by priority so finer-grained ranking is preserved.
With concurrency: 2 the mapping between job priority and queue is:
| Job priority | Queue | Role |
|---|---|---|
| 50–100 | 0 | High priority |
| 0–49 | 1 | Low priority |
Queue 0 always holds the highest-priority class; queue concurrency - 1 always holds the lowest.
poll-higher-prio¶
The poll-higher-prio flag decides whether a thread pinned to a lower-priority queue may pick up jobs from higher-priority queues.
poll-higher-prio: true(default) — higher-priority queues are always polled before the assigned queue. This lets lower-priority threads assist with higher-priority workloads whenever higher-priority jobs are waiting.poll-higher-prio: false— the thread only polls its assigned queue.
Recommended setting for encore-worker is false, so each worker pool stays strictly within its priority class. Either setting preserves the core guarantee that low-priority jobs can never be picked up ahead of high-priority jobs.
Configuration reference¶
| Property | Default | Description |
|---|---|---|
encore-settings.concurrency |
2 |
Number of priority queues. Also the number of polling threads in encore-web. |
encore-settings.poll-queue |
(all) | Pin instance to a specific queue number. Required on encore-worker to select its priority class. |
encore-settings.poll-higher-prio |
true |
Poll higher-priority queues before the assigned queue |
encore-settings.poll-disabled |
false |
Disable polling entirely (API-only mode for encore-web) |
encore-settings.poll-delay |
5s |
Interval between queue polls |
encore-settings.poll-initial-delay |
10s |
Delay after startup before first poll |
encore-settings.worker-drain-queue |
false |
Keep polling until queue is empty before exiting (encore-worker) |
Note
Every instance sharing a Redis should use the same concurrency value, because it defines the set of queues that exist. If you need to change it, existing jobs are migrated to the new queue layout on the next startup.
Segmented transcoding¶
For large files, Encore can split the input into segments and transcode them in parallel across multiple workers.
To enable segmented transcoding:
- Set
encore-settings.shared-work-dirto a directory accessible by all Encore instances (e.g., NFS mount) - Submit a job with
segmentLengthset (in seconds, should be a multiple of the target GOP)
| Property | Default | Description |
|---|---|---|
encore-settings.shared-work-dir |
(none) | Shared directory for segment files. Required for segmented transcoding. |
encore-settings.segmented-encode-timeout |
120m |
Timeout for a segmented transcode before failing |
Warning
Thumbnails and VMAF quality metrics are not supported in segmented transcoding mode.
Security¶
Encore supports optional HTTP basic authentication. When enabled, users are configured as a named map — there are no fixed account names.
| Property | Default | Description |
|---|---|---|
encore-settings.security.enabled |
false |
Enable authentication |
encore-settings.security.users.<name>.password |
(required) | Password — plain text or Spring Security encoder prefix (e.g. {bcrypt}$2a$10$...) |
encore-settings.security.users.<name>.role |
USER |
USER or ADMIN |
Roles:
- USER — read jobs, access Swagger UI
- ADMIN — create and cancel jobs, plus everything USER can do (
ADMINimpliesUSER)
Swagger UI (/swagger-ui/**) is always accessible without authentication even when security is enabled — authentication is required only when making API calls from the UI.
Actuator endpoints are always accessible without authentication (see Health and Actuator endpoints).
encore-settings:
security:
enabled: true
users:
ops-user:
password: "${OPS_PASSWORD}"
role: USER
ci-admin:
password: "${CI_ADMIN_PASSWORD}"
role: ADMIN
Health and Actuator endpoints¶
Spring Boot Actuator exposes health and management endpoints, reachable without authentication even when security is enabled:
| Endpoint | Purpose |
|---|---|
/actuator/health |
Overall health status |
/actuator/health/liveness |
Kubernetes liveness probe |
/actuator/health/readiness |
Kubernetes readiness probe |
Detailed health info (status field breakdown per component) is only shown to authenticated admin users; anonymous callers get the top-level status only.
Local temporary transcoding¶
By default, Encore writes output files directly to the outputFolder. If output storage is slow or unstable (e.g., network-mounted storage), you can enable local temporary transcoding:
When enabled, Encore transcodes to a local temporary directory first and copies the finished files to the output folder afterwards. This avoids FFmpeg stalling on slow writes during transcoding.
Scaling with KEDA¶
While Encore is platform-agnostic, it works well with KEDA (Kubernetes Event-Driven Autoscaling) for autoscaling workers based on queue depth.
The following example creates a KEDA ScaledJob that monitors the Redis queue and spins up worker pods as jobs arrive:
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: encore-worker-high-prio
spec:
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
jobTargetRef:
ttlSecondsAfterFinished: 3600
activeDeadlineSeconds: 864000
backoffLimit: 0
parallelism: 1
template:
metadata:
labels:
app: encore-worker-high-prio
app.kubernetes.io/name: encore-worker
app.kubernetes.io/instance: high-prio-job
spec:
topologySpreadConstraints:
- labelSelector:
matchLabels:
app.kubernetes.io/name: encore-worker
maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
containers:
- image: your-encore-worker-image:latest
name: encore-worker
env:
- name: SPRING_APPLICATION_NAME
value: encore
- name: ENCORE_SETTINGS_REDIS_URI
value: redis://redis:6379
- name: ENCORE_SETTINGS_PROFILE_LOCATION
value: file:/profiles/profiles.yml
- name: ENCORE_SETTINGS_POLL_QUEUE
value: "0"
- name: ENCORE_SETTINGS_POLL_HIGHER_PRIO
value: "false"
- name: ENCORE_TMPDIR
value: /scratch
resources:
requests:
cpu: 10
memory: 10Gi
limits:
memory: 40Gi
volumeMounts:
- name: storage
mountPath: /storage
- name: scratch-volume
mountPath: /scratch
volumes:
- name: storage
persistentVolumeClaim:
claimName: encore-storage
- name: scratch-volume
ephemeral:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 40Gi
restartPolicy: Never
maxReplicaCount: 50
pollingInterval: 10
scalingStrategy:
strategy: accurate
rollout:
strategy: gradual
triggers:
- type: redis
metadata:
address: redis:6379
listName: encore:queue:0
listLength: "1"
activationListLength: "0"
Key points:
ENCORE_SETTINGS_POLL_QUEUEpins the worker to a specific queue (here queue0, the highest priority). Create multipleScaledJobresources for different priority queues.ENCORE_TMPDIRpoints to the scratch volume for temporary transcoding files.listNamematches the Redis key pattern{prefix}:queue:{queueNumber}. The prefix defaults to thespring.application.name.maxReplicaCountlimits the number of concurrent worker pods.- Ephemeral volumes provide fast local scratch storage for each worker pod.
- KEDA monitors the Redis sorted set and scales workers up when jobs are queued, and scales to zero when idle.
Logging¶
Encore uses JSON-structured logging via Logback. Logs are formatted for integration with log aggregation tools (ELK, Loki, etc.).
Each job can include a logContext map of key-value pairs that are added to the MDC (Mapped Diagnostic Context) for all log entries related to that job, making it easy to filter and correlate logs.
Configuration¶
Encore is configured via standard Spring Boot mechanisms — application.yml, environment variables, or command-line arguments. See the Configuration Reference for all available properties.
Shared configuration with per-role overrides¶
The recommended way to configure a deployment is to share a single application.yml across encore-web and every encore-worker, and override only the settings that differ per role using environment variables. Keeping the bulk of the configuration identical across instances avoids drift between the API and the workers, and means the Redis URI, profile location, concurrency, encoding defaults and security settings only need to be changed in one place.
A typical split looks like this:
Shared application.yml — mounted into every instance:
encore-settings:
concurrency: 2
encoding:
# …shared encoding defaults
redis:
uri: redis://redis:6379
profile:
location: file:/profiles/profiles.yml
encore-web — API only, no polling:
High-priority worker:
Low-priority worker:
All properties support Spring Boot's relaxed binding, so any YAML property can be overridden with an equivalently-named environment variable.