Docker
What is this
The root Dockerfile builds the dlboard distribution into a container
image. It’s a two-stage build — a uv-based builder stage that resolves and compiles the project,
and a slim runtime stage that only ever sees the finished virtual environment, never uv, a
compiler, or the source tree outside what got installed into site-packages.
The image’s default deployment is Postgres metadata + local-disk artifacts, with password
sign-in — composed in
dlboard/scripts/docker_deployment.py:
PLUGINS = [*POSTGRES_STORAGE, *PASSWORD_AUTH, *BUILTIN_BACKEND, *BUILTIN_CHARTS, themes.default]
This is the simplest deployment that’s still a real, multi-person deployment rather than dlboard
serve local’s single-user setup: one Postgres database plus one volume for artifact blobs, nothing
else to run or pay for. The image also ships the s3 extra, so pointing it at an S3-protocol store
instead needs no rebuild — see “Using an S3 artifact store instead” below.
When you’d need this
You’re running dlboard in Docker/Compose/Kubernetes/ECS rather than installing it directly with
pip/uv tool install. If you’re running it as a plain process on a machine you control, see the
root README instead — there’s no reason to containerize just to get
dlboard serve local or serve custom running.
Running it
It needs a reachable Postgres to actually start — that’s not optional, since the default deployment
has no other metadata store to fall back to. Artifact storage defaults to a local directory inside
the container (/data/artifacts), so it only needs a volume, not another service.
To try it with no infrastructure of your own, docker-compose.yml at the
repo root is entirely self-contained apart from one thing it refuses to make up a default for — a
session secret:
export DLBOARD_SECRET_KEY=$(openssl rand -hex 32)
docker compose up --build
docker compose exec dlboard \
dlboard users set-password alice --admin --plugins dlboard.scripts.docker_deployment:PLUGINS
It builds the image and brings up a Postgres container alongside it — nothing else to configure
first. Both of its volumes (dlboard-postgres, dlboard-artifacts) genuinely persist across
docker compose down/up (no -v); that was checked by actually restarting the stack and
confirming a project and an uploaded artifact were both still there afterward, not assumed from
“it’s a volume.” It’s still a local trial, not a deployment you’d run as-is: docker compose down
-v throws both volumes away on purpose.
Pointed at your own Postgres instead, override that same file’s environment (or write your own
docker-compose.yml/Kubernetes manifest): point DLBOARD_POSTGRES_HOST at your real database, set a real
DLBOARD_SECRET_KEY (openssl rand -hex 32), and either keep mounting a volume at /data for
artifacts or switch to S3 (below).
Configuration
Every setting is an environment variable — nothing is passed on the container’s command line.
These are the ones every deployment needs; the full lists (TLS, pooling, timeouts, …) are in
storage.md (PostgresSettings) and auth.md (PASSWORD_AUTH’s
settings table).
| Variable | Required | |
|---|---|---|
DLBOARD_POSTGRES_HOST |
yes | Also _PORT (5432), _DATABASE (dlboard), _USER (dlboard), _PASSWORD. |
DLBOARD_ARTIFACT_STORE_LOCATION |
no | Where artifact blobs land inside the container. Defaults to /data/artifacts — mount a volume at /data to persist them. |
DLBOARD_SECRET_KEY |
yes | Signs session cookies for the password auth plugin this image runs. A missing one is a startup error, not a silent fall-through to anonymous access — generate one with openssl rand -hex 32. |
DLBOARD_ADMIN_USERS |
no | Comma-separated usernames made admin whenever they sign in — the alternative to creating the first admin by hand (below). |
DLBOARD_MAX_UPLOAD_MB |
no | Hard cap on any single request body, mainly artifact uploads. Defaults to 256. |
DLBOARD_TELEMETRY_METRICS |
no | prometheus to serve /metrics, otlp to push to a collector, or both – the image has the otel extra. See telemetry.md. |
Serve it over HTTPS in front of a reverse proxy — session cookies are Secure by default, which
most HTTP clients (browsers included, localhost sometimes excepted) silently refuse to send back
over a plain-HTTP connection, breaking sign-in rather than just weakening it. docker-compose.yml
sets DLBOARD_SECURE_COOKIES=false for exactly this reason, since it’s plain HTTP by design — leave
it at its default (true) everywhere else (see auth.md for rate-limiting
/login too).
Creating the first admin
Without a shell (a NAS or hosting UI that only shows you logs):
- Start the container with
DLBOARD_PASSWORD_FIRST_ADMIN_SETUP=true. - Open
/setupon your server, for examplehttps://dlboard.example.com/setup. Opening it writes a setup token to the container log (First-admin setup is open. Setup token: ...). - Enter that token along with a username and password. You’re signed in as an admin.
- Remove
DLBOARD_PASSWORD_FIRST_ADMIN_SETUP(or set it tofalse). The container won’t start again with it still on, since the database now has users.
/setup is off unless you set that variable, and even then it only works while the database has no users
at all. Once anyone exists it returns 404 for good, even if every admin is later demoted. Create the admin
before you share the address: anyone who can read the container log in that window could do it first. If
DLBOARD_PASSWORD_SIGNUP lets people sign up, the first signup also closes /setup, and you’d use the
command below instead.
From a shell, you can also create the admin directly:
docker run --rm -it --env-file .env dlboard \
users set-password alice --admin --plugins dlboard.scripts.docker_deployment:PLUGINS
--env-file .env needs the same DLBOARD_POSTGRES_* variables as the running container, since this writes
to the same database. After that, admins can add more users and reset passwords from Admin → Users
in the app itself — see auth.md.
Authenticating a training script
Password sign-in means dlboard serve local’s “no key needed” shortcut doesn’t apply here — a
training script is a separate caller from your browser session, and it authenticates the way
client.md describes for any signed-in deployment:
- Sign in at
http://localhost:8050(or wherever you exposed it) with the user you created above. - Create an API token on that user’s Account page.
-
Wherever
DLBoardLoggerruns, setDLBOARD_API_KEYto that token and point it at the server:from dlboard.client.dlboard_logger import DLBoardLogger logger = DLBoardLogger(project_id=project.id, server_url="http://localhost:8050")
If the training script runs in its own container on the same Compose network, use the service name
instead of localhost — server_url="http://dlboard:8050". The logger checks the key against
/api/v1/whoami before training starts, so a missing or wrong DLBOARD_API_KEY fails immediately rather
than silently dropping every metric.
Using an S3 artifact store instead
Local disk is the simplest default, not the only option — the image is already built with the s3
extra, so swapping dlboard.plugins.POSTGRES_STORAGE for POSTGRES_S3_STORAGE (postgres + S3 +
the artifact purge worker) needs no rebuild, just your own small module instead of
docker_deployment.py (dlboard deliberately doesn’t ship a ready-made S3 “deployment” of its own —
see storage.md):
# mydeployment.py
from dlboard.plugins import BUILTIN_BACKEND, BUILTIN_CHARTS, PASSWORD_AUTH, POSTGRES_S3_STORAGE, themes
PLUGINS = [*POSTGRES_S3_STORAGE, *PASSWORD_AUTH, *BUILTIN_BACKEND, *BUILTIN_CHARTS, themes.default]
Mount it into the container and point --plugins at it instead:
docker run -v ./mydeployment.py:/app/mydeployment.py:ro -e PYTHONPATH=/app \
-e DLBOARD_POSTGRES_HOST=... -e DLBOARD_S3_BUCKET=... -e DLBOARD_SECRET_KEY=... \
dlboard serve custom --plugins mydeployment:PLUGINS --host 0.0.0.0
DLBOARD_S3_BUCKET is required once you do this; DLBOARD_S3_ENDPOINT_URL/DLBOARD_S3_ADDRESSING_STYLE=path instead of
AWS for a self-hosted S3-protocol store (MinIO, VAST, …). See storage.md for
the full S3Settings list.
The docker-compose.yml at the repo root deliberately doesn’t do this: a throwaway S3-protocol
emulator worth bundling for a zero-setup local trial turned out not to exist for free — LocalStack’s
free tier doesn’t actually persist state across a restart (checked, not assumed) and MinIO’s own
images now require a Docker Hub login (see dlboard/conftest.py’s s3_test_server). Local disk
sidesteps both and is just as real a storage choice for a small deployment.
Running a different plugin list
More generally, the image’s ENTRYPOINT/CMD just runs dlboard serve custom --plugins
dlboard.scripts.docker_deployment:PLUGINS --host 0.0.0.0 — override either to run your own
PLUGINS module (a different auth provider, a different theme, S3 as above) the same way:
FROM dlboard
COPY mydeployment.py /app/mydeployment.py
ENV PYTHONPATH=/app
CMD ["serve", "custom", "--plugins", "mydeployment:PLUGINS", "--host", "0.0.0.0"]
See plugins/overview.md for what a PLUGINS module needs, and
storage.md/auth.md for the built-in plugins it can compose.
CI and releases
The image is published to ghcr.io/dlangerm/dlboard (amd64 and arm64) under tags that trade
stability for freshness:
| Tag | Moves when | Use it for |
|---|---|---|
X.Y.Z, X.Y |
a vX.Y.Z tag is released (X.Y follows the newest patch) |
pinning an exact release |
stable |
a release is published | production: only ever a release |
beta, latest |
a change lands on main and passes CI |
trying bugfixes before they are released |
sha-<commit> |
never; one per landing on main |
rolling back a beta deployment |
docker pull ghcr.io/dlangerm/dlboard:stable
Because beta and latest track unreleased code, a database migration they carry has not had a
release’s worth of use; see compatibility.md before pointing one at data you care about.
.github/workflows/ci.yml pushes latest/beta/sha-* once its test job passes on main (a PR
only gets the build-only docker-build check). Pushing a vX.Y.Z
tag runs .github/workflows/release.yml, which publishes dlboard-client/dlboard to PyPI and then the
release tags above.
See CHANGELOG.md for what each release contains, and the repo’s GitHub Releases page for the full tag history.