-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompose.dev.yaml
More file actions
154 lines (149 loc) · 7.51 KB
/
Copy pathcompose.dev.yaml
File metadata and controls
154 lines (149 loc) · 7.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# ============================================================================
# THIS IS FOR LOCAL DEVELOPMENT ONLY
# ============================================================================
# Requires .env file with secrets - see .env.example
#
# Database credentials come from a single source: DB_USER / DB_PASSWORD /
# DB_NAME in .env. The Postgres container reads the same variables, so there
# is no separate POSTGRES_* set to keep in sync.
services:
db:
# Pinned to the MULTI-ARCH manifest-list digest (amd64 + arm64), not a
# per-arch digest, so this dev stack still comes up on both. Tag mutable,
# digest immutable. To bump: re-resolve the tag's manifest-list digest.
image: postgres@sha256:e013e867e712fec275706a6c51c966f0bb0c93cfa8f51000f85a15f9865a28cb # postgres:16-alpine
container_name: app_db
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
TZ: ${TZ:-Australia/Melbourne}
volumes:
- postgres_data:/var/lib/postgresql/data:Z
# Published ports are bound to 127.0.0.1 so the dev database is reachable from
# the host (psql, integration tests) but NOT from other machines on the LAN /
# any wider network. Container-to-container traffic uses the compose network
# (service name `db:5432`), not the published port, so this does not affect it.
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 5s
retries: 5
app:
build:
context: .
dockerfile: container.dev
container_name: app_api
volumes:
- .:/app:Z
- go_modules:/go/pkg/mod:Z
ports:
# Both sides track $PORT / $METRICS_PORT so a non-default port in .env
# binds the same port inside the container as it is published on. The
# container HEALTHCHECK already tracks $PORT via config.ResolvePort.
# Bound to 127.0.0.1: this is a LOCAL-DEV stack, so it must not be exposed
# to the LAN. The METRICS_PORT admin listener has NO auth (restricted at the
# infra layer in prod), so loopback-only is especially important for it here.
- "127.0.0.1:${PORT:-8080}:${PORT:-8080}"
- "127.0.0.1:${METRICS_PORT:-9090}:${METRICS_PORT:-9090}"
# Wholesale pass-through of .env so EVERY documented variable reaches the
# container — no enumerated list to drift out of sync with .env.example
# (previously TRUST_PROXY_HEADERS, RATE_LIMIT_RPM, PUBLIC_READINESS, the
# SERVER_*_TIMEOUT_SECS and the DB pool/lifetime vars were silently dropped).
# This also forwards the MIGRATOR_* vars, so `make db-migrate` (an exec
# inside this container) now honours MIGRATOR_LOCK_TIMEOUT /
# MIGRATOR_STATEMENT_TIMEOUT from .env — a correctness gain, not a
# regression. `environment` entries override `env_file`, so the TZ default
# below still applies when TZ is unset.
env_file:
- .env
environment:
TZ: ${TZ:-Australia/Melbourne}
# Point the app at the Collector so `make run` yields a working pipeline
# with no extra steps, even if .env predates the OTEL_* variables. A value
# in .env (loaded by compose for interpolation) still wins via the :-
# default. Environment overrides the env_file passthrough either way.
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
depends_on:
db:
condition: service_healthy
otel-collector:
condition: service_started
opa:
condition: service_healthy
# OpenTelemetry Collector: receives OTLP from the app, batches, exports to a
# debug logger, and re-exposes Prometheus on :8889 for local scraping. See
# deploy/otel-collector-config.yaml. Runs as a SEPARATE process (never inside
# the app image), mirroring the sidecar/agent/gateway deployment model.
otel-collector:
# MULTI-ARCH manifest-list digest (amd64 + arm64). Tag mutable, digest
# immutable. To bump: re-resolve the tag's manifest-list digest.
image: otel/opentelemetry-collector-contrib@sha256:4935caa35e9a4cb387e35732e8fb22b2b5759af8d12e7043357f03837f6e8df5 # otel/opentelemetry-collector-contrib:0.155.0
container_name: app_otel_collector
command: ["--config=/etc/otelcol-contrib/config.yaml"]
volumes:
- ./deploy/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:Z
# Loopback-only: the app exports to the Collector over the compose network
# (otel-collector:4318), so these published ports exist only for host-side
# debugging (scraping :8889) and must not be exposed beyond localhost.
ports:
- "127.0.0.1:4317:4317" # OTLP gRPC receiver
- "127.0.0.1:4318:4318" # OTLP HTTP receiver (the app exports here)
- "127.0.0.1:8889:8889" # Prometheus exporter, scrape metrics at localhost:8889/metrics
# Open Policy Agent (OPA) sidecar: LAYER 2 authorisation. The app POSTs the
# decision input to OPA's REST Data API and enforces the boolean result, failing
# closed (see internal/middleware/authz_middleware.go). Runs as a SEPARATE
# process — the sidecar model, mirroring the otel-collector service above, never
# inside the app image. The standard openpolicyagent/opa image is used (NOT the
# -envoy variant): the app calls OPA's REST API directly, so no Envoy is in the
# path. The `-static` (distroless) tag is pinned because it is multi-arch
# (amd64 + arm64) — the plain `1.9.0` tag is amd64-only and will not run on
# arm64 hosts (Apple Silicon / arm VMs).
#
# LOCAL-DEV policy source: the policy is bind-mounted read-only from
# deploy/opa/. In PRODUCTION, serve policy as a signed BUNDLE from a remote
# bundle server instead of a volume mount (OPA guidance).
#
# AUTH IS OPT-IN even in local dev, so `make run` works out of the box: the app
# only calls OPA when OPA_URL is set (see .env.example, where OPA_URL is
# commented and points at this service — http://opa:8181). Enabling
# authorisation (OPA) WITHOUT authentication (OIDC) means every request arrives
# with no roles and the default-deny policy returns 403, so to exercise the two
# layers uncomment BOTH the OIDC_* variables and OPA_URL in .env.
opa:
# MULTI-ARCH manifest-list digest (amd64 + arm64). Tag mutable, digest
# immutable. To bump: re-resolve the tag's manifest-list digest.
image: openpolicyagent/opa@sha256:60b6af32b58377718546ac7d4634eecbfe50ec36f7d3ca3f8ebf515f9826c2ac # openpolicyagent/opa:1.9.0-static
container_name: app_opa
command:
- "run"
- "--server"
- "--addr=0.0.0.0:8181"
- "/policies"
volumes:
- ./deploy/opa:/policies:ro,Z
# Loopback-only: the app calls OPA over the compose network (opa:8181); this
# published port is only for host-side decision debugging. OPA has no auth, so
# it must never be reachable beyond localhost.
ports:
# OPA REST API. Query a decision from the host with, e.g.:
# curl localhost:8181/v1/data/api/authz/allow -d '{"input":{"roles":["admin"]}}'
- "127.0.0.1:8181:8181"
healthcheck:
# The default opa image is distroless (no shell / curl), so probe the
# server's GET /health using OPA's OWN binary via http.send. --fail makes an
# unreachable server or a non-200 response exit non-zero.
test:
- "CMD"
- "/opa"
- "eval"
- "--fail"
- 'x := http.send({"method":"GET","url":"http://127.0.0.1:8181/health","raise_error":false}); x.status_code == 200'
interval: 5s
timeout: 3s
retries: 5
volumes:
postgres_data:
go_modules: