Repository navigation
Docker
The Docker image is the recommended way to run Obelo. It is a multi-stage build
that compiles the React/Vite SPA, embeds it into the Go binary, and runs the
result on a slim Debian image carrying ffmpeg and the GPU userspace for every
hardware backend Obelo supports on Linux.
The image is the release build, so the embedded Tailscale node is compiled
in (Remote Access with Tailscale). Nothing about
the container changes to support it: no extra capabilities, no /dev/net/tun,
no NET_ADMIN, and it still runs as the unprivileged obelo user, because the
node runs in userspace inside the server process.
The build context is the repository root, not the docker/ directory:
docker build -f docker/Dockerfile -t obelo .Building on an Apple Silicon / arm64 host still produces an amd64 image — the Go
binary cross-compiles (pure Go, no CGO) and the runtime stage is pinned to
linux/amd64. If your Docker cannot run amd64 images locally:
docker buildx build --platform linux/amd64 -f docker/Dockerfile -t obelo . --loaddocker run -d --name obelo -p 8080:8080 \
-v "$PWD/data:/data" \
-v /path/to/your/media:/media:ro \
obeloWhat the image already sets for you:
OBELO_DATA_DIR |
/data |
OBELO_LISTEN_ADDR |
:8080 |
| Exposed port | 8080 |
| Declared volume | /data |
| Runtime user |
obelo (unprivileged) |
Pass anything else with -e — see Configuration for the
full list.
-
/datais the only writable directory Obelo needs, and it holds everything you would miss: the SQLite database, artwork and subtitle caches, transcode scratch, and TLS material. Mount a real volume. A container without one loses your library on everydocker run --rm, and inacmemode re-issues certificates until the CA rate-limits you. -
Mount media read-only (
:ro). Obelo never writes to your media folders, and mounting them read-only means a bug never can either. - Media can be mounted anywhere. You point libraries at these container
paths from the web UI, so
/media/moviesis what you type — not the host path.
There is no compose file in the repository; here is a complete one to copy. It builds from a checkout in the same directory:
services:
obelo:
build:
context: .
dockerfile: docker/Dockerfile
image: obelo
container_name: obelo
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/data
- /srv/media/movies:/media/movies:ro
- /srv/media/tv:/media/tv:ro
- /srv/media/music:/media/music:ro
environment:
OBELO_SERVER_NAME: "Living Room"
OBELO_SCAN_INTERVAL: "1h"
OBELO_MAX_CONCURRENT_TRANSCODES: "3"
# Enrichment is off until you configure it. First boot only —
# afterwards the admin settings UI is authoritative.
# OBELO_TMDB_API_KEY: "…"
# OBELO_MUSICBRAINZ_ENABLED: "true"Bring it up and read the log for the first-admin claim token:
docker compose up -d
docker compose logs -f obeloservices:
obelo:
# …as above…
environment:
OBELO_HARDWARE_ACCEL: "nvenc"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]services:
obelo:
# …as above…
environment:
OBELO_HARDWARE_ACCEL: "vaapi"
devices:
- /dev/dri:/dev/dri
group_add:
- "993" # the HOST's render GID — check with: getent group renderThe group_add value is host-specific and there is no portable way to write it.
Look it up with getent group render | cut -d: -f3. Getting this wrong is the
single most common cause of "the GPU works as root but not normally" — see
Hardware Transcoding.
-p 8080:8080 is right for almost everything. The web app, the API, and native
clients all work over it. The one thing it cannot do is LAN discovery.
mDNS (Bonjour, _obelo._tcp) is link-local multicast and does not cross a
Docker bridge. A published port gives you a perfectly reachable server that no
client will ever discover:
docker run -d --name obelo --network host \
-e OBELO_LISTEN_ADDR=:8088 \
-v "$PWD/data:/data" \
-v /path/to/your/media:/media:ro \
obeloManual address entry still works without it — discovery is a convenience, never the only path. The full story, including how to read the boot log line that tells you what was advertised, is on LAN Discovery.
In compose, that is network_mode: host (and you then drop the ports: block,
which host networking ignores).
Obelo says what it decided at startup, and those lines are the fastest diagnosis you will get. Three worth knowing:
obelo: no users yet — first-Admin claim token: <token>
obelo: advertising "Living Room" as nuc.local. on _obelo._tcp port 8088 at 192.168.1.50 (id: …)
obelo: hardware acceleration: using configured hardware backend nvenc (h264_nvenc validated)
docker logs obelo
docker logs -f obelo # followThe claim token appears only while the database has no users. If you missed it and have not completed setup, restart the container and it is printed again.
git pull
docker build -f docker/Dockerfile -t obelo .
docker stop obelo && docker rm obelo
docker run -d --name obelo … # same flags, same -v "$PWD/data:/data"Or with compose: docker compose build && docker compose up -d.
Your database, settings, users and watch history live in /data and survive
this — that is the entire reason the volume matters. Schema migrations run
automatically at startup.
Back up data/obelo.db before a major update. The caches (artwork/,
subtitles/) rebuild themselves and are not worth backing up.
-
Transcodes are the expensive thing. Direct play and remux are cheap;
full transcodes can peg a box.
OBELO_MAX_CONCURRENT_TRANSCODES(default3) bounds only that tier. Raise it if you have cores to spare, lower it if the server does other work. -
/datagrows. Artwork and subtitle caches are bounded by library size; transcode scratch is transient but can be large mid-session. - The admin Transcoding tab shows the resolved backend and live load, plus GPU telemetry on NVENC. It is the right place to look before guessing.
Getting started
Reference
Going further
Plugins
Sharing with another household
Reaching it from away
When it breaks