Deployment
Docker image runtime semantics
The current Docker image uses:
ENTRYPOINT ["vylux"]
CMD ["--mode=all"]
That means:
- the image defaults to
all --mode=allis only a default argument, not a lock-in- Docker, Compose, and Kubernetes can all override
argsto run--mode=serveror--mode=worker
This keeps the smallest deployment shape convenient while still supporting split roles later.
Local development
The most common local shape is:
- PostgreSQL via Docker
- Redis via Docker
- RustFS or another S3-compatible storage service
- Vylux running either on the host or as the image in
allmode
The repository exposes two different local workflows:
docker-compose.dev.yml: infrastructure only, with Vylux running on the hostdocker-compose.yml: Vylux, PostgreSQL, Redis, and optional Cloudflare Tunnel all running in containers
For example:
docker compose -f docker-compose.dev.yml up -d
go run ./cmd/vylux
For the complete environment-variable reference, default values, and validation rules behind these deployment examples, see Configuration.
Runtime modes
MODE=all: run the HTTP server and worker in one processMODE=server: run only the HTTP server and delivery endpointsMODE=worker: run only the queue worker
CLI flags override the environment variable:
./bin/vylux --mode=server
./bin/vylux --mode=worker
Single-process mode
This is the simplest production shape when scale is small or operational simplicity matters more than isolation.
docker run --rm \
--env-file .env \
-p 3000:3000 \
-v vylux-scratch:/var/cache/vylux \
ghcr.io/carry0987/vylux:latest
This mode is reasonable for local development, staging, or small deployments.
The image sets TMPDIR=/var/cache/vylux and declares /var/cache/vylux as a Docker volume. If you do not mount anything explicitly, Docker will create an anonymous volume. In production, prefer an explicit named volume or platform-managed disk so scratch usage remains visible and easier to manage.
Docker Compose deployment
The repository docker-compose.yml uses a single vylux service in the default all mode and also starts:
- PostgreSQL
- Redis
- an optional Cloudflare Tunnel sidecar
Operational notes:
- the container health check uses
GET /healthz /var/cache/vyluxis mounted as the dedicated scratch volume- the image also sets
TMPDIR=/var/cache/vylux, so tool-level temp files land on the same workspace - there is no separate key tmpfs mount because raw encryption keys are passed directly to Shaka Packager and are not staged as files on disk
- a
ports:mapping is only required if you want direct host access such ashttp://localhost:3000orhttp://localhost:3100
Minimal startup:
docker compose up -d --build
ports: is optionalIf all external traffic goes through Cloudflare Tunnel, the vylux service does not need a host ports: mapping. Keep ports: only when you also want direct host-side access for local browser or curl testing.
Container-network semantics
Inside docker-compose.yml, each container gets its own localhost. That means:
localhostinside thevyluxcontainer points back to thevyluxcontainer itselflocalhostinside thetunnelcontainer points back tocloudflared, not tovylux- cross-container traffic should use compose service names such as
postgres,redis, andvylux
If you run Vylux itself inside compose, do not reuse host-only examples such as SOURCE_S3_ENDPOINT=http://localhost:9002 unless the storage endpoint is actually reachable from inside that container.
Cloudflare Tunnel sidecar
localhost is the wrong tunnel origin in DockerWhen cloudflared runs in Docker, http://localhost:3100 points back to the tunnel container itself, not to the vylux service.
The tunnel sidecar shares the compose network with vylux, so the origin service should be configured with the compose service name, for example http://vylux:3100.
Do not point the tunnel origin at http://localhost:3100 when cloudflared runs in Docker. In that case localhost resolves inside the tunnel container and typically produces 502 errors with logs like dial tcp [::1]:3100: connect: connection refused.
Split mode
For larger environments, run the HTTP server and queue worker separately.
Why split
- isolated failure domains
- easier horizontal scaling
- worker-specific metrics and resource tuning
Docker example
docker run -d \
--name vylux-server \
--env-file .env \
-p 3000:3000 \
ghcr.io/carry0987/vylux:latest \
--mode=server
docker run -d \
--name vylux-worker \
--env-file .env \
-p 3001:3001 \
-v vylux-scratch:/var/cache/vylux \
ghcr.io/carry0987/vylux:latest \
--mode=worker
If you expect large transcodes, also tune:
WORKER_CONCURRENCYfor the normal poolLARGE_WORKER_CONCURRENCYfor the dedicatedvideo:largepool- the size and performance of the
/var/cache/vyluxscratch volume
Kubernetes guidance
On Kubernetes, the recommended baseline is two Deployments using the same image:
vylux-servervylux-worker
Both should share the same PostgreSQL, Redis, buckets, and secret material. In practice that means at least the same:
- image
- PostgreSQL and Redis connectivity
- S3 credentials and bucket names
API_KEY,WEBHOOK_SECRET,KEY_TOKEN_SECRET, andENCRYPTION_KEY
The Pod spec only needs different args:
containers:
- name: vylux-server
image: ghcr.io/carry0987/vylux:latest
args: ["--mode=server"]
containers:
- name: vylux-worker
image: ghcr.io/carry0987/vylux:latest
args: ["--mode=worker"]
Probe guidance
Server:
- liveness:
GET /healthz - readiness:
GET /readyz - metrics:
GET /metrics
Worker:
- liveness:
GET :WORKER_METRICS_PORT/healthz - metrics:
GET :WORKER_METRICS_PORT/metrics
If you schedule large transcodes on Kubernetes, each worker Pod should have enough local scratch capacity for /var/cache/vylux. The video:large queue is intentionally separated so you can keep normal job throughput stable while limiting concurrent large transcodes.
Worker-only mode does not expose /readyz, so startup and readiness handling should be conservative at the platform layer.
Bucket access model
- source store: read only access to
SOURCE_BUCKETviaSOURCE_S3_* - media store: read/write access to
MEDIA_BUCKETviaMEDIA_S3_*
This keeps the privilege boundary clear and allows server and worker to share the same storage plane without local persistent volumes.
Deployment checklist
- media tools installed and reachable in PATH or env config
- server and worker receive the same
SOURCE_S3_*,MEDIA_S3_*, bucket names, and secrets - Redis and PostgreSQL reachable from both processes
- health and metrics endpoints exposed to your platform
Also verify:
BASE_URLpoints to the public media hostname and does not end with/- server and worker receive the same secret material
- tracing and metrics are wired into your platform before load testing
Anti-patterns
- keeping
allmode in Kubernetes while trying to scale API and worker independently - exposing
API_KEY,HMAC_SECRET, orKEY_TOKEN_SECRETto public clients - giving source and media buckets indistinguishable write permissions when they should represent different trust boundaries