Install and startup
The stack came up cleanly on the first docker compose up -d, or one container
is unhappy and the rest wait on it. The db starts, api runs migrations
against it, and web proxies to api. A failure anywhere upstream leaves the
containers below it stuck, so read from the bottom of the chain up: start with
docker compose logs db, then api.
The api container restart-loops on a config error
On startup the api validates its environment and exits if anything required is missing or malformed. That is deliberate: a half-configured instance fails loudly at boot rather than misbehaving later. The log names the offending value.
Invalid environment:
JWT_SECRET: String must contain at least 16 character(s)
Two secrets are required and must each be at least 16 characters:
JWT_SECRET and TENANT_CREDS_ENCRYPTION_KEY. The setup builder
generates both, so a blank or truncated value usually means the .env was
hand-edited or a copy-paste dropped characters. Regenerate the file, or set a
strong value yourself:
openssl rand -hex 32
Before you replace TENANT_CREDS_ENCRYPTION_KEY, read the warning in
Data and backups: once it has encrypted
stored credentials, changing it makes those unreadable.
A migration failed on boot
Database migrations run automatically when the api container starts, before it
serves any request. Each migration runs in its own transaction, so a failure
rolls itself back and the api exits rather than leaving the schema half-applied.
The container then restart-loops, and docker compose logs api shows which
migration file failed and the SQL error under it.
On a stock install this should not happen; migrations are tested against the
schema they ship with. If you see it, the usual causes are a hand-modified
database, an image and a data directory from mismatched versions, or a disk that
filled mid-migration. Check df -h for space, confirm you pulled the current
images (docker compose pull), and read the SQL error in the log. Since your
data is in ./data/db, you can restore a pre-update copy and try again
(see Data and backups).
Port already in use
If docker compose up reports a bind error like address already in use, some
other process holds the port the web container wants (8088 by default).
Error: ... failed to bind host port 0.0.0.0:8088 ... address already in use
Either stop whatever holds it, or move Cobblr to a free port by setting
WEB_PORT in .env and bringing the stack back up:
WEB_PORT=8090
docker compose up -d
Then open the box at the new port. See what else is listening with
sudo lsof -i :8088 (or ss -ltnp | grep 8088).
Image pull is denied or unauthorized
The images are public, so a pull needs no login and no account:
Error response from daemon: ... denied / unauthorized
That message almost always means the image reference is wrong rather than that
you lack access. Check the image lines in your docker-compose.yml against the
one the setup builder generated, and check COBBLR_VERSION in
your .env is a tag that exists (stable, nightly, or a release like
2026.8.0); a typo there reads to the registry as an image you cannot see.
If the reference is right, the box may be behind a proxy or a firewall that
blocks ghcr.io. Test the path on its own:
docker pull ghcr.io/cobblrhq/cobblr-api:stable
One case where a login is genuinely required: if you were given access to a
private pre-release lane, docker login ghcr.io with your GitHub username and a
personal access token carrying the read:packages scope.
Nothing is obviously wrong, but the page won't load
Check the order of the chain. docker compose ps should show db healthy,
api up, and web up. If web is up but the browser shows nothing, the api
may still be finishing migrations on a first run; give it a moment and re-run the
health check. If you run HTTPS, the caddy
container also has to be up and to have a certificate; that path is covered in
HTTPS and access.