Getting Started
Prerequisites
- Go 1.27+
- Docker and Docker Compose
curl- S3-compatible storage for source and derived assets such as RustFS, R2, or S3
If you use the repository Docker image directly, the runtime already contains ffmpeg, vips, vips-heif, and packager. If you run Vylux on the host with go run, install FFmpeg, libvips, pkg-config, and Shaka Packager locally.
If you want HEIF-family inputs such as heic, heif, or avif, or you want AVIF output while running on the host, make sure your local libvips installation includes HEIF support as well. In practice, that usually means having libheif or a distro-specific HEIF plugin package available to libvips.
On macOS with Homebrew, brew install vips libheif pkg-config is a safe baseline for the image pipeline toolchain.
A quick local verification is:
vips -l | grep -i heif
Look for loader and saver entries such as heifload, heifload_buffer, and heifsave. If they are missing, host-run Vylux will usually fail to decode heic / heif / avif inputs or fail to produce AVIF output even though libvips itself is installed.
This check only confirms that local libvips can handle HEIF-family static image decode / encode. It does not imply support for animated AVIF or animated HEIF sequences, which Vylux does not currently process as animated inputs on the synchronous /img path.
Recommended local development flow
Host-run Vylux
Use docker-compose.dev.yml for infrastructure only, then run Vylux on the host with go run ./cmd/vylux.
In this workflow, localhost inside DATABASE_URL, REDIS_URL, SOURCE_S3_ENDPOINT, and MEDIA_S3_ENDPOINT refers to your Mac, so host-mapped ports such as localhost:5434 and localhost:9002 are correct.
Compose-run Vylux
Use the repository docker-compose.yml when Vylux itself should also run inside Docker.
That file pulls the published ghcr.io/carry0987/vylux:latest image, which is built automatically by GitHub Actions and published as a native multi-platform image for linux/amd64 and linux/arm64.
In that workflow, localhost inside Vylux no longer points to your Mac. Use container-reachable addresses such as compose service names or an external endpoint instead.
1. Start the infrastructure services
docker compose -f docker-compose.dev.yml up -d
This starts:
- PostgreSQL on
localhost:5434 - Redis on
localhost:6381 - RustFS S3 API on
localhost:9002 - RustFS Console on
localhost:9003
2. Prepare environment variables
cp .env.example .env.local
For the complete environment-variable reference, validation rules, and secret guidance, see Configuration.
If you use docker-compose.dev.yml, at minimum point the following settings to localhost:
DATABASE_URL=postgres://myuser:mypassword@localhost:5434/mydb
REDIS_URL=redis://localhost:6381
SOURCE_S3_ENDPOINT=http://localhost:9002
SOURCE_S3_REGION=auto
SOURCE_BUCKET=app-bucket
MEDIA_S3_ENDPOINT=http://localhost:9002
MEDIA_S3_REGION=auto
MEDIA_BUCKET=media-bucket
BASE_URL=http://localhost:3000
Those localhost values are correct only when the Vylux process itself runs on the host. If Vylux runs in a container, localhost refers to that container.
If you copy SOURCE_S3_ENDPOINT=http://localhost:9002 or REDIS_URL=redis://localhost:6381 into a containerized Vylux deployment, those addresses will resolve inside the container itself and usually fail.
The most important required settings are:
DATABASE_URLREDIS_URLSOURCE_S3_ENDPOINTSOURCE_S3_ACCESS_KEYSOURCE_S3_SECRET_KEYSOURCE_BUCKETMEDIA_S3_ENDPOINTMEDIA_S3_ACCESS_KEYMEDIA_S3_SECRET_KEYMEDIA_BUCKETHMAC_SECRETIMAGE_SOURCE_KEY(optional; enables encrypted source URLs)WEBHOOK_SECRETAPI_KEYKEY_TOKEN_SECRETENCRYPTION_KEYFFMPEG_PATHSHAKA_PACKAGER_PATH
Generate the common secrets with openssl:
cat >> .env.local <<EOF
HMAC_SECRET=$(openssl rand -hex 32)
IMAGE_SOURCE_KEY=$(openssl rand -hex 32)
API_KEY=$(openssl rand -hex 32)
WEBHOOK_SECRET=$(openssl rand -hex 32)
KEY_TOKEN_SECRET=$(openssl rand -hex 16)
ENCRYPTION_KEY=$(openssl rand -hex 32)
EOF
3. Create the storage buckets and upload sample source objects
Vylux does not create SOURCE_BUCKET or MEDIA_BUCKET for you. At minimum you need:
- a source bucket for original objects, configured by
SOURCE_BUCKETandSOURCE_S3_* - a media bucket for image cache entries, thumbnails, previews, covers, and HLS output, configured by
MEDIA_BUCKETandMEDIA_S3_*
If both roles use the same local RustFS instance or the same S3 provider, still set both storage groups explicitly. Vylux does not infer media settings from source settings.
For S3-compatible storage, Vylux writes derived objects with CRC32C upload checksums. That works with AWS S3, Cloudflare R2, and RustFS in the supported setup here; validate checksum-header support first if you swap in another S3-compatible service.
Upload at least one test asset such as:
- image:
uploads/sample.jpg - video:
uploads/sample.mp4
4. Start Vylux
go run ./cmd/vylux
Or split roles into two processes:
go run ./cmd/vylux --mode=server
go run ./cmd/vylux --mode=worker
If startup fails with a linker error such as library 'vips' not found, the most common causes are:
- libvips is not installed on the host
pkg-configcannot resolve the current libvips installation- Go's build cache still contains stale cgo linker flags from an older Homebrew Cellar path
This last case is easy to miss after a local brew upgrade of vips or libvips. The Go toolchain may keep reusing cached linker flags that still point at the previous Homebrew Cellar directory, even though the package is already installed correctly.
Typical symptoms look like this:
go build ./cmd/vylux
...
ld: warning: search path '/opt/homebrew/Cellar/vips/8.18.6/lib' not found
ld: library 'vips' not found
clang: error: linker command failed with exit code 1
On macOS with Homebrew, the fastest recovery path is usually:
brew install vips pkg-config
go clean -cache
go run ./cmd/vylux
If brew install reports that both packages are already installed, rerun go clean -cache anyway after a Homebrew upgrade. That forces cgo to rebuild with the current pkg-config output instead of reusing stale linker paths.
In practice, if you recently upgraded Homebrew packages and see a build or make build failure mentioning a missing Cellar/vips/<version>/lib path, treat go clean -cache as the first recovery step before debugging anything deeper.
5. Validate service health
Check probe endpoints first, then exercise the Jobs API, and only then inspect the returned media URLs. That order separates process problems, dependency problems, and integration problems much faster.
Server / all mode
curl -i http://localhost:3000/healthz
curl -i http://localhost:3000/readyz
curl -s http://localhost:3000/metrics | rg '^vylux_'
Expected success bodies:
{"status":"ok"}
Worker-only mode
curl -i http://localhost:3001/healthz
curl -s http://localhost:3001/metrics | rg '^vylux_'
If GET /readyz fails, the response is structured JSON with a summary error plus individual dependency checks. That makes it easier to tell whether PostgreSQL, Redis, the source bucket, or the media bucket is actually blocking startup.
Minimal validation order
The smallest useful API validation flow is:
- Create a preview job
BASE_URL='http://localhost:3000'
API_KEY='replace-with-api-key'
curl -s \
-X POST "$BASE_URL/api/video/jobs" \
-H 'Content-Type: application/json' \
-H "X-API-Key: $API_KEY" \
-d '{
"source": {
"hash": "quickstart-preview-sample",
"key": "uploads/sample.mp4"
},
"pipeline": {
"preview": {
"enabled": true,
"start_sec": 1,
"duration": 3,
"width": 480,
"fps": 12,
"format": "webp"
}
}
}'
- Poll the job until it becomes
completedorfailed
curl -s \
-H "X-API-Key: $API_KEY" \
http://localhost:3000/api/jobs/<job-id>
- Validate the derived asset from
results.keyorresults.streaming.master_playlist
At this point, do not stop at the storage key alone:
- if the job returned an image-like media-bucket key such as a cover, preview, or thumbnail, convert it into a signed
/thumb/{sig}/{encoded_key}URL before exposing it to a browser - if the job returned streaming results, use
/stream/{hash}/master.m3u8as the public playback entrypoint rather than the rawmaster_playliststorage key - if encrypted playback is enabled, mint a Bearer token for the returned key endpoint, typically
/api/key/{id}, and attach it only on key requests
For the full mapping from job results to public URLs, see Integration Guide.
If a job result contains a media-bucket key, that usually is not the final public URL. Convert it into a signed /thumb URL or a /stream/{hash} playback entrypoint before handing it to a browser.
Before release, cover at least these three smoke-test groups:
video:previewwithgifvideo:previewwithwebpvideo:transcodewithencrypt=true
What you should observe after startup
GET /healthzreturns200with{"status":"ok"}GET /readyzreturns200with{"status":"ok"}when dependencies are ready- if
GET /readyzreturns503, the JSON body identifies which readiness checks failed GET /metricsexposes Prometheus metricsPOST /api/audio/jobsandPOST /api/video/jobscan enqueue asynchronous media work
Next, most teams continue with Integration Guide, Configuration, Jobs API, and Observability.