Files
omar ffc462cf28 Build in detached worktrees and advance pins only after the fence
- update.py builds target commits in temporary detached worktrees with
  temporary image tags; checkouts and runtime tags move only after the API
  fence and quiescence check pass, so refused/build-only runs keep
  --verify-only passing.
- Activation includes the execution profile (worker/watchdog restart under
  the fence) and verifies every service container runs the built image
  before writing approved-commits.env.
- Compose: bounded local logging, API healthcheck via `otche healthcheck`,
  web healthcheck and healthy API dependency, env-driven worker limits.
- TLS example: request-time upstream resolution, gzip, immutable assets,
  headers not duplicated on /api.
- README: new update flow, sizing for parallel runs, fenced backup and
  restore drill.
2026-09-24 17:55:13 +03:00

158 lines
4.8 KiB
YAML

name: otche
x-logging: &logging
driver: local
options:
max-size: "10m"
max-file: "5"
services:
postgres:
logging: *logging
image: postgres:17-alpine
user: "70:70"
read_only: true
tmpfs: ["/tmp:size=32m,noexec,nosuid", "/var/run/postgresql:size=8m,noexec,nosuid"]
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
environment:
POSTGRES_DB: otche
POSTGRES_USER: otche
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
secrets: [postgres-password]
volumes: [postgres-data:/var/lib/postgresql/data]
healthcheck:
test: [CMD-SHELL, "pg_isready -U otche -d otche"]
interval: 5s
timeout: 3s
retries: 20
restart: unless-stopped
networks: [private]
mem_limit: 768m
storage-init:
logging: *logging
image: debian:bookworm-slim
user: "0:0"
command: [sh, -ec, "chown 10001:10001 /var/lib/otche && chmod 0700 /var/lib/otche"]
volumes: [artifacts:/var/lib/otche]
network_mode: none
cap_drop: [ALL]
cap_add: [CHOWN, FOWNER]
security_opt: [no-new-privileges:true]
migrate:
logging: *logging
build: {context: ../otche-backend, target: api}
command: [migrate]
environment: {DATABASE_URL_FILE: /run/secrets/database-url}
secrets: [database-url]
depends_on:
postgres: {condition: service_healthy}
networks: [private]
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
api:
logging: *logging
build: {context: ../otche-backend, target: api}
environment:
DATABASE_URL_FILE: /run/secrets/database-url
PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:?Set PUBLIC_ORIGIN}
ALLOW_INSECURE_HTTP: ${ALLOW_INSECURE_HTTP:-false}
ARTIFACT_ROOT: /var/lib/otche
MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-536870912}
MAX_OWNER_BYTES: ${MAX_OWNER_BYTES:-8589934592}
MAX_QUEUED_JOBS: ${MAX_QUEUED_JOBS:-20}
secrets: [database-url]
volumes: [artifacts:/var/lib/otche]
depends_on:
migrate: {condition: service_completed_successfully}
storage-init: {condition: service_completed_successfully}
healthcheck:
test: ["CMD", "otche", "healthcheck"]
interval: 10s
timeout: 3s
retries: 6
start_period: 10s
restart: unless-stopped
networks: [private, edge]
read_only: true
tmpfs: ["/tmp:size=32m,noexec,nosuid"]
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: 256m
cpus: 1.0
worker:
logging: *logging
build: {context: ../otche-backend, target: worker}
command: [worker]
profiles: [execution]
environment:
DATABASE_URL_FILE: /run/secrets/database-url
WORKER_CONFIG_FILE: /run/otche/config.json
ARTIFACT_ROOT: /var/lib/otche
secrets: [database-url]
volumes:
- artifacts:/var/lib/otche
- ./secrets/worker:/run/otche:ro
depends_on:
migrate: {condition: service_completed_successfully}
storage-init: {condition: service_completed_successfully}
restart: unless-stopped
networks: [private, execution]
read_only: true
tmpfs: ["/tmp:size=128m,noexec,nosuid"]
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: ${WORKER_MEM_LIMIT:-1536m}
cpus: ${WORKER_CPUS:-2.0}
pids_limit: ${WORKER_PIDS_LIMIT:-128}
stop_grace_period: 60s
watchdog:
logging: *logging
build: {context: ../otche-backend, target: worker}
command: [watchdog]
profiles: [execution]
environment:
DATABASE_URL_FILE: /run/secrets/database-url
WORKER_CONFIG_FILE: /run/otche/config.json
secrets: [database-url]
volumes: ["./secrets/worker:/run/otche:ro"]
depends_on:
migrate: {condition: service_completed_successfully}
restart: unless-stopped
networks: [private, execution]
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: 128m
cpus: 0.25
web:
logging: *logging
build: {context: ../otche-frontend}
ports: ["${BIND_ADDRESS:-127.0.0.1}:${WEB_PORT:-8088}:8080"]
depends_on:
api: {condition: service_healthy}
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/ || wget -q --spider --no-check-certificate https://127.0.0.1:8443/"]
interval: 10s
timeout: 3s
retries: 6
start_period: 10s
restart: unless-stopped
networks: [edge]
read_only: true
tmpfs: ["/tmp:size=32m,noexec,nosuid", "/var/cache/nginx:size=32m,noexec,nosuid", "/var/run:size=1m,noexec,nosuid"]
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: 128m
secrets:
postgres-password:
file: ./secrets/postgres-password
database-url:
file: ./secrets/database-url
volumes:
postgres-data:
artifacts:
networks:
private: {internal: true}
edge: {}
execution: {}