# Cobblr self-host: the standalone stack. Two files: this and the .env beside
# it (generate the .env at https://docs.cobblr.xyz/setup-builder). No repo
# clone, no build: the images are pulled from GHCR. Docs:
# https://docs.cobblr.xyz/self-hosting
#
# GENERATED from core deploy/selfhost/standalone/docker-compose.yml. Do not
# edit here; edit the canonical file and re-run `npm run sync:compose`.
#
# HTTPS is a compose profile: COMPOSE_PROFILES=caddy (default, set in .env)
# runs the bundled TLS proxy; empty runs bare, for `tailscale serve/funnel` on
# the host. All data lives in bind mounts under the data root (COBBLR_DATA_ROOT
# in the .env, default ./data). Set it to relocate every mount at once, onto a
# NAS or a data disk; copying that tree is a complete backup. Deliberately NO
# named volumes.
services:
  db:
    image: ghcr.io/cobblrhq/cobblr-db:${COBBLR_VERSION:-latest}
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-cobblr}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: ${POSTGRES_DB:-cobblr_meta}
    volumes:
      # One level up: PG18+ stores the cluster in a versioned subdirectory,
      # and the image upgrades an older one in place on boot, so a plain
      # `docker compose pull` carries you across major versions.
      - ${COBBLR_DATA_ROOT:-./data}/db:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cobblr} -d ${POSTGRES_DB:-cobblr_meta}"]
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped

  api:
    image: ghcr.io/cobblrhq/cobblr-api:${COBBLR_VERSION:-latest}
    env_file: .env # the whole config surface (secrets, email, privacy, …)
    environment:
      NODE_ENV: production
      # Derived from POSTGRES_* so the password lives in exactly ONE line of
      # .env. Using an external Postgres? Set DATABASE_URL /
      # SUPERUSER_DATABASE_URL in .env and they win over the derived values.
      DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-cobblr}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-cobblr_meta}}
      SUPERUSER_DATABASE_URL: ${SUPERUSER_DATABASE_URL:-postgres://${POSTGRES_USER:-cobblr}:${POSTGRES_PASSWORD}@db:5432/postgres}
      # Uploaded files + runtime-installed modules MUST land on the bind
      # mounts below - without these env vars the app writes to its defaults
      # inside the container's writable layer, and every `compose pull && up`
      # (the documented update) silently discards them. The mount target and
      # the env value must stay in lockstep; scripts/lint-selfhost-mounts.ts
      # enforces it in CI.
      COBBLR_FILES_ROOT: /var/cobblr/files
      COBBLR_RUNTIME_MODULES_DIR: /var/cobblr/sandboxed-modules
      # The tag this stack runs, passed through so /healthz and Health can
      # report it. Runtime-only on purpose: a stable cut re-tags a nightly
      # digest bit-for-bit, so a version can never be baked into the image.
      COBBLR_VERSION: ${COBBLR_VERSION:-}
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ${COBBLR_DATA_ROOT:-./data}/files:/var/cobblr/files
      - ${COBBLR_DATA_ROOT:-./data}/modules:/var/cobblr/sandboxed-modules
    restart: unless-stopped

  web:
    image: ghcr.io/cobblrhq/cobblr-web:${COBBLR_VERSION:-latest}
    depends_on:
      - api
    # Loopback ALWAYS, never on the LAN. Caddy proxies over the compose
    # network; tailscale serve/funnel forwards to 127.0.0.1 on the host; and
    # localhost stays available for on-box debugging in every mode. WEB_BIND
    # exists as an escape hatch; think twice before widening it.
    ports:
      - "${WEB_BIND:-127.0.0.1}:${WEB_PORT:-8088}:8080"
    restart: unless-stopped

  caddy:
    image: ghcr.io/cobblrhq/cobblr-caddy:${COBBLR_VERSION:-latest}
    profiles: ["caddy"]
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      # The four TLS variants are baked into the image; pick one:
      COBBLR_TLS_MODE: ${COBBLR_TLS_MODE:-duckdns} # duckdns | cloudflare | internal | tsnet
      # tsnet mode only: a reusable auth key; the proxy joins the tailnet itself.
      TS_AUTHKEY: ${TS_AUTHKEY:-}
      COBBLR_SITE_ADDRESS: ${COBBLR_SITE_ADDRESS:?set COBBLR_SITE_ADDRESS in .env}
      COBBLR_ACME_EMAIL: ${COBBLR_ACME_EMAIL:-}
      DUCKDNS_TOKEN: ${DUCKDNS_TOKEN:-}
      CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN:-}
    volumes:
      # certs + CA state survive restarts (never re-request on boot)
      - ${COBBLR_DATA_ROOT:-./data}/caddy:/data
      - ${COBBLR_DATA_ROOT:-./data}/caddy-config:/config
    depends_on:
      - web
    restart: unless-stopped
