docker-compose.server.yml
Rama main · réplica de lectura del repositorio original.
# =============================================================================
# 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: