Installation
All you need on the host is Docker with the Compose plugin. Fadebox runs as two containers: the app and a PostgreSQL database.
1. Create the compose file
Save this as docker-compose.yml:
# Fadebox — app + PostgreSQL. Create .env next to this file first (step 2).
#
# Nothing host-specific is baked in here: the Docker socket path and the GID that owns it
# come from .env, because they differ per host (docker group on Linux, root on Docker
# Desktop, your own uid under rootless Docker).
services:
db:
image: postgres:18
environment:
POSTGRES_DB: fadebox
POSTGRES_USER: fadebox
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
# No published port: the app reaches it as db:5432 on the compose network, and a published
# port bypasses the host firewall (ufw, firewalld). For a shell or a dump, go through the
# container: `docker compose exec db psql -U fadebox fadebox`.
volumes:
# PG18 images store data in a version-specific subdir under /var/lib/postgresql
- fadebox-db:/var/lib/postgresql
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U fadebox -d fadebox" ]
interval: 5s
timeout: 5s
retries: 10
app:
image: fadebox/fadebox:1.4
depends_on:
db:
condition: service_healthy
# The image's own uid (185, UBI convention); only the supplementary group varies per host.
user: "${FADEBOX_UID:-185}:${FADEBOX_GID:-0}"
group_add:
# GID that owns the Docker socket — see step 2 for how to find it.
- "${DOCKER_GID:?set DOCKER_GID in .env}"
# On an SELinux-enforcing host (Fedora, RHEL, openSUSE with the daemon's SELinux integration
# on) a confined container cannot open the bind-mounted socket whatever its uid and gid, and
# every deploy fails with "permission denied". A no-op anywhere else.
security_opt:
- label=disable
environment:
# Point the baked-in postgres datasource at the db service.
QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/fadebox
QUARKUS_DATASOURCE_USERNAME: fadebox
QUARKUS_DATASOURCE_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
# Session-cookie encryption key; generate with `openssl rand -base64 32`.
QUARKUS_HTTP_AUTH_SESSION_ENCRYPTION_KEY: "${FADEBOX_SESSION_KEY:?set FADEBOX_SESSION_KEY in .env}"
# At-rest encryption key for stored credentials; generate the same way. Keep it stable —
# changing it makes every stored credential unreadable until re-entered.
FADEBOX_ENCRYPTION_KEY: "${FADEBOX_ENCRYPTION_KEY:?set FADEBOX_ENCRYPTION_KEY in .env}"
# Bare-clone cache for git value sources (kept on a volume so fetches stay incremental)
FADEBOX_GIT_CACHE_DIR: /data/git
# Container logs captured before a stop or reset removes them, so a stopped instance can
# still be read. Same volume as the git cache: state Fadebox owns and can lose.
FADEBOX_LOGS_CAPTURE_DIR: /data/logs
# The address users open Fadebox on, e.g. https://fadebox.example.com. Gating an
# instance needs it, and so does a managed ingress stack that cannot reach Fadebox on
# host.docker.internal:8080: a remote (tcp://) runtime, FADEBOX_PORT other than 8080, a
# proxy in front with no port published. Never localhost — inside Traefik's container that
# is Traefik itself — so leave it unset for a trial on this machine.
FADEBOX_PUBLIC_URL: ${FADEBOX_PUBLIC_URL:-}
volumes:
- ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock
- fadebox-git:/data
ports:
# A managed ingress stack on this host polls fadebox on host.docker.internal:8080 unless
# FADEBOX_PUBLIC_URL says otherwise, so with FADEBOX_PORT other than 8080 set
# FADEBOX_PUBLIC_URL too, or fadebox never hears the poll and after a minute the stack's
# status reads it as STALE.
- "${FADEBOX_PORT:-8080}:8080"
# Healthy once Fadebox serves and reaches its database; `docker compose up --wait` waits for
# it. Docker only reports an unhealthy container — it restarts nothing.
healthcheck:
test: [ "CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost:8080/q/health/ready" ]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
volumes:
fadebox-db:
fadebox-git:
The image tag 1.0 is the release line: it moves to each 1.0.x patch as it ships and never
to the next minor, so docker compose pull picks up fixes without a version jump. Every release
also carries its exact tag (1.0.0) for an installation that wants nothing to move on its own;
latest follows the newest release of any line, which is the one to avoid in a compose file that
is meant to stay put.
2. Create .env
Nothing host-specific is baked into the compose file, so it reads these from .env next to it
(compose fails fast with a named variable if one is missing):
{
echo "DOCKER_GID=$(stat -c '%g' /var/run/docker.sock)" # macOS: stat -f '%g'
echo "FADEBOX_SESSION_KEY=$(openssl rand -base64 32)"
echo "FADEBOX_ENCRYPTION_KEY=$(openssl rand -base64 32)"
echo "DB_PASSWORD=$(openssl rand -base64 24)"
} >> .env
DOCKER_GID is the group that owns the Docker socket — docker on most Linux hosts, 0 on Docker
Desktop, your own gid under rootless Docker (which also needs
DOCKER_SOCK=/run/user/$UID/docker.sock). The app container keeps its own uid and just joins that
group; without it, every deploy fails with permission denied on /var/run/docker.sock.
security_opt: label=disable is the other half of reaching the socket, on one class of host. Where
SELinux is enforcing and the daemon runs with its SELinux integration on (docker info lists
name=selinux under Security Options — the default on Fedora and RHEL), a container is confined
to files labelled for containers, and the daemon socket is not one of them: the same
permission denied follows whatever group the container is in. The option takes this one container
out of that confinement, which for a container that manages the daemon anyway changes little. On a
host without SELinux it does nothing. Fadebox sets the same option on the socket proxy of the
managed ingress stack since 1.3.0, for the same reason.
FADEBOX_ENCRYPTION_KEY encrypts stored credentials (registry passwords, git tokens, mTLS client
keys, OIDC client secrets) at rest, so a database dump or backup does not leak them. Keep it stable
and include it in your backups: unlike FADEBOX_SESSION_KEY (rotating that one just signs
everyone out), changing this key makes every stored credential unreadable until re-entered.
FADEBOX_PUBLIC_URL is optional, and a trial on this machine leaves it out. Anywhere else, add the
address users open Fadebox on:
echo "FADEBOX_PUBLIC_URL=https://fadebox.example.com" >> .env
Requiring a sign-in on an instance needs it: the
sign-in sends the visitor's browser there. So does a managed ingress stack that cannot
reach Fadebox on host.docker.internal:8080:
one on a remote (tcp://) runtime, Fadebox published on a port other than 8080, or a
reverse proxy in front with no port published. It must never be
localhost: inside Traefik's container that address is Traefik itself, so installing a stack is
refused.
3. Start it
docker compose up -d --wait
--wait returns once the app's healthcheck passes. It probes /q/health/ready, which needs no
sign-in and fails while the database is unreachable — the same path a load balancer or an uptime
monitor can watch, and docker compose ps shows the result as (healthy).
4. Sign in
Open http://localhost:8080. The initial admin user's generated password is printed once in the
log:
docker compose logs app | grep "generated password"
Sign in with it. Fadebox then requires a new password before the account can do anything else — a password somebody (or something) else chose is treated as temporary, so the first screen after this sign-in is the password form and every other page answers 403 until you are through it. The same applies to any account an admin creates or resets. See Passwords.
Fadebox's app container mounts /var/run/docker.sock — managing a Docker daemon is effectively
root on that host, so treat Fadebox admin access accordingly.
This publishes Fadebox on plain HTTP on port 8080, which is fine on a laptop and not fine anywhere
else: sign-in posts a password, and the session cookie is only marked Secure when the request
arrives as https. For anything beyond a local trial, put a TLS-terminating reverse proxy in front
and have it send X-Forwarded-Proto — see
Reverse proxy.
Which version you are running
The version sits in the sidebar footer, under your account, on every page — hover it for the date that build was made, and CLI beside it downloads the command-line client this server was released with. Administration → License shows the same two facts as a row, directly above the license's update horizon, which is checked against that build date; see Licensing.
The CLI reports its own build the usual way:
fadebox --version
Quote the server's version in any support conversation — it is the first thing that decides whether a behaviour is a bug or a release you have not upgraded to yet.
Next
Deploy your first environment — ingress domain, a template from the catalog, and a running URL.
Optional, whenever you need them:
- Remote runtimes to keep this VM small and deploy environments onto a powerful Docker host instead.
- Single sign-on against your identity provider, configured in the UI.
- Users, groups and roles for anything beyond the bootstrap admin.
- Reverse proxy if you terminate TLS in front of Fadebox — the headers it needs from your proxy, and the one setting that stops them being spoofed.
- Configuration reference for the environment variables the app understands.
- Container logs — Docker keeps every line a container
ever wrote until the daemon is told otherwise; two keys in
daemon.jsoncap it. - Upgrading when the next release ships — what to check first, and the release notes that say what changed.