forked from SShadowS/DevOpsWorker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
223 lines (216 loc) · 9.39 KB
/
Copy pathdocker-compose.yml
File metadata and controls
223 lines (216 loc) · 9.39 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: pipeline
POSTGRES_USER: pipeline
POSTGRES_PASSWORD: ${PG_PASSWORD:-pipeline}
volumes:
- pgdata:/var/lib/postgresql/data
ports:
# Loopback only. Anyone who can write to this database can insert a row
# into `sessions` and mint themselves an admin cookie, which bypasses the
# dashboard login entirely. Nothing in the stack needs the port published
# to the network: the other services and every spawned container reach
# PostgreSQL by service name over pipeline-net, and host tools connect
# over loopback.
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pipeline"]
interval: 5s
timeout: 3s
retries: 5
networks:
- pipeline-net
watcher:
build:
context: .
# Build provenance (see Dockerfile). Compose cannot run git, so a plain
# `docker compose build` bakes the literal string "unknown" rather than
# a stale or fabricated sha. Pass the real sha with:
# BUILD_SHA=$(git rev-parse --short HEAD) docker compose build
# Leaving it unset is not neutral: the stale-image guard in
# .claude/hooks/docker-guard.ts cannot compare an unstamped image against HEAD.
# On a composed deployment (private/ present) it does not go silent about
# that — it blocks `compose up` and asks you to stamp it. A generic clone
# without private/ stays unblocked, since it never opted into the practice.
args:
BUILD_SHA: ${BUILD_SHA:-unknown}
restart: unless-stopped
depends_on:
postgres: { condition: service_healthy }
entrypoint: ["/root/.bun/bin/bun", "run", "src/cli/index.ts"]
command: ["watch"]
user: root
env_file: .env
environment:
DATABASE_URL: postgres://pipeline:${PG_PASSWORD:-pipeline}@postgres:5432/pipeline
# No-post replay control (empty = post normally in production; set to 1 for measurement replays).
# Takes precedence over any PR_REVIEW_NO_POST in .env. Forwarded into spawned review containers
# by getPrReviewContainerEnv() in src/cli/watch/container-dispatcher.ts.
PR_REVIEW_NO_POST: ${PR_REVIEW_NO_POST:-}
# ABSOLUTE host path to this repo's private/ overlay. The watcher passes it as the
# bind-mount source when spawning pipeline/review containers (docker.ts), so they
# load the same overlay. Leave empty to run spawned containers generic. Set in .env.
HOST_PRIVATE_DIR: ${HOST_PRIVATE_DIR:-}
volumes:
- do-pipeline-state:/state
- /var/run/docker.sock:/var/run/docker.sock
# Private overlay (gitignored) injected read-only at runtime. The image stays
# generic/public-safe; the proprietary registry/env/prompts load from here.
- ./private:/app/private:ro
networks:
- pipeline-net
dashboard:
build:
context: .
args:
BUILD_SHA: ${BUILD_SHA:-unknown}
restart: unless-stopped
depends_on:
postgres: { condition: service_healthy }
entrypoint: ["/root/.bun/bin/bun", "run", "src/cli/index.ts"]
command: ["dashboard"]
user: root
env_file: .env
environment:
DATABASE_URL: postgres://pipeline:${PG_PASSWORD:-pipeline}@postgres:5432/pipeline
# Mark the session cookie Secure now that Caddy terminates TLS in front
# of this service (see the `caddy` service below) — without this the
# cookie would still be sendable over plain HTTP even though HTTPS is
# available. See .env.example for the longer explanation.
DASHBOARD_SECURE_COOKIES: "1"
# Trust the X-Forwarded-For header Caddy sets on every request it
# proxies here, for the login rate limiter's key and the auth_events.ip
# column — see resolveClientIp() in src/auth/http.ts. Safe specifically
# because Caddy is the only thing that can reach this service (no
# ports: below) and Caddy itself sets/overwrites that header from the
# real connection rather than forwarding whatever a client already put
# in it. Do not set this if the dashboard is ever reachable any other
# way.
TRUST_PROXY_HEADERS: "1"
# No ports: mapping. The dashboard is reachable only through the `caddy`
# service below, over pipeline-net by service name — never directly from
# the host or the wider network. This is the fix for the gap the caddy
# service exists to close: this port used to publish "3000:3000" to ALL
# interfaces (unlike postgres below, which is loopback-scoped), so the
# login cookie could cross the LAN in clear text. If you need direct
# access for local debugging, scope it to loopback the way postgres does
# — do not republish it to every interface again.
volumes:
- ./private:/app/private:ro
# Read-only so the status ribbon can resolve a LIVE HEAD (src/dashboard/stats.ts
# resolveHeadSha) instead of the hardcoded "not observable" placeholder. The
# watcher/dashboard are themselves compose services, so this is what makes the
# ribbon able to detect compose drifting from source at all — see task-5-report.md.
- ./.git:/repo/.git:ro
networks:
- pipeline-net
# Terminates TLS in front of the dashboard with Caddy's internal CA — see
# the Caddyfile at the repo root for the reasoning (SSE flush, Host header)
# and private/internal-docs/operator-runbook.md (overlay) for what a
# client needs to do once to trust Caddy's root certificate.
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- dashboard
environment:
# Hostname clients use to reach the dashboard. Override per deployment;
# the default below is a placeholder, not a real host — see Caddyfile.
DASHBOARD_HOSTNAME: ${DASHBOARD_HOSTNAME:-dashboard.localhost}
ports:
# 443 only — no 80. `tls internal` never needs port 80 for an ACME
# challenge, so leaving 80 unpublished means there is no way to reach
# this stack over plain HTTP by mistake, ever. (Caddy still configures
# an automatic HTTP->HTTPS redirect listener on 80 internally; it's
# simply never reachable from outside the container.)
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
# /data is what makes the internal CA survive a restart — it holds the
# root certificate and key Caddy minted the first time it ran. Losing
# this volume mints a NEW root, and every client that already trusted
# the old one has to re-trust the new one. /config just persists
# Caddy's autosaved running config between restarts; not security
# sensitive, kept for continuity.
- caddy_data:/data
- caddy_config:/config
healthcheck:
# Checks Caddy's own admin API (plain HTTP, container-local only) is
# up and serving a loaded config — the same "is the daemon alive"
# level of check as postgres's pg_isready below, not an end-to-end
# probe of the HTTPS site. An end-to-end probe isn't practical here:
# busybox wget's minimal TLS stack cannot complete a handshake against
# Caddy's own HTTPS listener (verified while building this — it fails
# the TLS handshake outright, unrelated to certificate trust).
test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:2019/config/ || exit 1"]
interval: 5s
timeout: 3s
retries: 5
networks:
- pipeline-net
webhook-server:
build:
context: .
args:
BUILD_SHA: ${BUILD_SHA:-unknown}
restart: unless-stopped
depends_on:
postgres: { condition: service_healthy }
entrypoint: ["/root/.bun/bin/bun", "run", "src/cli/index.ts"]
command: ["webhook-server"]
user: root
env_file: .env
environment:
DATABASE_URL: postgres://pipeline:${PG_PASSWORD:-pipeline}@postgres:5432/pipeline
ports:
- "3001:3002"
volumes:
- ./private:/app/private:ro
networks:
- pipeline-net
pg-backup:
image: postgres:17-alpine
depends_on:
postgres: { condition: service_healthy }
environment:
PGPASSWORD: ${PG_PASSWORD:-pipeline}
volumes:
- ./backups:/backups
entrypoint: /bin/sh
command:
- -c
- |
echo "pg-backup: starting (daily backups, keeping 7 days)"
while true; do
# Wait until postgres accepts connections before attempting a dump.
until pg_isready -h postgres -U pipeline -d pipeline -t 5; do
echo "pg-backup: postgres not ready, retrying in 10s"
sleep 10
done
STAMP=$$(date +%Y%m%d-%H%M%S)
TMPFILE=/backups/.pipeline-$$STAMP.sql.gz.tmp
if pg_dump -h postgres -U pipeline pipeline | gzip > "$$TMPFILE" && [ "$$(stat -c%s "$$TMPFILE")" -gt 1024 ]; then
mv "$$TMPFILE" "/backups/pipeline-$$STAMP.sql.gz"
echo "pg-backup: created pipeline-$$STAMP.sql.gz ($$(stat -c%s "/backups/pipeline-$$STAMP.sql.gz") bytes)"
else
rm -f "$$TMPFILE"
echo "pg-backup: ERROR — pg_dump failed or output too small (<1KB), no backup written"
fi
# Remove backups older than 7 days
find /backups -name 'pipeline-*.sql.gz' -mtime +7 -delete
sleep 86400
done
restart: unless-stopped
networks:
- pipeline-net
networks:
pipeline-net:
name: pipeline-net
volumes:
pgdata:
do-pipeline-state:
caddy_data:
caddy_config: