151 lines
6.5 KiB
YAML
151 lines
6.5 KiB
YAML
# ============================================================================
|
|
# Krow API — production stack
|
|
#
|
|
# cd infrastructure
|
|
# cp .env.docker.example .env
|
|
# docker compose up -d --build
|
|
#
|
|
# This file assumes a MANAGED PostgreSQL (RDS, Cloud SQL, Neon, a DBA's
|
|
# cluster) reachable from wherever these containers run. That is deliberate:
|
|
# a production database wants backups, point-in-time recovery, failover and a
|
|
# patch schedule, none of which a container in this file provides.
|
|
#
|
|
# To run PostgreSQL alongside the API — for staging, or to test the stack
|
|
# end-to-end on one host — layer the override on top:
|
|
#
|
|
# docker compose -f docker-compose.yml -f docker-compose.local-db.yml up -d
|
|
#
|
|
# ── ORDER OF OPERATIONS ─────────────────────────────────────────────────────
|
|
#
|
|
# `migrate` runs to completion BEFORE `api` starts, and `api` will not start if
|
|
# it fails. That is the expand/migrate/contract discipline from README.md
|
|
# expressed in the dependency graph rather than in a runbook: schema first,
|
|
# binary second, and every migration backwards-compatible with the version
|
|
# still running.
|
|
#
|
|
# ── TLS IS NOT OPTIONAL HERE ────────────────────────────────────────────────
|
|
#
|
|
# internal/config rejects DATABASE_SSLMODE=disable when APP_ENV=production, so
|
|
# this stack will refuse to start against a database that cannot do TLS. That
|
|
# is the intended behaviour and not something to work around by dropping to
|
|
# APP_ENV=staging — a staging flag on a production deployment is a lie the next
|
|
# person has to discover.
|
|
# ============================================================================
|
|
|
|
name: krow
|
|
|
|
# The database settings the API and the migration runner must agree on.
|
|
x-database-env: &database-env
|
|
DATABASE_HOST: ${DATABASE_HOST:?DATABASE_HOST is required}
|
|
DATABASE_PORT: ${DATABASE_PORT:-5432}
|
|
DATABASE_NAME: ${DATABASE_NAME:?DATABASE_NAME is required}
|
|
DATABASE_USER: ${DATABASE_USER:?DATABASE_USER is required}
|
|
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD is required}
|
|
DATABASE_SCHEMA: ${DATABASE_SCHEMA:-public}
|
|
DATABASE_SSLMODE: ${DATABASE_SSLMODE:-require}
|
|
|
|
x-logging: &logging
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "5"
|
|
|
|
services:
|
|
# ── Schema ────────────────────────────────────────────────────────────────
|
|
# One-shot. Exits 0 when the schema is current, including when there was
|
|
# nothing to apply. `api` waits on that exit code.
|
|
#
|
|
# DATABASE_URL is a single pre-built string rather than assembled from the
|
|
# parts above because golang-migrate takes a URL, and a password containing
|
|
# @ : / ? or # must be percent-encoded in one. Compose cannot encode it, so
|
|
# the encoding is the operator's job — see .env.docker.example.
|
|
migrate:
|
|
image: migrate/migrate:v4.19.0
|
|
container_name: krow-migrate
|
|
volumes:
|
|
- ../migrations:/migrations:ro
|
|
command:
|
|
- -path=/migrations
|
|
- -database=${DATABASE_URL:?DATABASE_URL is required — see .env.docker.example}
|
|
- up
|
|
restart: "no"
|
|
logging: *logging
|
|
networks: [krow]
|
|
|
|
# ── API ───────────────────────────────────────────────────────────────────
|
|
api:
|
|
build:
|
|
context: ..
|
|
dockerfile: infrastructure/Dockerfile.api
|
|
args:
|
|
VERSION: ${VERSION:-dev}
|
|
image: ${IMAGE:-doormile/krowbackend:latest}
|
|
container_name: krow-api
|
|
depends_on:
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
environment:
|
|
<<: *database-env
|
|
APP_ENV: ${APP_ENV:-production}
|
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
# 0.0.0.0 so the published port actually reaches the process. The binary
|
|
# defaults to 127.0.0.1, which inside a container means "nobody".
|
|
HTTP_HOST: 0.0.0.0
|
|
HTTP_PORT: "8080"
|
|
HTTP_READ_TIMEOUT: ${HTTP_READ_TIMEOUT:-15s}
|
|
HTTP_WRITE_TIMEOUT: ${HTTP_WRITE_TIMEOUT:-30s}
|
|
HTTP_IDLE_TIMEOUT: ${HTTP_IDLE_TIMEOUT:-60s}
|
|
HTTP_SHUTDOWN_TIMEOUT: ${HTTP_SHUTDOWN_TIMEOUT:-10s}
|
|
# Browser origins allowed to call this API. Empty means same-origin only,
|
|
# and the CORS middleware is then not installed at all. Cookies are the
|
|
# auth mechanism, so any origin listed here can hold a session.
|
|
#
|
|
# "*" is refused at startup: browsers reject Allow-Origin "*" together
|
|
# with credentials, so it would break authenticated calls rather than
|
|
# loosen anything.
|
|
HTTP_CORS_ORIGINS: ${HTTP_CORS_ORIGINS:-}
|
|
# lax | none | strict, or unset to let the server choose.
|
|
#
|
|
# "none" is required when the frontend is on a different registrable
|
|
# DOMAIN from the API — otherwise the browser withholds the cookie
|
|
# however correct the CORS headers are. A different ORIGIN on the same
|
|
# domain (platform.krowforce.com → mcp.krowforce.com) needs CORS but not
|
|
# this: a Lax cookie already travels between them, and "none" would give
|
|
# up the only CSRF protection this API has.
|
|
#
|
|
# Unset, the server derives it from HTTP_CORS_ORIGINS. The default below
|
|
# is deliberate: a compose deployment keeps Lax unless told otherwise.
|
|
HTTP_COOKIE_SAMESITE: ${HTTP_COOKIE_SAMESITE:-lax}
|
|
DATABASE_MAX_OPEN_CONNS: ${DATABASE_MAX_OPEN_CONNS:-25}
|
|
DATABASE_MIN_IDLE_CONNS: ${DATABASE_MIN_IDLE_CONNS:-2}
|
|
DATABASE_CONN_MAX_LIFETIME: ${DATABASE_CONN_MAX_LIFETIME:-30m}
|
|
DATABASE_CONNECT_TIMEOUT: ${DATABASE_CONNECT_TIMEOUT:-10s}
|
|
DATABASE_STATEMENT_TIMEOUT: ${DATABASE_STATEMENT_TIMEOUT:-10s}
|
|
ports:
|
|
# Bound to loopback by default. A reverse proxy in front of this is what
|
|
# terminates TLS; publishing 0.0.0.0:8080 would put a plain-HTTP service
|
|
# carrying session cookies straight onto the network.
|
|
- "${API_BIND:-127.0.0.1}:${API_PORT:-8080}:8080"
|
|
restart: unless-stopped
|
|
stop_grace_period: 30s
|
|
read_only: true
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
tmpfs:
|
|
- /tmp:size=32m
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: "${API_CPU_LIMIT:-2}"
|
|
memory: ${API_MEMORY_LIMIT:-512M}
|
|
reservations:
|
|
memory: 128M
|
|
logging: *logging
|
|
networks: [krow]
|
|
|
|
networks:
|
|
krow:
|
|
driver: bridge
|