# A database-backed application: the stack that needs more than "sync and up". # # Every credential comes from .env, with no defaults. `${VAR:?message}` makes # Compose refuse to start and say which variable is missing, which is strictly # better than booting with an empty password. .env lives on the host and is never # synced from this repository, so ansible/playbooks/webapp.yml asserts it is # there before doing anything. services: db: container_name: webapp-db image: postgres:17-alpine restart: unless-stopped # ./pgdata is deliberately NOT pre-created by the playbook: Postgres refuses # to start unless its data directory is 0700 or 0750, and the image gets that # right on first start. See "Common mistakes" in skills/adding-a-stack. volumes: - ./pgdata:/var/lib/postgresql/data environment: POSTGRES_DB: webapp POSTGRES_USER: webapp POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set it in .env on the host} healthcheck: test: ["CMD-SHELL", "pg_isready -U webapp"] interval: 10s timeout: 5s retries: 5 networks: - backend web: container_name: webapp # Built from the payload rather than pulled. stack_build in the playbook # decides when a rebuild happens: on an update run, not on every deploy. build: context: ./app image: example/webapp:local restart: unless-stopped depends_on: db: condition: service_healthy environment: DATABASE_URL: postgres://webapp:${POSTGRES_PASSWORD:?set it in .env on the host}@db:5432/webapp SECRET_KEY: ${SECRET_KEY:?set it in .env on the host} # Set by the role to stack_dest, so a path handed to a sibling container is # the path on the host and not wherever the SSH session happened to land. UPLOAD_HOST_DIR: ${PWD}/uploads VIRTUAL_HOST: app.example.com VIRTUAL_PORT: "8080" LETSENCRYPT_HOST: app.example.com volumes: - ./uploads:/var/lib/webapp/uploads networks: - backend - proxy-net networks: backend: proxy-net: external: true