Docker Setup¶
Everything specific to running fetchly as a container.
Image layout¶
The image is built in stages so the runtime layer carries only what the app needs:
| Stage | Produces |
|---|---|
ffmpeg | A static ffmpeg/ffprobe build |
essentia | Essentia, compiled from source on every architecture from one upstream commit |
builder | The Python virtualenv and the vendored wavesurfer.js bundle |
runtime | The final image |
Application code is copied root-owned and read-only. The container starts as root so the entrypoint can fix ownership of a mounted /app/data, then drops to the unprivileged appuser (UID 1000) via gosu before exec'ing Gunicorn. A compromised application process cannot rewrite its own Python modules, templates, or JavaScript — only /app/data is writable.
Volume layout¶
Mount one volume at /app/data. Everything that survives a container recreation lives under it:
/app/data/
├── jobs.db # SQLite database (WAL mode): jobs, settings, share links
├── <job-uuid>/ # One directory per job: media, thumbnail, trims, stems
├── cookies/ # Imported per-platform cookie jars
├── logo/ # Uploaded watermark logo, if any
├── thumb-cache/ # Cached remote thumbnails
├── watermark-cache/ # Rendered video watermark badges
├── update_check.json # Cached upstream release check (24 h)
└── .cache/torch/ # beat_this model checkpoint (~81 MB)
The data volume is sensitive
cookies/ holds live, signed-in browser sessions for your own platform accounts, and jobs.db holds your Lalal.ai key and the admin password hash. Treat the volume with the same care as a credentials store: restrictive permissions, and encrypted backups.
Container environment¶
The image and its entrypoint apply these defaults; override them only when you have a reason:
| Variable | Default | Notes |
|---|---|---|
DATA_DIR | /app/data | Use an absolute path |
HOST | 0.0.0.0 | Bind address inside the container |
PORT | 8000 | Also probed by the health check |
WORKERS | 1 | Must remain 1 — see below |
TIMEOUT | 60 | Gunicorn worker timeout |
GRACEFUL_TIMEOUT | 15 | Shutdown budget before SIGKILL |
ACCESS_LOG_FORMAT | [%(t)s] %(h)s "%(r)s" %(s)s %(b)s | Gunicorn access-log format |
APP_USER | appuser | Unprivileged account the entrypoint drops to |
TORCH_HOME | ${DATA_DIR}/.cache/torch | Keeps the model checkpoint on the volume |
UVICORN_HOST, UVICORN_PORT, and UVICORN_WORKERS are accepted as fallbacks for HOST, PORT, and WORKERS.
The full list of application variables is in Environment Variables. Download workers, timeouts, input size, and the BPM and Lalal.ai limits are not environment variables — they are set in the UI, see Application Settings.
WORKERS must stay at 1
The job queue and the SSE subscriber registry live in process memory with no cross-process coordination. A second Gunicorn worker means the same job processed twice, and clients subscribed to a process that never sees their job's events. This is a correctness failure, not a throughput trade-off — the entrypoint enforces it. CPU parallelism comes from the Governor's worker threads and semaphores instead; see Resources & Workers.
Health check¶
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD python -c "... urlopen('http://127.0.0.1:'+PORT+'/health') ..." || exit 1
GET /health is unauthenticated by design so orchestrators can probe it, and returns only {"status":"ok"}.
Hardening¶
services:
fetchly:
image: giiibates/fetchly:latest
restart: always
stop_grace_period: 20s
security_opt:
- no-new-privileges:true
ports:
- "127.0.0.1:8000:8000" # bind to loopback; publish via a reverse proxy
environment:
FETCHLY_SECRET_KEY: "${FETCHLY_SECRET_KEY:?required}"
FETCHLY_BEHIND_HTTPS: "1"
FORWARDED_ALLOW_IPS: "172.18.0.0/16"
TZ: "${TZ:-Etc/UTC}"
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
volumes:
- ./data:/app/data
Read-only root filesystem
read_only: true works if you also mount tmpfs at /tmp — ffmpeg and the cookie importer write temporary files there.
stop_grace_period must stay above GRACEFUL_TIMEOUT so the SQLite WAL checkpoint on shutdown actually completes before Docker kills the container.
Logs¶
Set LOG_LEVEL to debug, info, warning, or error. The startup banner prints the resolved version, data directory, worker count, and detected tool versions — useful as a first stop when something behaves unexpectedly.
Timezone¶
TZ controls the timestamps rendered in the UI and written to the logs. Job records themselves are stored in UTC.
Backups¶
Everything is in the volume. Stop the container so the database is checkpointed, then copy the directory:
Restore by putting the directory back and starting the container; the schema migrates forward automatically.