-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.single.yml
More file actions
314 lines (303 loc) · 13.6 KB
/
Copy pathdocker-compose.single.yml
File metadata and controls
314 lines (303 loc) · 13.6 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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
services:
db:
image: timescale/timescaledb@sha256:95a7997408e3295935e4afe8af8c09aae22bd51732131d1e7351c63d6198a71c
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-ctops}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-ctops}
volumes:
- db_data:/var/lib/postgresql/data
ports:
# Loopback-only — nothing outside this host speaks to Postgres directly.
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ctops -d ctops"]
interval: 10s
timeout: 5s
retries: 5
password-manager-db:
image: postgres:17-alpine@sha256:c7526c0f6c3f30260a563d7bcf8ad778effac59a44f8ffa86678c35418338609
restart: unless-stopped
environment:
POSTGRES_DB: password_manager
POSTGRES_USER: password_manager
POSTGRES_PASSWORD: ${PASSWORD_MANAGER_DB_PASSWORD:-${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}}
volumes:
- password_manager_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U password_manager -d password_manager"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
password-manager-migrate:
image: ghcr.io/carrtech-dev/ct-password-manager/api@sha256:6f5cc00a33d5df59cbca9968b178675ea45afff9b8f9c2394c2b3ae0a7d09220
restart: "no"
depends_on:
password-manager-db:
condition: service_healthy
environment:
PM_DATABASE_URL: postgres://password_manager:${PASSWORD_MANAGER_DB_PASSWORD:-${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}}@password-manager-db:5432/password_manager?sslmode=disable
PM_CT_OPS_ISSUER: ${PASSWORD_MANAGER_CT_OPS_ISSUER:-${BETTER_AUTH_URL:-https://localhost}}
PM_CT_OPS_AUDIENCE: ${PASSWORD_MANAGER_CT_OPS_AUDIENCE:-ct-password-manager}
PM_CT_OPS_PRODUCT: ${PASSWORD_MANAGER_CT_OPS_PRODUCT:-ct-password-manager}
PM_CT_OPS_INSTANCE_ID: ${CT_OPS_INSTANCE_ID:-ct-ops-dev}
PM_CT_OPS_ED25519_PUBLIC_KEY: ${PASSWORD_MANAGER_CT_OPS_ED25519_PUBLIC_KEY:-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=}
PM_TRUSTED_ORIGINS: ${PASSWORD_MANAGER_TRUSTED_ORIGINS:-${BETTER_AUTH_TRUSTED_ORIGINS:-https://localhost}}
PM_SESSION_COOKIE_SECURE: ${PASSWORD_MANAGER_SESSION_COOKIE_SECURE:-true}
entrypoint: ["/app/password-manager-migrate"]
command: ["-action", "up"]
password-manager-api:
image: ghcr.io/carrtech-dev/ct-password-manager/api@sha256:6f5cc00a33d5df59cbca9968b178675ea45afff9b8f9c2394c2b3ae0a7d09220
restart: unless-stopped
depends_on:
password-manager-db:
condition: service_healthy
password-manager-migrate:
condition: service_completed_successfully
environment:
PM_DATABASE_URL: postgres://password_manager:${PASSWORD_MANAGER_DB_PASSWORD:-${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}}@password-manager-db:5432/password_manager?sslmode=disable
PM_CT_OPS_ISSUER: ${PASSWORD_MANAGER_CT_OPS_ISSUER:-${BETTER_AUTH_URL:-https://localhost}}
PM_CT_OPS_AUDIENCE: ${PASSWORD_MANAGER_CT_OPS_AUDIENCE:-ct-password-manager}
PM_CT_OPS_PRODUCT: ${PASSWORD_MANAGER_CT_OPS_PRODUCT:-ct-password-manager}
PM_CT_OPS_INSTANCE_ID: ${CT_OPS_INSTANCE_ID:-ct-ops-dev}
PM_CT_OPS_ED25519_PUBLIC_KEY: ${PASSWORD_MANAGER_CT_OPS_ED25519_PUBLIC_KEY:-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=}
PM_TRUSTED_ORIGINS: ${PASSWORD_MANAGER_TRUSTED_ORIGINS:-${BETTER_AUTH_TRUSTED_ORIGINS:-https://localhost}}
PM_SESSION_COOKIE_SECURE: ${PASSWORD_MANAGER_SESSION_COOKIE_SECURE:-true}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
web:
image: ${WEB_IMAGE:-ghcr.io/carrtech-dev/ct-ops/web:latest}
build:
context: .
dockerfile: apps/web/Dockerfile
restart: unless-stopped
depends_on:
agent-dist-init:
condition: service_completed_successfully
migrate:
condition: service_completed_successfully
password-manager-api:
condition: service_healthy
ports:
# Loopback-only — browser traffic flows in via the nginx container on :443.
# Keeping :3000 on 127.0.0.1 lets operators tunnel for debugging without
# exposing plaintext HTTP externally.
- "127.0.0.1:3000:3000"
environment:
POSTGRES_USER: ${POSTGRES_USER:-ctops}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
POSTGRES_DB: ${POSTGRES_DB:-ctops}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET must be set in your .env file}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-https://localhost}
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-https://localhost}
REQUIRE_EMAIL_VERIFICATION: ${REQUIRE_EMAIL_VERIFICATION:-true}
CT_OPS_TRUST_PROXY_HEADERS: ${CT_OPS_TRUST_PROXY_HEADERS:-true}
CT_OPS_INSTANCE_ID: ${CT_OPS_INSTANCE_ID:-ct-ops-dev}
PASSWORD_MANAGER_CT_OPS_ISSUER: ${PASSWORD_MANAGER_CT_OPS_ISSUER:-${BETTER_AUTH_URL:-https://localhost}}
PASSWORD_MANAGER_CT_OPS_AUDIENCE: ${PASSWORD_MANAGER_CT_OPS_AUDIENCE:-ct-password-manager}
PASSWORD_MANAGER_CT_OPS_PRODUCT: ${PASSWORD_MANAGER_CT_OPS_PRODUCT:-ct-password-manager}
PASSWORD_MANAGER_CT_OPS_ED25519_PRIVATE_KEY: ${PASSWORD_MANAGER_CT_OPS_ED25519_PRIVATE_KEY:-}
NODE_ENV: production
AGENT_DIST_DIR: /var/lib/ct-ops/agent-dist
AGENT_DOWNLOAD_BASE_URL: ${AGENT_DOWNLOAD_BASE_URL:-https://localhost}
# Empty by default so the browser uses the page's own origin and nginx
# routes /ws/terminal/* to the ingest container. Override with an
# absolute wss:// URL only if you intentionally want the browser to
# bypass nginx and connect to ingest directly.
INGEST_WS_URL: ${INGEST_WS_URL-}
# Path to the nginx-facing server cert. The enrolment bundle route reads
# this file and ships it to agents so they can verify the HTTPS URL used
# for self-update downloads, even when it is signed by a private CA not
# in the agent host's trust store.
WEB_TLS_CERT: /var/lib/ct-ops/server-tls/server.crt
CT_OPS_LOADTEST_ADMIN_KEY: ${CT_OPS_LOADTEST_ADMIN_KEY:-}
ANSIBLE_API_URL: ${ANSIBLE_API_URL:-http://ansible-api:8080}
# Latest CarrTech licence verifier key. CT-Ops stores the exact public key
# that validates each saved licence, so future image upgrades cannot break
# existing licences. This mounted file is used for newly pasted licences.
LICENCE_PUBLIC_KEY_PATH: /var/lib/ct-ops/licence-keys/current.pem
volumes:
- agent_dist:/var/lib/ct-ops/agent-dist
# Read-only mount of the nginx-facing server cert so the enrolment
# bundle route can embed it in agent downloads.
- ./deploy/tls:/var/lib/ct-ops/server-tls:ro
- ./licence-keys:/var/lib/ct-ops/licence-keys:ro
agent-dist-init:
image: busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
restart: "no"
user: "0:0"
volumes:
- agent_dist:/var/lib/ct-ops/agent-dist
command:
- /bin/sh
- -c
- |
set -e
mkdir -p /var/lib/ct-ops/agent-dist
chown 1001:1001 /var/lib/ct-ops/agent-dist
migrate:
image: ${WEB_IMAGE:-ghcr.io/carrtech-dev/ct-ops/web:latest}
build:
context: .
dockerfile: apps/web/Dockerfile
restart: "no"
depends_on:
db:
condition: service_healthy
environment:
POSTGRES_USER: ${POSTGRES_USER:-ctops}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
POSTGRES_DB: ${POSTGRES_DB:-ctops}
command: ["node", "migrate.js"]
ingest:
image: ${INGEST_IMAGE:-ghcr.io/carrtech-dev/ct-ops/ingest:latest}
build:
context: .
dockerfile: apps/ingest/Dockerfile
restart: unless-stopped
depends_on:
migrate:
condition: service_completed_successfully
ports:
- "9443:9443" # gRPC (mTLS) — agents connect here directly, bypassing nginx.
# Loopback-only — nginx reaches :8080 over the docker network; there is
# no reason to expose plaintext JWKS/healthz/WS externally.
- "127.0.0.1:8080:8080"
environment:
POSTGRES_USER: ${POSTGRES_USER:-ctops}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
POSTGRES_DB: ${POSTGRES_DB:-ctops}
INGEST_TLS_CERT: /etc/ct-ops/tls/server.crt
INGEST_TLS_KEY: /etc/ct-ops/tls/server.key
INGEST_JWT_KEY_FILE: /var/lib/ct-ops/jwt_key.pem
# Agent CA — signs per-agent mTLS client certs. Leave unset to let the
# ingest auto-generate on first boot and store in Postgres. To bring
# your own CA, mount the PEM files and uncomment below.
# INGEST_AGENT_CA_CERT: /etc/ct-ops/tls/agent-ca.crt
# INGEST_AGENT_CA_KEY: /etc/ct-ops/tls/agent-ca.key
# Required for encrypting the auto-generated agent CA key at rest.
# Same value used by the web app for LDAP bind-password encryption.
LDAP_ENCRYPTION_KEY: ${LDAP_ENCRYPTION_KEY:-${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET must be set}}
# Public base URL of the web app — agents construct their download URL
# from this, so it must be reachable from the agent host (not just from
# inside the docker network). Defaults to the nginx TLS endpoint.
INGEST_AGENT_DOWNLOAD_BASE_URL: ${AGENT_DOWNLOAD_BASE_URL:-https://localhost}
# Path to the nginx-facing server cert. Ingest reads this so it can
# compare the fingerprint an agent sends in its heartbeat against the
# current live cert — when they differ, ingest pushes the new cert down
# the heartbeat stream so the agent can keep verifying downloads after
# the operator swaps in a new cert without re-enrolling every host.
INGEST_WEB_SERVER_CERT: /etc/ct-ops/server-tls/server.crt
# The release-please manifest is baked into the ingest image at build
# time (see apps/ingest/Dockerfile) and INGEST_RELEASE_MANIFEST_PATH is
# set there as well, so production users running from GHCR do not need
# a source checkout for the agent version to be discovered.
volumes:
- ./deploy/dev-tls:/etc/ct-ops/tls:ro
- ./deploy/tls:/etc/ct-ops/server-tls:ro
- ingest_data:/var/lib/ct-ops
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 10s
timeout: 5s
retries: 5
ansible-api:
image: ${ANSIBLE_API_IMAGE:-ghcr.io/carrtech-dev/ct-ops/ansible-api:latest}
build:
context: apps/ansible-api
dockerfile: Dockerfile
environment:
ANSIBLE_API_PAIRING_USERNAME: ${ANSIBLE_API_PAIRING_USERNAME:-ctops}
ANSIBLE_API_PAIRING_PASSWORD: ${ANSIBLE_API_PAIRING_PASSWORD:-}
ANSIBLE_API_SERVICE_TOKEN_FILE: /var/lib/ct-ops/ansible-api/service-token.json
ANSIBLE_API_SERVICE_TOKEN_ID: ${ANSIBLE_API_SERVICE_TOKEN_ID:-}
ANSIBLE_API_SERVICE_TOKEN_SECRET: ${ANSIBLE_API_SERVICE_TOKEN_SECRET:-}
volumes:
- ansible_api_data:/var/lib/ct-ops/ansible-api
profiles:
- ansible
restart: unless-stopped
expose:
- "8080"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
nginx:
image: nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
restart: unless-stopped
depends_on:
tls-init:
condition: service_completed_successfully
web:
condition: service_started
ingest:
condition: service_healthy
password-manager-api:
condition: service_healthy
ports:
- "${NGINX_HTTPS_PORT:-443}:443"
- "${NGINX_HTTP_PORT:-80}:80"
volumes:
- ./deploy/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./deploy/tls:/etc/nginx/tls:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/nginx-healthz"]
interval: 30s
timeout: 5s
retries: 3
# Generates the nginx server cert on first boot if deploy/tls is empty.
# Reuses the nginx:alpine image (openssl is included) so we don't add a
# second image to the pull list — this matters for air-gap bundles. The
# cert is created as a run-once job: container exits 0 when done, and
# nginx starts only after a successful exit (depends_on above). When
# start.sh has already populated deploy/tls/ this is a no-op.
tls-init:
image: nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
restart: "no"
user: "0:0"
volumes:
- ./deploy/tls:/tls
entrypoint:
- /bin/sh
- -c
- |
set -e
if [ -f /tls/server.crt ] && [ -f /tls/server.key ]; then
echo "tls-init: cert already present at /tls — skipping generation."
exit 0
fi
if ! command -v openssl >/dev/null 2>&1; then
echo "tls-init: installing openssl..."
apk add --no-cache openssl >/dev/null
fi
echo "tls-init: generating self-signed cert at /tls..."
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
-keyout /tls/server.key \
-out /tls/server.crt \
-subj "/CN=ct-ops" \
-addext "subjectAltName=DNS:localhost,DNS:ingest,DNS:ct-ops,IP:127.0.0.1" 2>/dev/null
chmod 600 /tls/server.key
chmod 644 /tls/server.crt
echo "tls-init: wrote /tls/server.{crt,key}. Replace with your own cert"
echo "tls-init: at any time to remove the browser warning."
volumes:
db_data:
password_manager_db_data:
ingest_data:
agent_dist:
ansible_api_data: