Skip to main content

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.

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.

Reusing host-only values in containers

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_URL
  • REDIS_URL
  • SOURCE_S3_ENDPOINT
  • SOURCE_S3_ACCESS_KEY
  • SOURCE_S3_SECRET_KEY
  • SOURCE_BUCKET
  • MEDIA_S3_ENDPOINT
  • MEDIA_S3_ACCESS_KEY
  • MEDIA_S3_SECRET_KEY
  • MEDIA_BUCKET
  • HMAC_SECRET
  • IMAGE_SOURCE_KEY (optional; enables encrypted source URLs)
  • WEBHOOK_SECRET
  • API_KEY
  • KEY_TOKEN_SECRET
  • ENCRYPTION_KEY
  • FFMPEG_PATH
  • SHAKA_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_BUCKET and SOURCE_S3_*
  • a media bucket for image cache entries, thumbnails, previews, covers, and HLS output, configured by MEDIA_BUCKET and MEDIA_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-config cannot 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​

Validate in layers

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:

  1. 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"
}
}
}'
  1. Poll the job until it becomes completed or failed
curl -s \
-H "X-API-Key: $API_KEY" \
http://localhost:3000/api/jobs/<job-id>
  1. Validate the derived asset from results.key or results.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.m3u8 as the public playback entrypoint rather than the raw master_playlist storage 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.

Do not stop at the storage key

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:preview with gif
  • video:preview with webp
  • video:transcode with encrypt=true

What you should observe after startup​

  • GET /healthz returns 200 with {"status":"ok"}
  • GET /readyz returns 200 with {"status":"ok"} when dependencies are ready
  • if GET /readyz returns 503, the JSON body identifies which readiness checks failed
  • GET /metrics exposes Prometheus metrics
  • POST /api/audio/jobs and POST /api/video/jobs can enqueue asynchronous media work

Next, most teams continue with Integration Guide, Configuration, Jobs API, and Observability.