Ctrl K
ComunidadGuías…

docker-compose.server.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# =============================================================================
# ProjectAchilles — Public / on-prem single-server stack
# =============================================================================
# Purpose-built compose file for a publicly-reachable OR on-prem install behind
# a Caddy reverse proxy that terminates TLS. Unlike docker-compose.yml (which
# binds app ports to 127.0.0.1 for local dev), here ONLY Caddy is exposed to the
# network; backend, frontend, and Elasticsearch stay on the internal compose
# network.
#
# Driven by scripts/deploy-server.sh, which generates the root .env and renders
# deploy/caddy/rendered/{tls,global,sites-extra}.caddy (gitignored). Run
# ./scripts/deploy-server.sh first (and again after every `git pull`, so the
# rendered snippets match the checkout); after that you can also run compose
# directly:
#
#   docker compose -f docker-compose.server.yml up -d --build
#   docker compose -f docker-compose.server.yml --profile elasticsearch up -d --build
#
# All ${VARS} below are read from the root .env file written by the deploy script.
# =============================================================================

services:
  caddy:
    image: ${CADDY_IMAGE:-caddy:2.10.0-alpine}
    restart: unless-stopped
    # CADDY_BIND_IP pins the published ports to one host address (the LAN IP
    # for an on-prem install). Unset or empty = no host IP at all, so Docker
    # publishes on every IPv4 AND IPv6 address exactly as before (an explicit
    # 0.0.0.0 would drop the [::] listeners). Bracket an IPv6 address: [fd00::1].
    ports:
      - "${CADDY_BIND_IP:+${CADDY_BIND_IP}:}80:80"
      - "${CADDY_BIND_IP:+${CADDY_BIND_IP}:}443:443"
      - "${CADDY_BIND_IP:+${CADDY_BIND_IP}:}443:443/udp"   # HTTP/3
    environment:
      - ACHILLES_DOMAIN=${ACHILLES_DOMAIN:?set ACHILLES_DOMAIN in .env}
      - ACME_EMAIL=${ACME_EMAIL:-}
      - CADDY_DNS_PROVIDER=${CADDY_DNS_PROVIDER:-}
      - CADDY_DNS_TOKEN=${CADDY_DNS_TOKEN:-}
    volumes:
      - ./deploy/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - ./deploy/caddy/rendered:/etc/caddy/rendered:ro
      - ./deploy/caddy/certs:/etc/caddy/certs:ro
      # TLS_MODE=achilles-ca: ACHILLES_CA_DIR=/etc/projectachilles/ca. Mount
      # DIRECTORIES, not files: renewal swaps files by rename, and a file bind
      # mount would keep serving the old inode. caddy/ holds only root.crt and
      # the intermediate cert+key; the root KEY is never mounted. public/ is the
      # root (PEM + DER) served over plain HTTP at /ca/. Other modes mount the
      # empty committed deploy/caddy/no-ca.
      - ${ACHILLES_CA_DIR:-./deploy/caddy/no-ca}/caddy:/etc/caddy/ca:ro
      - ${ACHILLES_CA_DIR:-./deploy/caddy/no-ca}/public:/srv/ca:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      frontend:
        condition: service_started
    healthcheck:
      # TCP-level: an HTTP probe from inside the container gets the catch-all
      # 308 to https://localhost, which has no cert, in every TLS mode.
      test: ["CMD-SHELL", "nc -z -w 3 127.0.0.1 443 && nc -z -w 3 127.0.0.1 80 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3

  backend:
    # The commit label lets scripts/deploy-server.sh reuse images built from
    # the checked-out commit (a pre-built appliance) instead of rebuilding.
    build:
      context: ./backend
      args:
        GIT_COMMIT: "${ACHILLES_GIT_COMMIT:-}"
      labels:
        io.projectachilles.commit: "${ACHILLES_GIT_COMMIT:-}"
    restart: unless-stopped
    expose:
      - "3000"
    # All app config (Clerk, secrets, CORS_ORIGIN, AGENT_SERVER_URL, and the
    # Elasticsearch settings — self-hosted node OR Elastic Cloud) is written to
    # backend/.env by scripts/deploy-server.sh. Single source of truth.
    env_file: ./backend/.env
    environment:
      - PORT=3000
      - NODE_ENV=production
      - AGENT_SOURCE_PATH=/agent-src
      # The checkout's .git, read-only: agent builds record the commit they
      # compiled ("Built from source … at <sha>"), so a build made before a
      # `git pull` shows the old commit.
      - AGENT_SOURCE_GIT_DIR=/agent-src-git
      # Keep Go's compiled-object cache in the go-cache volume (Go's default,
      # /root/.cache/go-build, is lost on every container recreate, so the next
      # build compiles everything again). Agent builds use it too.
      - GOCACHE=/root/go/build-cache
    volumes:
      - achilles-data:/root/.projectachilles
      - repo-cache:/app/data
      - go-cache:/root/go
      - ./agent:/agent-src:ro
      - ./.git:/agent-src-git:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  frontend:
    build:
      context: ./frontend
      labels:
        io.projectachilles.commit: "${ACHILLES_GIT_COMMIT:-}"
    restart: unless-stopped
    expose:
      - "80"
    environment:
      # Same-origin: empty VITE_API_URL makes the SPA call /api on this host,
      # which nginx proxies to backend:3000 internally.
      - VITE_API_URL=
      - CLERK_PUBLISHABLE_KEY=${CLERK_PUBLISHABLE_KEY:-}
      - VITE_CLERK_PUBLISHABLE_KEY=${VITE_CLERK_PUBLISHABLE_KEY:-${CLERK_PUBLISHABLE_KEY:-}}
    depends_on:
      backend:
        condition: service_healthy

  # ---------------------------------------------------------------------------
  # Optional self-hosted Elasticsearch (TLS_ES=self).
  # Activate with: --profile elasticsearch
  # Stays on the internal network only — never published to the host/internet.
  # Use Elastic Cloud instead by setting ELASTICSEARCH_CLOUD_ID/API_KEY in
  # backend/.env and omitting this profile.
  # ---------------------------------------------------------------------------
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
    profiles: ["elasticsearch"]
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      # Security disabled is acceptable here ONLY because ES is not exposed
      # outside the compose network. Do not publish port 9200.
      - xpack.security.enabled=false
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx1024m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    expose:
      - "9200"
    volumes:
      - esdata:/usr/share/elasticsearch/data
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
      interval: 15s
      timeout: 10s
      retries: 10
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 2G

  # One-shot seed: creates the index + ~1000 synthetic records, then exits.
  # Runs only with --profile elasticsearch, and only when SEED_ES=true (set in
  # the root .env). It is a no-op otherwise, so it is safe to leave enabled.
  # Compose re-runs it on every `up` (each re-install), so it skips an index
  # that already holds documents.
  es-seed:
    image: python:3.12-slim
    profiles: ["elasticsearch"]
    depends_on:
      elasticsearch:
        condition: service_healthy
    volumes:
      - ./scripts:/scripts:ro
    entrypoint: >
      sh -c "test \"${SEED_ES:-false}\" = \"true\" || { echo 'SEED_ES not set — skipping seed'; exit 0; } &&
             pip install -q 'elasticsearch==8.17.2' &&
             python /scripts/generate_synthetic_data.py -c 1000 -o /tmp/seed.ndjson -d 30 &&
             python /scripts/upload_to_elasticsearch.py -f /tmp/seed.ndjson --host http://elasticsearch:9200 --create-index --skip-if-populated"
    restart: "no"

volumes:
  achilles-data:
  repo-cache:
  go-cache:
  esdata:
  caddy-data:
  caddy-config: