# Self-host CodemagicPatch stack for a single Docker host. # # Run through scripts/selfhost/install.sh so .env.selfhost exists and the # first API token is bootstrapped safely. Day-2 ops use the matching scripts: # scripts/selfhost/backup.sh, scripts/selfhost/restore.sh, scripts/selfhost/upgrade.sh. # # Running compose directly? This file expands ${VARS} from .env.selfhost, which # `docker compose up` does NOT auto-load (only a file named .env is). Pass it # explicitly, and use the same --project-name the installer/scripts use, or the # stack comes up with empty config and under a different project (separate # volumes) than backup/restore/upgrade expect. This base file is also not the # whole stack: the database and the object storage each ship as a mode overlay, # selected by SELFHOST_DATABASE_MODE / SELFHOST_STORAGE_MODE in .env.selfhost. # Database: deploy/selfhost/compose.bundled-db.yml when the flag is "bundled" # or absent, compose.external-db.yml when it is "external". Storage: # compose.bundled-storage.yml when the flag is "bundled" or absent, # compose.external-storage-s3.yml or compose.external-storage-gcs.yml when it # is "s3" or "gcs". Scheme: compose.http.yml after the storage overlay when # SELFHOST_SCHEME is "http" (plain HTTP, no certificates); nothing when it is # "https" or absent. For the default bundled stack: # docker compose --project-name codemagic-patch-selfhost --env-file .env.selfhost \ # -f docker-compose.selfhost.yml -f deploy/selfhost/compose.bundled-db.yml \ # -f deploy/selfhost/compose.bundled-storage.yml up -d services: caddy: image: ${CODEMAGIC_PATCH_CADDY_IMAGE:-codemagic-patch-caddy:selfhost} build: context: . dockerfile: deploy/selfhost/Dockerfile.caddy restart: unless-stopped depends_on: server: condition: service_healthy environment: CODEMAGIC_PATCH_API_DOMAIN: ${CODEMAGIC_PATCH_API_DOMAIN} # CODEMAGIC_PATCH_STORAGE_DOMAIN comes from the bundled storage overlay # (deploy/selfhost/compose.bundled-storage.yml); external storage modes # have no storage domain. ACME_EMAIL: ${ACME_EMAIL} ports: - "80:80" - "443:443" volumes: # The external-storage overlays repoint this mount's source at # Caddyfile.api-only (same target, so compose replaces it in place — # which also keeps the mount order stable for the golden rendering test). - ./deploy/selfhost/Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config server: image: ${CODEMAGIC_PATCH_SERVER_IMAGE:-codemagic-patch-server:selfhost} build: context: . dockerfile: server/Dockerfile restart: unless-stopped environment: MODE: all HOST: 0.0.0.0 PORT: 3000 LOGGER: ${LOGGER:-true} RUN_MIGRATIONS: ${RUN_MIGRATIONS:-true} # DATABASE_URL comes from the database mode overlay # (deploy/selfhost/compose.bundled-db.yml or compose.external-db.yml). WORKER_SHARED_SECRET: ${WORKER_SHARED_SECRET} # STORAGE_ADAPTER and the storage credentials come from the storage mode # overlay (deploy/selfhost/compose.bundled-storage.yml, # compose.external-storage-s3.yml, or compose.external-storage-gcs.yml). PUBLIC_BASE_URL: ${PUBLIC_BASE_URL} # Left empty on purpose: the server derives the mutable-JSON policy from # DELIVERY_ADAPTER (revalidate-always for base-url, a five-minute # s-maxage behind a purging CDN). Set it in .env.selfhost only to # override that. MANIFEST_CACHE_CONTROL: ${MANIFEST_CACHE_CONTROL:-} DELIVERY_ADAPTER: ${DELIVERY_ADAPTER:-base-url} CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN:-} CLOUDFLARE_ZONE_ID: ${CLOUDFLARE_ZONE_ID:-} CLOUDFLARE_API_BASE_URL: ${CLOUDFLARE_API_BASE_URL:-} CLOUDFRONT_DISTRIBUTION_ID: ${CLOUDFRONT_DISTRIBUTION_ID:-} CLOUDFRONT_ACCESS_KEY_ID: ${CLOUDFRONT_ACCESS_KEY_ID:-} CLOUDFRONT_SECRET_ACCESS_KEY: ${CLOUDFRONT_SECRET_ACCESS_KEY:-} GITHUB_OAUTH_CLIENT_ID: ${GITHUB_OAUTH_CLIENT_ID:-} GITHUB_OAUTH_CLIENT_SECRET: ${GITHUB_OAUTH_CLIENT_SECRET:-} GITHUB_OAUTH_ALLOWED_REDIRECT_URIS: ${GITHUB_OAUTH_ALLOWED_REDIRECT_URIS:-} OAUTH_CLI_AUTH_SECRET: ${OAUTH_CLI_AUTH_SECRET:-} # Legacy name; the server reads it as a permanent fallback for # OAUTH_CLI_AUTH_SECRET, so env files from older installs keep working. OAUTH_DEVICE_POLL_TOKEN_SECRET: ${OAUTH_DEVICE_POLL_TOKEN_SECRET:-} GITHUB_OAUTH_SCOPES: ${GITHUB_OAUTH_SCOPES:-read:user user:email} # Bitbucket Cloud sign-in (web flow only) — a second provider next to # GitHub, or the sole provider when GitHub is not configured. BITBUCKET_OAUTH_CLIENT_ID: ${BITBUCKET_OAUTH_CLIENT_ID:-} BITBUCKET_OAUTH_CLIENT_SECRET: ${BITBUCKET_OAUTH_CLIENT_SECRET:-} BITBUCKET_OAUTH_ALLOWED_REDIRECT_URIS: ${BITBUCKET_OAUTH_ALLOWED_REDIRECT_URIS:-} # GitLab sign-in (web flow only) — next to GitHub/Bitbucket, or a # provider when they are not configured. GITLAB_API_BASE_URL defaults # server-side to /api/v4 when unset. GITLAB_OAUTH_CLIENT_ID: ${GITLAB_OAUTH_CLIENT_ID:-} GITLAB_OAUTH_CLIENT_SECRET: ${GITLAB_OAUTH_CLIENT_SECRET:-} GITLAB_OAUTH_ALLOWED_REDIRECT_URIS: ${GITLAB_OAUTH_ALLOWED_REDIRECT_URIS:-} GITLAB_OAUTH_SCOPES: ${GITLAB_OAUTH_SCOPES:-read_user} GITLAB_OAUTH_BASE_URL: ${GITLAB_OAUTH_BASE_URL:-} GITLAB_API_BASE_URL: ${GITLAB_API_BASE_URL:-} INITIAL_ADMIN_EMAILS: ${INITIAL_ADMIN_EMAILS:-} # The fixed `default-team` is created on boot (idempotent); the initial # admin becomes its owner on first sign-in. Team CRUD is disabled in the # CLI/dashboard. The team name is not configurable. REGISTRATION_MODE: ${REGISTRATION_MODE:-} healthcheck: # Probe /health/ready (DB-backed readiness), not /health (liveness): the # latter returns ok while the database is down, so the container — and # anything gated on `service_healthy` (caddy, the install/restore waits) — # would go green with a dead DB. start_period keeps failing checks from # flapping the container to "unhealthy" while it boots and migrates. test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health/ready >/dev/null || exit 1", ] interval: 5s timeout: 5s retries: 60 start_period: 60s volumes: caddy-data: caddy-config: