Repository navigation
Expand file tree
/
Copy pathcompose.yaml
More file actions
370 lines (358 loc) · 17.2 KB
/
Copy pathcompose.yaml
File metadata and controls
370 lines (358 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# Kept for self-hosters. 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
# docker compose --profile https up -d # plus Caddy on 80/443 for KEPT_DOMAIN (Caddyfile below)
# docker compose --profile ollama up -d # plus Ollama, a local AI model server (below)
#
# Start order: db (healthy) → bootstrap (one-shot `kept bootstrap`: the four database logins and
# the extensions) → migrate (one-shot `kept migrate`) → kept. Each step exits 0 before the next.
#
# Database passwords: none to make. On the first start the bootstrap generates one per login:
# kept_app, kept_auth and kept_system into the kept-config volume (the serving container reads
# them there), kept_owner into the kept-owner volume (only migrate and the bootstrap mount it). On
# every start it sets each login's password to that value, so nothing in .env can lock Postgres
# out. An install made before 1.13.0 keeps the five KEPT_DB_*_PASSWORD lines in .env: set, they
# are used as given and nothing is generated.
#
# First boot also generates KEPT_SECRET_KEY and KEPT_AUTH_SECRET into kept-config unless .env sets
# both; `docker compose logs kept` says where. Keep the recovery kit (`docker compose exec kept
# kept admin recovery-kit`): without the keys, secret values are unreadable.
#
# Needs Kept 1.13.0 or later (the bootstrap command). (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.3}
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 <command>`, 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).
# Only Caddy's fixed address (below) is believed about X-Forwarded-For. Nothing else ever
# holds it: it lies outside the network's dynamic range.
KEPT_TRUSTED_PROXIES: 172.29.72.200
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]
# A split deployment (D186), for when one process is not enough: replace `kept` above with a
# web service and a single worker, both from the same image and environment. The worker serves
# no HTTP and publishes no port; it runs the jobs, the nightly backup among them, so it is the
# one that needs a backup directory mounted. The image's HEALTHCHECK follows KEPT_ROLE: /readyz
# for web, and for worker the liveness file its job loop touches (docker/healthcheck.mjs), so
# neither needs a healthcheck override. With local file storage both mount kept-data.
#
# kept-web:
# <<: *kept-image
# restart: unless-stopped
# environment: { <<: *ops-env, KEPT_ROLE: web, …the kept service's variables… }
# volumes: [kept-data:/data, kept-config:/config]
# ports: ["${KEPT_HTTP_PORT:-8080}:8080"]
# depends_on: { db: { condition: service_healthy }, migrate: { condition: service_completed_successfully } }
# networks: [kept]
# kept-worker:
# <<: *kept-image
# restart: unless-stopped
# environment: { <<: *ops-env, KEPT_ROLE: worker, …the kept service's variables… }
# volumes: [kept-data:/data, kept-config:/config]
# 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.3}
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
caddy:
profiles: [https]
# Tag read from Docker Hub, pinned by its multi-arch index digest (D193: chosen at build
# time, tag verified on the registry).
image: caddy:2.11.4-alpine@sha256:6aeddd44c3078b0f9a35206472a11420648a79c184603ef95957d0a20044cb2b
restart: unless-stopped
environment:
# Not `:?`: Compose interpolates every service, profile or not, so a required value here
# would break plain `docker compose up`. The command refuses to start without it instead.
KEPT_DOMAIN: ${KEPT_DOMAIN:-}
command:
- sh
- -c
- >-
test -n "$$KEPT_DOMAIN" || { echo "caddy: set KEPT_DOMAIN in .env" >&2; exit 1; };
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
# Besides .env, the one file a service reads from this folder, and only with the https
# profile. Without the repository: mkdir -p docker/caddy && curl -fsSLo docker/caddy/Caddyfile
# https://raw.githubusercontent.com/keptinventory/kept/v1.14.3/docker/caddy/Caddyfile
# Missing, the start stops with "bind source path does not exist" and creates nothing.
- type: bind
source: ./docker/caddy/Caddyfile
target: /etc/caddy/Caddyfile
read_only: true
bind:
create_host_path: false
- caddy-data:/data
- caddy-config:/config
depends_on: [kept]
networks:
kept:
ipv4_address: 172.29.72.200
ollama:
# A local model server for Kept's AI features (D85): `docker compose --profile ollama up -d`,
# then `docker compose exec ollama ollama pull <model>`. In Settings → AI, add an
# OpenAI-compatible provider with the base URL http://ollama:11434/v1 (Compose's network;
# Ollama ignores the API key) and the model you pulled. Nothing is published on the host.
# That address is a private one: turn on Admin → Settings → Allow private addresses first,
# or adding the provider is refused ("That address is on a private network").
# Tag read from Docker Hub (the latest stable release on 2026-10-06), pinned by its
# multi-arch index digest; about 3.8 GB (amd64) or 2.8 GB (arm64) to pull, before any model.
profiles: [ollama]
image: ollama/ollama:0.35.1@sha256:292ee7945dfc3d5840a181f3ab86fedb1e66703e02c8af98b50f4da56b7e278c
restart: unless-stopped
volumes:
# The models (Ollama's own Docker instructions mount this path).
- ollama:/root/.ollama
networks: [kept]
networks:
kept:
ipam:
config:
# If 172.29.72.0/24 clashes with your LAN, change the subnet, ip_range, Caddy's
# ipv4_address and KEPT_TRUSTED_PROXIES together.
- subnet: 172.29.72.0/24
ip_range: 172.29.72.0/25
volumes:
kept-db: {}
kept-data: {}
kept-config: {}
kept-owner: {}
caddy-data: {}
caddy-config: {}
ollama: {}