Repository navigation
Expand file tree
/
Copy pathdocker-compose.dev.yml
More file actions
209 lines (197 loc) · 8.87 KB
/
Copy pathdocker-compose.dev.yml
File metadata and controls
209 lines (197 loc) · 8.87 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
# Full dev stack: Postgres, Redis, migrations, api, worker, the SPA, the docs
# site, and a mail catcher.
#
# docker compose -f docker-compose.dev.yml up
#
# No local Go, Node, or make required — the repo is bind-mounted and rebuilt
# inside the containers on change (air for Go, Vite HMR for the SPA). Ctrl+C
# stops everything.
#
# Transactional email (verify, password reset, login codes, invites) is sent to
# Mailpit instead of being logged: read it at http://localhost:8025. Nothing
# leaves the machine, and the message bodies — which carry single-use links and
# login codes — stay out of the logs.
#
# Dev credentials are hardcoded on purpose. This file must never run anything
# reachable from the internet: the JWT secret and master key below are public
# knowledge. Production is the repo-root docker-compose.yml, which generates
# real random secrets on first boot (init-secrets) or takes them from the
# environment — never a fixed default.
# Stable project name: without it the project is named after the manifest's
# directory, so moving this file (it used to live in deploy/compose/) would
# silently orphan the previous stack's containers and volumes.
name: inroad-dev
x-go-env: &go-env
INROAD_DATABASE_URL: postgres://inroad:inroad@postgres:5432/inroad?sslmode=disable
INROAD_REDIS_ADDR: redis:6379
# Dev-only fixed values so the stack boots with zero setup.
INROAD_JWT_SECRET: dev-jwt-secret-not-for-production-use
# base64 of exactly 32 bytes ("dev-master-key-for-local-only!!!").
INROAD_MASTER_KEY: ZGV2LW1hc3Rlci1rZXktZm9yLWxvY2FsLW9ubHkhISE=
INROAD_PUBLIC_URL: http://localhost:5173
INROAD_LOG_LEVEL: debug
# Reach a mail catcher or a local Ollama without extra config.
INROAD_MAIL_ALLOW_PRIVATE_HOSTS: "true"
INROAD_AI_ALLOW_PRIVATE_BASE_URL: "true"
# Deliver transactional email to the Mailpit container (UI on :8025) so
# verification and reset links are actually readable in dev. Mailpit speaks
# plaintext and advertises no AUTH, hence the explicit cleartext opt-out —
# which defaults to false everywhere else, so it can only ever be on because
# a file like this one turned it on. Never set it in production.
INROAD_TRANSACTIONAL_DRIVER: smtp
INROAD_SYSTEM_SMTP_HOST: mailpit
INROAD_SYSTEM_SMTP_PORT: "1025"
INROAD_SYSTEM_SMTP_ALLOW_PLAINTEXT: "true"
INROAD_SYSTEM_EMAIL_FROM: no-reply@inroad.test
# Google / Microsoft OAuth (optional). This file lives at the repo root, so
# compose auto-loads the root .env for these interpolations — set the values
# there (or in the shell) and restart; no --env-file flag needed. Blank keeps
# the provider disabled; redirect URLs default in-app to INROAD_PUBLIC_URL-
# derived paths, and the app treats empty as unset. The dev DSN and secrets
# above are pinned literals, so a native-dev .env can never leak those in.
INROAD_GOOGLE_CLIENT_ID: ${INROAD_GOOGLE_CLIENT_ID:-}
INROAD_GOOGLE_CLIENT_SECRET: ${INROAD_GOOGLE_CLIENT_SECRET:-}
INROAD_GOOGLE_REDIRECT_URL: ${INROAD_GOOGLE_REDIRECT_URL:-}
INROAD_GOOGLE_SIGNIN_CLIENT_ID: ${INROAD_GOOGLE_SIGNIN_CLIENT_ID:-}
INROAD_GOOGLE_SIGNIN_CLIENT_SECRET: ${INROAD_GOOGLE_SIGNIN_CLIENT_SECRET:-}
INROAD_GOOGLE_SIGNIN_REDIRECT_URL: ${INROAD_GOOGLE_SIGNIN_REDIRECT_URL:-}
INROAD_MS_CLIENT_ID: ${INROAD_MS_CLIENT_ID:-}
INROAD_MS_CLIENT_SECRET: ${INROAD_MS_CLIENT_SECRET:-}
INROAD_MS_REDIRECT_URL: ${INROAD_MS_REDIRECT_URL:-}
INROAD_MS_TENANT: ${INROAD_MS_TENANT:-common}
x-go-build: &go-build
build:
context: .
dockerfile: deploy/docker/Dockerfile.dev-go
volumes:
- .:/src
# Named volumes for the module and build caches. Without these, every
# container restart recompiles the whole dependency graph — the difference
# between a 5-second and a 3-minute restart.
- go-mod:/go/pkg/mod
- go-build:/root/.cache/go-build
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: inroad
POSTGRES_PASSWORD: inroad
POSTGRES_DB: inroad
ports: ["5433:5432"] # host 5433 avoids clashing with a native Postgres on 5432
# Stock max_connections is 100, and this stack already spends ~50 of them on
# the api and worker pools before a developer runs anything. The integration
# suite then adds four packages' worth of pools against the same server; at
# the default it ran out and failed as "sorry, too many clients already" in
# whichever package asked last. `command` must repeat `postgres` because the
# image's entrypoint passes it through to the server.
command: postgres -c max_connections=300
volumes:
- pgdata:/var/lib/postgresql/data
# Integration tests expect a separate database; create it on first boot
# so `go test -tags=integration` works against this stack too.
- ./deploy/docker/initdb-test-db.sh:/docker-entrypoint-initdb.d/10-test-db.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U inroad"]
interval: 3s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
ports: ["6379:6379"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 3s
retries: 10
# Catches every transactional email the stack sends and serves it at
# http://localhost:8025. Holds messages in memory only — nothing is persisted
# and nothing is relayed onward, so a dev signup can never email a real
# address.
mailpit:
image: axllent/mailpit:latest
ports:
- "8025:8025" # web UI
- "1025:1025" # SMTP
healthcheck:
test: ["CMD", "/mailpit", "readyz"]
interval: 3s
timeout: 3s
retries: 10
# One-shot: applies migrations then exits 0. api and worker wait for it to
# complete, so neither ever starts against a schema behind the code.
migrate:
<<: *go-build
environment: *go-env
# Clear the production entrypoint so dev runs migrations through Go.
entrypoint: []
command: go run ./cmd/migrate up
depends_on:
postgres: { condition: service_healthy }
restart: "no"
api:
<<: *go-build
environment: *go-env
# air only mkdirs one level, and tmp/ is gitignored so it may not exist.
# tmp/ lives on the bind mount, so it OUTLIVES the container: after an
# unclean stop (clean_on_exit never ran) a binary from the previous session
# is still there. Delete it, so the first build either produces a fresh one
# or leaves nothing to run — never yesterday's code.
command: sh -c "mkdir -p tmp/air-api && rm -f tmp/air-api/inroad tmp/air-api/inroad.next && air -c .air.api.toml"
ports: ["8080:8080"]
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
mailpit: { condition: service_healthy }
migrate: { condition: service_completed_successfully }
worker:
<<: *go-build
environment: *go-env
# Same stale-binary reset as the api above.
command: sh -c "mkdir -p tmp/air-worker && rm -f tmp/air-worker/worker tmp/air-worker/worker.next && air -c .air.worker.toml"
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
migrate: { condition: service_completed_successfully }
web:
build:
context: .
dockerfile: deploy/docker/Dockerfile.dev-web
volumes:
- ./web:/web
- ./api:/api # openapi.yaml, for `npm run gen:api`
# Anonymous volume shadows the bind mount so the container keeps its own
# node_modules: host binaries are the wrong platform, and a bind-mounted
# node_modules is pathologically slow on Windows and macOS.
- /web/node_modules
ports: ["5173:5173"]
environment:
# Vite proxies /api to the api container rather than localhost.
VITE_API_PROXY_TARGET: http://api:8080
# "Docs & MCP" links go to the docs container (browser-side URL, so
# localhost, not the service name).
VITE_DOCS_URL: http://localhost:4321
# Bind-mount file events are not delivered natively on Windows/macOS;
# polling is what actually makes HMR fire.
CHOKIDAR_USEPOLLING: "true"
depends_on:
api: { condition: service_started }
# The Astro/Starlight docs site (docs/), served on :4321. The SPA never
# renders documentation itself — its "Docs & MCP" links point here.
docs:
image: node:22-alpine
working_dir: /docs
# astro dev writes a PID lock into the bind-mounted tree (.astro/dev.json),
# so a container that died uncleanly blocks the next boot with "another
# astro dev is already running". Deleting it is the safe reset; `--force`
# is NOT — the stale PID can match a live process in the fresh container,
# and astro then SIGTERMs its own process tree.
command: sh -c "npm ci && rm -f .astro/dev.json && npm run dev -- --host --port 4321"
volumes:
- ./docs:/docs
# Container-private node_modules for the same platform reasons as web.
- /docs/node_modules
ports: ["4321:4321"]
volumes:
pgdata:
go-mod:
go-build: