Production deployment with Docker Compose. Configuration details: configuration.md. Day-to-day ops: operating.md.
- Docker Engine and Docker Compose v2
- A host directory or named volume for your media libraries
- (Recommended) A reverse proxy with TLS - see Configuration - Reverse proxy
Use the sample stack at the repo root:
docker-compose.yaml- Postgres + K7 (ghcr.io/kaybi-gh/k7:latest).env.example- copy to.envand setPOSTGRES_PASSWORDandSECURITY__APIKEYS__HASHSECRET
cp .env.example .env
# Edit .env and set POSTGRES_PASSWORD and SECURITY__APIKEYS__HASHSECRET
docker compose up -dOpen http://localhost:7080 (or your reverse-proxy URL) and complete first-run setup.
The sample sets Security__ForceHttps=false so the setup wizard works over plain HTTP. Behind a TLS reverse proxy, set Security__ForceHttps=true (and BaseUrl) - private Docker/LAN proxies are trusted by default. See configuration.md.
For anything beyond a local trial (OIDC, federation, reverse proxy), set BaseUrl in compose to the public URL browsers and peers use - see configuration.md.
Point a library at /media/movies (or replace the movies volume in compose with a bind mount to your real media folder). To build the image locally instead of pulling GHCR: docker build -t k7-server:latest . then set image: k7-server:latest on the k7-server service.
For production, set a realistic Docker memory limit on k7-server (home setups often use mem_limit: 2g). See Operating - Memory and container sizing.
The sample publishes Postgres on host port 5432 for convenience. On a public host, remove that ports mapping (or bind 127.0.0.1:5432 only) so the database is not reachable from the internet - see configuration.md.
Sqlite is supported for small trials but is less performant than Postgres and not recommended for production - see configuration.md.
| Volume / mount | Purpose |
|---|---|
k7-postgres-data -> /var/lib/postgresql |
Postgres database (PG 18+ parent path, not .../data) |
k7-data -> /data |
Server state: config, metadatas, logs, transcoding cache |
movies -> /media/movies:ro |
Example media library (Compose-managed named volume; swap for a bind mount in real use) |
Relative paths inside the container are not durable across recreate. Always use absolute paths under a mounted volume (the sample sets Paths__* to /data/...). See Backup and troubleshooting.
The image runs as non-root. entrypoint.sh remaps appuser using:
| Env | Default | Purpose |
|---|---|---|
PUID |
911 |
Container UID |
PGID |
911 |
Container GID |
Set these to match the owner of your media mounts. The entrypoint also chowns /data and /media when those directories exist and are writable. You can set PUID / PGID in .env (see .env.example).
The sample Compose file does not pass through a GPU. For Intel/AMD VAAPI mount /dev/dri; for NVIDIA NVENC use the NVIDIA Container Toolkit (gpus: all). Details and examples: Operating - Hardware acceleration.
Until setup completes, the server redirects non-API browser traffic to /setup and returns 503 for most /api/* routes.
You can finish first-run in any of these ways:
| Path | When to use | Setup token |
|---|---|---|
Password wizard on /setup |
Default local admin | Required when a token was generated (see logs / K7_SETUP_TOKEN) |
OIDC on /setup |
OIDC already enabled in config before first boot | Not required - completing IdP login creates the first Administrator |
| Unattended env | K7_ADMIN_USERNAME (or K7_ADMIN_EMAIL) + K7_ADMIN_PASSWORD |
N/A (bootstrap skips the wizard) |
- Open the server URL.
- Create the first Administrator with a username + password (email is optional).
- If a setup token field is shown, enter the token from the server logs. Look for a line containing
K7_SETUP_TOKEN=(re-logged on every restart until setup completes), or setK7_SETUP_TOKENbefore first start. The token stops anyone who can reach/setupfrom becoming admin without server access.
Password rules (Identity defaults): length at least 10, upper and lower case, a digit, at least 4 distinct characters.
- Configure OIDC (and usually
BaseUrl) before or on first boot - see configuration.md. - Open
/setupand use Create admin with ... (your IdP). - Sign in at the IdP. On success K7 creates the Administrator and marks setup complete - no setup token.
The first account that completes this OIDC flow becomes admin. Prefer an IdP you control (no open self-registration) if /setup is reachable on the network before setup finishes.
| Mechanism | How |
|---|---|
| Env credentials | Set K7_ADMIN_USERNAME (preferred) or K7_ADMIN_EMAIL, plus K7_ADMIN_PASSWORD, before first start. Optional K7_ADMIN_EMAIL is stored when it looks like an email. |
| Setup token | Applies to the password wizard only (K7_SETUP_TOKEN, or auto-generated and logged) |
| Existing admin | If an Administrator already exists, setup is treated as completed |
After setup: create libraries and enable Guest from the admin UI. Registration and OIDC are config-only (env / appsettings) - the Authentication admin panel is read-only. See configuration.md and operating.md.
On every startup the server applies pending EF Core migrations automatically.
- Back up the database and
/data(especiallyPaths:Config). - Read the GitHub release notes for breaking changes.
- Pull and recreate:
docker compose pull
docker compose up -d
docker compose logs -f k7-serverIf k7-postgres exits with a message about /var/lib/postgresql/data or pg_upgrade --link, see Postgres 18 volume mount.
Downgrading after newer migrations have been applied is not supported - restore from a pre-upgrade backup.
The official Postgres 18 image stores the cluster under a versioned path (/var/lib/postgresql/18/docker) and expects a single volume at /var/lib/postgresql. Mounting the historical /var/lib/postgresql/data makes the container exit on start - including a fresh empty volume - with a journal message about an unused mount and pg_upgrade --link.
The sample docker-compose.yaml and the Helm bundled Postgres (database.mode=deployment) use the new path. CloudNativePG (database.mode=cnpg) and an external database are unchanged.
Empty volume / first install that never started: pull this compose (or chart) and recreate. You do not need a dump.
Volume that already has a cluster (Postgres 17 or earlier, or a 18 container that never started because of the old mount): changing the mount alone is not enough. Dump with the previous major, recreate an empty volume, then restore:
- Stop the app. Keep the existing named volume.
docker compose stop k7-server postgres- Dump with a Postgres 17 container against the old mount layout. Compose names the volume
{project}_k7-postgres-data(docker volume ls). Load.envsoPOSTGRES_PASSWORDis set.
docker run --rm -d --name k7-pg17-dump \
-v PROJECT_k7-postgres-data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
-e POSTGRES_USER="${POSTGRES_USER:-postgres}" \
-e POSTGRES_DB="${POSTGRES_DB:-K7}" \
postgres:17-alpine
until docker exec k7-pg17-dump pg_isready -U "${POSTGRES_USER:-postgres}"; do sleep 1; done
docker exec -T k7-pg17-dump \
pg_dump -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-K7}" -Fc > k7-before-pg18.dump
docker stop k7-pg17-dump- Confirm the dump file is non-empty. Then remove only the Postgres volume so 18 can
initdb(the same volume remounted at/var/lib/postgresqlstill containsPG_VERSIONat the root and will fail the same check). Do not usedocker compose down -v- that also dropsk7-data.
docker compose down
docker volume rm PROJECT_k7-postgres-data- Start Postgres 18 from the updated compose, restore, then start K7:
docker compose up -d postgres
# Wait until healthy, then:
docker compose exec -T postgres \
pg_restore -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-K7}" --clean --if-exists < k7-before-pg18.dump
docker compose up -dKeep k7-before-pg18.dump until you have signed in and confirmed libraries. Same idea for Helm bundled Postgres: dump, delete the PVC (or restore into a new one), upgrade the chart, restore. See Backup and troubleshooting.
| Tag | Meaning |
|---|---|
latest |
Latest published non-prerelease |
x.y.z |
Exact semver |
x.y / x |
Floating major/minor from the release workflow |
Pin a semver tag in production. Breaking changes are called out in GitHub Releases (and breaking-change PR labels).
Use the import tool: tools/K7.Import/README.md.
Back up the database first. The import tool has no rollback; a failed or unwanted import is recovered by restoring that backup (see Backup and troubleshooting).
The chart is published as an OCI artifact on GHCR: oci://ghcr.io/kaybi-gh/charts/k7.
helm install k7 oci://ghcr.io/kaybi-gh/charts/k7 \
--version <x.y.z> \
--set database.mode=cnpg \
--set security.apiKeysHashSecret=<long-random-string>Database is selected by database.mode (charts/k7/values.yaml):
database.mode |
Behaviour |
|---|---|
cnpg |
Chart provisions a Postgres Cluster and wires K7 to it. Requires the CNPG operator. |
deployment |
Chart runs a single-replica bundled Postgres. Simple, not HA - trials/homelab. |
external (default) |
Set database.external.host, .user, and .password (or .existingSecret). |
Persist /data (config, metadata, logs, transcoding) via persistence, and mount media libraries read-only via mediaVolumes. Behind an ingress terminating TLS, set security.forceHttps=true and baseUrl.
Skip the setup wizard by bootstrapping the admin unattended: --set setup.adminUsername=admin --set setup.adminPassword=<strong-password>. Otherwise read the auto-generated setup token from the pod logs (kubectl logs deploy/<release>-k7), or pin it with setup.token.
Pod Security: the bundled Postgres ships restricted-compliant defaults. The K7 image starts as root (remaps PUID/PGID, chowns /data), so it is not restricted-compliant out of the box - podSecurityContext / securityContext are exposed in values for a baseline namespace or a non-root image.
Supported production path: Docker (or another container runtime) using the published image.
Running published binaries on bare metal is possible (.NET 10 runtime, ffmpeg, Postgres or Sqlite) but is not a documented or supported install mode.