# Kept for self-hosters: Kept and its database. This file works on its own, without the repository: # # docker compose run --rm init # asks the address you'll open Kept at; writes .env # docker compose up -d # then open that address and enter the setup code # # What runs: db and kept stay running. bootstrap (the database logins and extensions) and migrate # (`kept migrate`) run once on each start, in that order, and exit. init runs only when you run it. # # No passwords to make: bootstrap generates the database's into the kept-config and kept-owner # volumes, and Kept generates its keys into kept-config. Keep the recovery kit (`docker compose exec # kept kept admin recovery-kit`): without the keys, secret values are unreadable. # # Caddy for HTTPS and Ollama for local AI are in compose.full.yaml: this file plus those. # Needs Kept 1.13.0 or later. (compose.dev.yaml is the developer database, not this.) name: kept x-kept-image: &kept-image # A released image; `kept init` pins the version it ran as in .env (KEPT_IMAGE), and an upgrade # changes that line (`docker compose pull && docker compose up -d`). To run your own build, in a # checkout: `docker build -t kept:local .` and KEPT_IMAGE=kept:local. image: ${KEPT_IMAGE:-ghcr.io/keptinventory/kept:1.14.5} read_only: true tmpfs: [/tmp] cap_drop: [ALL] security_opt: [no-new-privileges:true] # What both the serving process and `migrate` read: the keys, the downgrade switch and the backup # settings. Backups (D64, D66) are restic, to a directory, an S3 bucket or SFTP. Every # backup variable set in .env locks its field in Admin → Backups; empty ones leave it to the # screen. The serving process runs the nightly backup, as the database owner, so it also needs # KEPT_OWNER_DATABASE_URL from a compose.override.yaml (the docs' Backups page); `migrate` takes # the snapshot before an upgrade. # A directory target is a path inside the container: bind-mount the host directory on both # services (the commented `/backups` lines), owned by uid 10001. SFTP's key and known-hosts files # are mounted the same way, read-only. compose.env.example explains each variable. x-ops-env: &ops-env KEPT_BACKUP_DIR: ${KEPT_BACKUP_DIR:-} KEPT_BACKUP_PASSWORD: ${KEPT_BACKUP_PASSWORD:-} KEPT_BACKUP_S3_BUCKET: ${KEPT_BACKUP_S3_BUCKET:-} KEPT_BACKUP_S3_PREFIX: ${KEPT_BACKUP_S3_PREFIX:-} KEPT_BACKUP_S3_ENDPOINT: ${KEPT_BACKUP_S3_ENDPOINT:-} KEPT_BACKUP_S3_REGION: ${KEPT_BACKUP_S3_REGION:-} KEPT_BACKUP_S3_ACCESS_KEY_ID: ${KEPT_BACKUP_S3_ACCESS_KEY_ID:-} KEPT_BACKUP_S3_SECRET_ACCESS_KEY: ${KEPT_BACKUP_S3_SECRET_ACCESS_KEY:-} KEPT_BACKUP_S3_FORCE_PATH_STYLE: ${KEPT_BACKUP_S3_FORCE_PATH_STYLE:-} KEPT_BACKUP_SFTP: ${KEPT_BACKUP_SFTP:-} KEPT_BACKUP_SFTP_KEY_FILE: ${KEPT_BACKUP_SFTP_KEY_FILE:-} KEPT_BACKUP_SFTP_KNOWN_HOSTS: ${KEPT_BACKUP_SFTP_KNOWN_HOSTS:-} KEPT_BACKUP_TIME: ${KEPT_BACKUP_TIME:-} KEPT_BACKUP_KEEP_DAILY: ${KEPT_BACKUP_KEEP_DAILY:-} KEPT_BACKUP_KEEP_WEEKLY: ${KEPT_BACKUP_KEEP_WEEKLY:-} KEPT_BACKUP_KEEP_MONTHLY: ${KEPT_BACKUP_KEEP_MONTHLY:-} # The keys open the backup settings sealed in the database (Admin → Backups). Both or neither; # unset, they are read from the kept-config volume's secrets.json (D193). KEPT_SECRET_KEY: ${KEPT_SECRET_KEY:-} KEPT_AUTH_SECRET: ${KEPT_AUTH_SECRET:-} # One-version rollback: `1` lets an older image start on a newer database anyway, # audited. Only for a restore runbook. KEPT_ALLOW_DOWNGRADE: ${KEPT_ALLOW_DOWNGRADE:-} services: db: # Tag read from Docker Hub, pinned by its multi-arch index digest. Postgres major upgrades # are a dump and restore (D186), never a tag bump. image: pgvector/pgvector:0.8.6-pg18-bookworm@sha256:2ba9ca5f2e7daa0f0e7723cba1ee9167bab54efd3640516a44ac1a928dd67e7a restart: unless-stopped # The superuser's password is read once, when the volume is first made. Empty (any install # made by `kept init`), a random one is made for that moment and kept nowhere: the superuser is # reached only from inside this container, over its socket or its own loopback, which the image # trusts (`docker compose exec db psql -U postgres`, and the bootstrap below). Over the network # every login needs its password. entrypoint: - sh - -c - >- if [ -z "$$POSTGRES_PASSWORD" ]; then POSTGRES_PASSWORD=$$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n'); export POSTGRES_PASSWORD; fi; exec docker-entrypoint.sh "$$@" - db command: [postgres] environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: ${KEPT_DB_SUPERUSER_PASSWORD:-} POSTGRES_DB: kept TZ: UTC volumes: - kept-db:/var/lib/postgresql healthcheck: # Over TCP: during init the server listens on the socket only, so this passes only once # the real server is up. test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres -d kept"] interval: 5s timeout: 5s retries: 30 networks: [kept] bootstrap: <<: *kept-image entrypoint: ["kept"] command: ["bootstrap"] restart: "no" # In the db container's network namespace: 127.0.0.1 is the database's own loopback, where the # Postgres image lets the superuser in. No other service shares it. network_mode: service:db environment: PGHOST: 127.0.0.1 PGPORT: "5432" PGUSER: postgres PGDATABASE: kept # Only an install whose loopback isn't trusted needs it. PGPASSWORD: ${KEPT_DB_SUPERUSER_PASSWORD:-} # Set (installs made before 1.13.0): used as given and never written to a file. Empty: the # file below, generated on the first start. KEPT_DB_OWNER_PASSWORD: ${KEPT_DB_OWNER_PASSWORD:-} KEPT_DB_APP_PASSWORD: ${KEPT_DB_APP_PASSWORD:-} KEPT_DB_AUTH_PASSWORD: ${KEPT_DB_AUTH_PASSWORD:-} KEPT_DB_SYSTEM_PASSWORD: ${KEPT_DB_SYSTEM_PASSWORD:-} KEPT_DB_OWNER_PASSWORD_FILE: /owner/kept_owner KEPT_DB_APP_PASSWORD_FILE: /config/database/kept_app KEPT_DB_AUTH_PASSWORD_FILE: /config/database/kept_auth KEPT_DB_SYSTEM_PASSWORD_FILE: /config/database/kept_system volumes: - kept-config:/config - kept-owner:/owner healthcheck: disable: true depends_on: db: condition: service_healthy migrate: <<: *kept-image entrypoint: ["kept"] command: ["migrate"] restart: "no" # Only the owner login: the serving process never holds it. The # public URL and mail settings are for `migrate admin `, whose notices are mailed # to the people they concern (D180). The keys (the config volume, read-only, or the two # variables) and the backup settings are for the snapshot `kept migrate` takes before it # migrates a populated database: with a backup target it snapshots the database # first and migrates nothing if that fails; without one it says so loudly and goes on. # KEPT_UPGRADE_SNAPSHOT=off skips it. environment: <<: *ops-env # Without a password in .env, the URL takes it from the owner's file. KEPT_OWNER_DATABASE_URL: postgres://kept_owner:${KEPT_DB_OWNER_PASSWORD:-}@db:5432/kept KEPT_DB_OWNER_PASSWORD_FILE: /owner/kept_owner KEPT_PUBLIC_URL: ${KEPT_PUBLIC_URL:-} KEPT_SMTP_URL: ${KEPT_SMTP_URL:-} KEPT_SMTP_FROM: ${KEPT_SMTP_FROM:-} KEPT_UPGRADE_SNAPSHOT: ${KEPT_UPGRADE_SNAPSHOT:-} volumes: # restic's cache and the snapshot's scratch space live under /data (KEPT_DATA_DIR). - kept-data:/data - kept-config:/config:ro - kept-owner:/owner:ro # - /mnt/backup/kept:/backups healthcheck: disable: true depends_on: db: condition: service_healthy bootstrap: condition: service_completed_successfully networks: [kept] kept: <<: *kept-image restart: unless-stopped environment: <<: *ops-env # Without passwords in .env, each URL takes its password from the bootstrap's file. KEPT_DATABASE_URL: postgres://kept_app:${KEPT_DB_APP_PASSWORD:-}@db:5432/kept KEPT_AUTH_DATABASE_URL: postgres://kept_auth:${KEPT_DB_AUTH_PASSWORD:-}@db:5432/kept KEPT_SYSTEM_DATABASE_URL: postgres://kept_system:${KEPT_DB_SYSTEM_PASSWORD:-}@db:5432/kept KEPT_DB_APP_PASSWORD_FILE: /config/database/kept_app KEPT_DB_AUTH_PASSWORD_FILE: /config/database/kept_auth KEPT_DB_SYSTEM_PASSWORD_FILE: /config/database/kept_system # Required; Kept refuses to start without it (`kept: invalid environment: KEPT_PUBLIC_URL`). # Not `:?` here, so `docker compose run --rm init` works before .env exists. KEPT_PUBLIC_URL: ${KEPT_PUBLIC_URL:-} # KEPT_SECRET_KEY and KEPT_AUTH_SECRET come with the backup variables above: both or # neither; unset, they are generated into kept-config on first boot (D193). # Your reverse proxy's address as Kept sees it, from .env: the only address believed about # X-Forwarded-For. Empty, no proxy is believed (the docs' Reverse proxies page). KEPT_TRUSTED_PROXIES: ${KEPT_TRUSTED_PROXIES:-} # HSTS over https (#403): off sends max-age=0, for plain-http services on the same hostname. KEPT_HSTS: ${KEPT_HSTS:-} KEPT_HTTP_SIGN_IN_DAYS: ${KEPT_HTTP_SIGN_IN_DAYS:-} KEPT_EXTRA_ADDRESSES: ${KEPT_EXTRA_ADDRESSES:-} KEPT_SMTP_URL: ${KEPT_SMTP_URL:-} KEPT_SMTP_FROM: ${KEPT_SMTP_FROM:-} KEPT_LOG_LEVEL: ${KEPT_LOG_LEVEL:-info} KEPT_LOG_FORMAT: ${KEPT_LOG_FORMAT:-json} KEPT_SETUP_CODE: ${KEPT_SETUP_CODE:-} KEPT_SIGNUP_OPEN: ${KEPT_SIGNUP_OPEN:-} # Files: empty values fall back to the defaults. KEPT_MAX_FILE_MB: ${KEPT_MAX_FILE_MB:-} KEPT_IMAGE_CONCURRENCY: ${KEPT_IMAGE_CONCURRENCY:-} KEPT_STORAGE: ${KEPT_STORAGE:-local} KEPT_S3_ENDPOINT: ${KEPT_S3_ENDPOINT:-} KEPT_S3_PUBLIC_ENDPOINT: ${KEPT_S3_PUBLIC_ENDPOINT:-} KEPT_S3_REGION: ${KEPT_S3_REGION:-} KEPT_S3_BUCKET: ${KEPT_S3_BUCKET:-} KEPT_S3_ACCESS_KEY_ID: ${KEPT_S3_ACCESS_KEY_ID:-} KEPT_S3_SECRET_ACCESS_KEY: ${KEPT_S3_SECRET_ACCESS_KEY:-} KEPT_S3_FORCE_PATH_STYLE: ${KEPT_S3_FORCE_PATH_STYLE:-} # Barcode lookup (D104, D126): off unless the admin turns it on or this locks it. KEPT_BARCODE_LOOKUP: ${KEPT_BARCODE_LOOKUP:-} KEPT_BARCODE_CONTACT: ${KEPT_BARCODE_CONTACT:-} # Web push: both keys or neither; unset, a pair is generated at first run. KEPT_VAPID_PUBLIC_KEY: ${KEPT_VAPID_PUBLIC_KEY:-} KEPT_VAPID_PRIVATE_KEY: ${KEPT_VAPID_PRIVATE_KEY:-} KEPT_VAPID_SUBJECT: ${KEPT_VAPID_SUBJECT:-} # Semantic search (D207), generic OIDC sign-in (D127) and Google sign-in # (1.2, D226): empty values fall back to the defaults. KEPT_EMBEDDINGS: ${KEPT_EMBEDDINGS:-} KEPT_OIDC_ISSUER: ${KEPT_OIDC_ISSUER:-} KEPT_OIDC_CLIENT_ID: ${KEPT_OIDC_CLIENT_ID:-} KEPT_OIDC_CLIENT_SECRET: ${KEPT_OIDC_CLIENT_SECRET:-} KEPT_OIDC_NAME: ${KEPT_OIDC_NAME:-} KEPT_OIDC_AUTOPROVISION_DOMAINS: ${KEPT_OIDC_AUTOPROVISION_DOMAINS:-} KEPT_OIDC_AUTOPROVISION_GROUPS: ${KEPT_OIDC_AUTOPROVISION_GROUPS:-} KEPT_OIDC_GROUPS_CLAIM: ${KEPT_OIDC_GROUPS_CLAIM:-} KEPT_OIDC_SCOPES: ${KEPT_OIDC_SCOPES:-} KEPT_GOOGLE_CLIENT_ID: ${KEPT_GOOGLE_CLIENT_ID:-} KEPT_GOOGLE_CLIENT_SECRET: ${KEPT_GOOGLE_CLIENT_SECRET:-} # Check for new versions (D65): off by default and an admin setting; true or false locks it. KEPT_UPDATE_CHECK: ${KEPT_UPDATE_CHECK:-} # Tracing to your own OpenTelemetry collector (D84): unset, nothing is loaded or sent. OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} # The bearer token that turns /metrics on: empty, the path doesn't exist. KEPT_METRICS_TOKEN: ${KEPT_METRICS_TOKEN:-} # KEPT_SOURCE_URL is not set here: the image's default (its OCI source + revision, D147, # D186) applies. A fork that serves modified code adds KEPT_SOURCE_URL pointing at its own. volumes: - kept-data:/data - kept-config:/config # - /mnt/backup/kept:/backups ports: - "${KEPT_HTTP_PORT:-8080}:8080" depends_on: db: condition: service_healthy migrate: condition: service_completed_successfully networks: [kept] init: # `docker compose run --rm init` (`kept init`): asks the address and writes .env in this # folder. `up` never starts it. The only service that sees this folder; it runs as root with # just the two capabilities that create .env in a folder you own and hand the file to you, # and no network. profiles: [init] image: ${KEPT_IMAGE:-ghcr.io/keptinventory/kept:1.14.5} entrypoint: ["kept", "init"] user: "0:0" read_only: true tmpfs: [/tmp] cap_drop: [ALL] cap_add: [CHOWN, DAC_OVERRIDE] security_opt: [no-new-privileges:true] network_mode: none working_dir: /install volumes: - .:/install restart: "no" healthcheck: disable: true networks: kept: ipam: config: # If 172.29.72.0/24 clashes with your LAN, change the subnet and ip_range together (and # Caddy's ipv4_address, in compose.full.yaml). - subnet: 172.29.72.0/24 ip_range: 172.29.72.0/25 volumes: kept-db: {} kept-data: {} kept-config: {} kept-owner: {}