-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
211 lines (203 loc) · 9.3 KB
/
Copy pathdocker-compose.yml
File metadata and controls
211 lines (203 loc) · 9.3 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
# SECURITY NOTES — read before running in any environment that isn't your
# laptop:
# 1. Run ./scripts/setup.sh first. It creates .env and fills in a random
# POSTGRES_PASSWORD and MANTIS_API_KEY_PEPPER. Both are required — the
# services below refuse to start without them (there are no insecure
# defaults to fall back on).
# 2. POSTGRES_PASSWORD (from .env) is the single source of truth for the
# database password; the mantis DATABASE_URL is derived from it below, so
# there is nothing to keep in sync.
# 3. MANTIS_API_KEY_PEPPER is REQUIRED. The mantis service will refuse to
# boot without it. Do NOT rotate it after the first key is minted —
# rotating invalidates every existing API key.
# 4. Postgres publishes NO host port and sits on an internal-only docker
# network (see `networks:` at the bottom) — it's reachable only by the
# mantis service, never from the host or the LAN.
# 5. Do NOT expose mantis over plain HTTP on a routable interface. Front it
# with a tunnel (tailscale / cloudflared profiles below) or a
# TLS-terminating reverse proxy. Over plain HTTP the API key
# (Authorization: Bearer) and the session cookie travel in cleartext.
services:
postgres:
# Pinned to a digest for reproducible pulls. Refresh with:
# docker buildx imagetools inspect <image:tag> --format '{{.Manifest.Digest}}'
# (Dependabot's docker ecosystem bumps these automatically.)
image: postgres:18-alpine@sha256:96d56f7f57c6aacd1fcb908bc83b345ec5f83231ee486dd66a1baadce274db88
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-mantis}
# Required — no insecure default. Generated into .env by ./scripts/setup.sh
# and the single source of truth for the DB password (mantis derives its
# DATABASE_URL from it below).
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required. Run ./scripts/setup.sh to generate one (or set it in .env).}
POSTGRES_DB: ${POSTGRES_DB:-mantis}
# postgres:18+ refuses to initialize when a volume is mounted directly at
# /var/lib/postgresql/data (the pre-18 path). Point PGDATA at a
# subdirectory of the mount so init succeeds while keeping the volume here.
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- mantis-pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mantis -d mantis"]
interval: 5s
timeout: 5s
retries: 10
# Intentionally NO `ports:` — Postgres is never published to the host. It
# lives only on the internal-only `db` network and is reachable as
# `postgres:5432` from the mantis service and nowhere else.
networks:
- db
mantis:
build:
context: .
dockerfile: docker/Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
# Optional: makes every var documented in .env.example reachable by the
# container without listing each one. Long form so an absent .env is fine.
env_file:
- path: .env
required: false
environment:
# Derived from POSTGRES_* so the password has a single source of truth
# (POSTGRES_PASSWORD in .env). The `@postgres` host is the internal
# docker-network DNS name, so this is correct inside the container
# regardless of any localhost DATABASE_URL kept in .env for
# run-from-source.
DATABASE_URL: postgres://${POSTGRES_USER:-mantis}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required. Run ./scripts/setup.sh to generate one (or set it in .env).}@postgres:5432/${POSTGRES_DB:-mantis}
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:3000}
# REQUIRED. Server refuses to boot without it. Generate with:
# openssl rand -base64 32
MANTIS_API_KEY_PEPPER: ${MANTIS_API_KEY_PEPPER:?MANTIS_API_KEY_PEPPER is required. Generate with `openssl rand -base64 32` and put it in .env.}
PUBLIC_ONLY_HOSTS: ${PUBLIC_ONLY_HOSTS:-}
PUBLIC_ONLY_ALLOW_HEALTH: ${PUBLIC_ONLY_ALLOW_HEALTH:-0}
PUBLIC_ONLY_ALLOW_INBOX: ${PUBLIC_ONLY_ALLOW_INBOX:-0}
SMTP_URL: ${SMTP_URL:-}
SMTP_FROM: ${SMTP_FROM:-}
BOOTSTRAP_API_KEY: ${BOOTSTRAP_API_KEY:-}
LOG_LEVEL: ${LOG_LEVEL:-info}
AUTO_MIGRATE: "1"
# Bind to localhost by default. The tunnel sidecars (tailscale, cloudflared)
# reach mantis via the docker network — they don't need the published port.
# Set MANTIS_BIND_HOST=0.0.0.0 to expose to LAN when running without a tunnel
# — but only behind a TLS-terminating proxy. Never serve mantis over plain
# HTTP on a routable interface: the API key and session cookie travel in
# cleartext.
ports:
- "${MANTIS_BIND_HOST:-127.0.0.1}:${MANTIS_HOST_PORT:-3000}:3000"
networks:
# `default` carries inbound (published port / tunnels) and the app's
# outbound calls (webhooks, SMTP, APNs); `db` is the internal-only link to
# Postgres. mantis is the only service on both.
- default
- db
# --- Tailscale profile -----------------------------------------------------
# Enables: docker compose --profile tailscale up
# Requires: TS_AUTHKEY env var (from https://login.tailscale.com/admin/settings/keys)
# Result: mantis reachable at https://<TS_HOSTNAME>.<your-tailnet>.ts.net
# Funnel (public exposure) opt-in via Tailscale admin → DNS → HTTPS + Funnel
tailscale:
image: tailscale/tailscale:stable@sha256:25cde9ad76020b0e29229136d0c38b5962e9a0e1774ffac9b0df68e4a37d6cf0
profiles: ["tailscale"]
hostname: ${TS_HOSTNAME:-mantis}
restart: unless-stopped
depends_on:
mantis:
condition: service_started
environment:
TS_AUTHKEY: ${TS_AUTHKEY:-}
TS_HOSTNAME: ${TS_HOSTNAME:-mantis}
TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-}
TS_SERVE_CONFIG: /config/serve.json
TS_STATE_DIR: /var/lib/tailscale
TS_USERSPACE: "false"
volumes:
- ts-state:/var/lib/tailscale
- ./docker/tailscale/serve.json:/config/serve.json:ro
cap_add:
- net_admin
- sys_module
devices:
- /dev/net/tun
# --- Tailscale split profile ---------------------------------------------
# Enables: docker compose --profile tailscale-split up
# Result: private dashboard/API at https://<TS_PRIVATE_HOSTNAME>.<tailnet>.ts.net
# public triggers/status/wallet at https://<TS_PUBLIC_HOSTNAME>.<tailnet>.ts.net
# Set PUBLIC_BASE_URL + PUBLIC_ONLY_HOSTS to the public hostname, and
# DASHBOARD_HOSTS to the private hostname.
tailscale-private:
image: tailscale/tailscale:stable@sha256:25cde9ad76020b0e29229136d0c38b5962e9a0e1774ffac9b0df68e4a37d6cf0
profiles: ["tailscale-split"]
hostname: ${TS_PRIVATE_HOSTNAME:-mantis-private}
restart: unless-stopped
depends_on:
mantis:
condition: service_started
environment:
TS_AUTHKEY: ${TS_AUTHKEY:-}
TS_HOSTNAME: ${TS_PRIVATE_HOSTNAME:-mantis-private}
TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-}
TS_SERVE_CONFIG: /config/serve-private.json
TS_STATE_DIR: /var/lib/tailscale
TS_USERSPACE: "false"
volumes:
- ts-private-state:/var/lib/tailscale
- ./docker/tailscale/serve-private.json:/config/serve-private.json:ro
cap_add:
- net_admin
- sys_module
devices:
- /dev/net/tun
tailscale-public:
image: tailscale/tailscale:stable@sha256:25cde9ad76020b0e29229136d0c38b5962e9a0e1774ffac9b0df68e4a37d6cf0
profiles: ["tailscale-split"]
hostname: ${TS_PUBLIC_HOSTNAME:-mantis-public}
restart: unless-stopped
depends_on:
mantis:
condition: service_started
environment:
TS_AUTHKEY: ${TS_AUTHKEY:-}
TS_HOSTNAME: ${TS_PUBLIC_HOSTNAME:-mantis-public}
TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-}
TS_SERVE_CONFIG: /config/serve-public.json
TS_STATE_DIR: /var/lib/tailscale
TS_USERSPACE: "false"
volumes:
- ts-public-state:/var/lib/tailscale
- ./docker/tailscale/serve-public.json:/config/serve-public.json:ro
cap_add:
- net_admin
- sys_module
devices:
- /dev/net/tun
# --- Cloudflare Tunnel profile --------------------------------------------
# Enables: docker compose --profile cloudflared up
# Requires: CLOUDFLARE_TUNNEL_TOKEN env var (from Cloudflare Zero Trust → Networks → Tunnels)
# Tunnel routing (hostname → http://mantis:3000) is configured in the CF dashboard.
# Dashboard access policies via Cloudflare Access also live in the CF dashboard.
cloudflared:
image: cloudflare/cloudflared:latest@sha256:ba461b8aa9c042156dbd39c38657fe7431bafa063220eab8d5330a523863da9f
profiles: ["cloudflared"]
restart: unless-stopped
depends_on:
mantis:
condition: service_started
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN:-}
networks:
# Internal-only network for app <-> database traffic. `internal: true` gives
# containers on it no gateway to the host or the internet, so Postgres has no
# outbound path and is unreachable from outside the compose project. mantis is
# multi-homed (also on `default`), so its outbound traffic still routes out.
db:
internal: true
# The normal bridge network compose creates. mantis and the tunnel sidecars
# share it; declared here only to reference it explicitly above.
default:
volumes:
mantis-pg:
ts-state:
ts-private-state:
ts-public-state: