forked from hankadotai/edc-docker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yaml
More file actions
454 lines (410 loc) · 19.1 KB
/
Copy pathdocker-compose.yaml
File metadata and controls
454 lines (410 loc) · 19.1 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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
###############################################################################
# Standalone Tractus-X EDC — production posture
#
# Caddy TLS termination + Let's Encrypt
# Vault production raft mode, persistent volume, scoped EDC token
# Postgres persistent EDC state store
# EDC control-plane + data-plane (tractusx-edc 0.12.0)
#
# Dataspace identity, directory, and credential issuance are NOT bundled —
# provided by the dataspace operator.
#
# Bootstrap (one time):
# ./scripts/setup.sh hanka # generate .env from preset
# <edit .env>
# docker compose up -d --wait vault
# docker compose run --rm vault-init
# docker compose cp vault-init:/vault/state/init.json ./vault-init.json
# # back up vault-init.json off-host, then delete it locally
# ./scripts/up.sh
#
# Day-to-day:
# ./scripts/up.sh # safe in any state (cold/warm/post-restart)
# docker compose logs -f controlplane
# docker compose down # stop, keep volumes
#
# See docs/ONBOARDING.md for the full procedure and docs/LIMITATIONS.md for
# the (deliberate) limitations of this single-node stack.
###############################################################################
x-java-env: &java-env
JAVA_TOOL_OPTIONS: "${JAVA_TOOL_OPTIONS:--Xmx512m}"
# The upstream tractusx/edc-* images hard-code the OpenTelemetry javaagent in
# their entrypoint, defaulting its OTLP exporter to localhost:4318. This stack
# ships no collector, so the agent endlessly fails to export, swallows the EDC
# app logs (JUL FINE has no effect while it is active), and adds okhttp
# overhead. Disable it by default; set OTEL_JAVAAGENT_ENABLED=true (and run a
# collector) to opt back in.
OTEL_JAVAAGENT_ENABLED: "${OTEL_JAVAAGENT_ENABLED:-false}"
services:
# --- HTTPS termination ---------------------------------------------------
# Caddyfile is written inline to avoid a bind-mount that would fail in
# Portainer git-stack deployments. {$VAR} is Caddy's env-var syntax and
# is NOT expanded by the shell (heredoc uses quoted 'EOF' delimiter).
caddy:
image: caddy:2.11.2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- caddy-data:/data
- caddy-config:/config
environment:
EDC_PUBLIC_HOST: ${EDC_PUBLIC_HOST}
ACME_EMAIL: ${ACME_EMAIL}
entrypoint: ["/bin/sh", "-c"]
command:
- |
cat > /tmp/Caddyfile << 'EOF'
{
email {$ACME_EMAIL}
}
{$EDC_PUBLIC_HOST} {
encode gzip
@dsp path /api/v1/dsp /api/v1/dsp/*
handle @dsp {
reverse_proxy controlplane:8084
}
@public path /api/public /api/public/*
handle @public {
reverse_proxy dataplane:8081
}
handle {
respond "not found" 404
}
log {
output stdout
format console
}
}
EOF
exec caddy run --config /tmp/Caddyfile --adapter caddyfile
depends_on:
controlplane:
condition: service_healthy
dataplane:
condition: service_healthy
# --- PostgreSQL ----------------------------------------------------------
postgres:
image: postgres:17.9-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: edc
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 3s
retries: 10
# --- One-shot: ensure vault-data is owned by the vault user --------------
# Docker creates named volumes as root:root by default, but the official
# hashicorp/vault image runs as the unprivileged "vault" user — without
# this chown the raft store fails on first boot with "permission denied".
# Idempotent: re-running on an already-owned volume is a no-op.
vault-permissions:
image: hashicorp/vault:1.21.4
restart: "no"
user: "0"
entrypoint: ["chown", "-R", "vault:vault", "/vault/data"]
volumes:
- vault-data:/vault/data
# --- HashiCorp Vault (production raft) -----------------------------------
# Config is written inline via the entrypoint to avoid a bind-mount of
# vault.hcl, which fails in Portainer git-stack deployments (Portainer
# resolves relative paths inside its own container, not on the Docker host).
vault:
image: hashicorp/vault:1.21.4
restart: unless-stopped
cap_add: [IPC_LOCK]
entrypoint: ["/bin/sh", "-c"]
command:
- |
mkdir -p /vault/config
cat > /vault/config/config.hcl << 'EOF'
storage "raft" {
path = "/vault/data"
node_id = "node1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = "true"
}
api_addr = "http://vault:8200"
cluster_addr = "http://vault:8201"
ui = false
disable_mlock = false
EOF
exec vault server -config=/vault/config/config.hcl
volumes:
- vault-data:/vault/data
environment:
VAULT_ADDR: "http://127.0.0.1:8200"
healthcheck:
test:
- "CMD"
- "wget"
- "-qO-"
- "http://127.0.0.1:8200/v1/sys/health?standbyok=true&sealedcode=200&uninitcode=200"
interval: 5s
timeout: 3s
retries: 30
depends_on:
vault-permissions:
condition: service_completed_successfully
# --- Vault init / unseal / seed (runs to completion on every up) ---------
# Script is inlined to avoid a bind-mount that would fail in Portainer
# git-stack deployments. Idempotent: safe to re-run on every compose up.
vault-init:
image: hashicorp/vault:1.21.4
restart: "no"
user: "0"
environment:
VAULT_ADDR: "http://vault:8200"
STS_CLIENT_SECRET: "${STS_CLIENT_SECRET}"
TOKEN_SIGNER_KEY_JWK: "${TOKEN_SIGNER_KEY_JWK}"
volumes:
- vault-state:/vault/state
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
VAULT_ADDR="${VAULT_ADDR:-http://vault:8200}"
STATE_DIR="/vault/state"
INIT_FILE="${STATE_DIR}/init.json"
export VAULT_ADDR
mkdir -p "${STATE_DIR}"
chmod 700 "${STATE_DIR}"
command -v jq >/dev/null 2>&1 || apk add --no-cache jq >/dev/null
wait_for_vault() {
i=0
while ! wget -qO- "${VAULT_ADDR}/v1/sys/health?standbyok=true&sealedcode=200&uninitcode=200" >/dev/null 2>&1; do
i=$((i+1))
[ "${i}" -gt 60 ] && { echo "vault did not become reachable in 60s" >&2; exit 1; }
sleep 1
done
}
wait_for_vault
INITIALIZED="$(vault status -format=json 2>/dev/null | jq -r .initialized)"
if [ "${INITIALIZED}" != "true" ]; then
echo "=== Initialising vault (first boot) ==="
vault operator init -key-shares=5 -key-threshold=3 -format=json > "${INIT_FILE}"
chmod 600 "${INIT_FILE}"
echo "=== ACTION REQUIRED: back up /vault/state/init.json off-host ==="
fi
SEALED="$(vault status -format=json 2>/dev/null | jq -r .sealed)"
if [ "${SEALED}" = "true" ]; then
[ ! -f "${INIT_FILE}" ] && { echo "vault sealed but ${INIT_FILE} missing" >&2; exit 1; }
echo "=== Unsealing vault ==="
for i in 0 1 2; do
vault operator unseal "$(jq -r ".unseal_keys_b64[${i}]" "${INIT_FILE}")" > /dev/null
done
fi
ROOT_TOKEN="$(jq -r .root_token "${INIT_FILE}")"
export VAULT_TOKEN="${ROOT_TOKEN}"
vault secrets list -format=json 2>/dev/null | jq -e '."secret/"' > /dev/null || \
vault secrets enable -path=secret -version=2 kv > /dev/null
echo "=== Writing edc policy ==="
vault policy write edc - <<'POLICY' > /dev/null
path "secret/data/sts-oauth-client-secret" { capabilities = ["read"] }
path "secret/data/token-signer-key" { capabilities = ["read"] }
path "auth/token/renew-self" { capabilities = ["update"] }
path "auth/token/lookup-self" { capabilities = ["read"] }
POLICY
[ -n "${STS_CLIENT_SECRET:-}" ] && \
vault kv put secret/sts-oauth-client-secret content="${STS_CLIENT_SECRET}" > /dev/null || \
echo "WARNING: STS_CLIENT_SECRET empty, skipping" >&2
[ -n "${TOKEN_SIGNER_KEY_JWK:-}" ] && \
vault kv put secret/token-signer-key content="${TOKEN_SIGNER_KEY_JWK}" > /dev/null || \
echo "WARNING: TOKEN_SIGNER_KEY_JWK empty, skipping" >&2
echo "=== Vault is ready ==="
depends_on:
vault:
condition: service_healthy
# --- Vault auto-unseal sidecar (self-heal across restarts) ---------------
# Script is inlined to avoid a bind-mount that would fail in Portainer
# git-stack deployments.
vault-unseal:
image: hashicorp/vault:1.21.4
restart: unless-stopped
user: "0"
environment:
VAULT_ADDR: "http://vault:8200"
volumes:
- vault-state:/vault/state:ro
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -u
VAULT_ADDR="${VAULT_ADDR:-http://vault:8200}"
export VAULT_ADDR
INIT_FILE="/vault/state/init.json"
INTERVAL="${UNSEAL_INTERVAL:-15}"
command -v jq >/dev/null 2>&1 || apk add --no-cache jq >/dev/null 2>&1 || true
is_sealed() {
vault status 2>/dev/null | grep -q '^Sealed[[:space:]]*true'
}
echo "[auto-unseal] watcher started (interval=${INTERVAL}s)"
while true; do
if [ -f "${INIT_FILE}" ] && is_sealed; then
echo "[auto-unseal] Vault is sealed — unsealing"
command -v jq >/dev/null 2>&1 || apk add --no-cache jq >/dev/null 2>&1 || true
i=0
while [ "${i}" -lt 3 ]; do
key="$(jq -r ".unseal_keys_b64[${i}]" "${INIT_FILE}" 2>/dev/null)"
[ -n "${key}" ] && [ "${key}" != "null" ] && \
vault operator unseal "${key}" >/dev/null 2>&1
i=$((i + 1))
done
if is_sealed; then
echo "[auto-unseal] STILL sealed after 3 keys — check init.json"
else
echo "[auto-unseal] Vault unsealed"
fi
fi
sleep "${INTERVAL}"
done
depends_on:
vault:
condition: service_healthy
# --- EDC control-plane ---------------------------------------------------
controlplane:
image: tractusx/edc-controlplane-postgresql-hashicorp-vault:${EDC_VERSION}
restart: unless-stopped
volumes:
- ./config/edc-logging.properties:/app/logging.properties:ro
# Hyphenated config keys (e.g. tx.edc.iam.iatp.default-scopes.*)
# cannot be represented as environment variables — the EDC's
# env-var translator turns "_" into ".", not "-". Mounting them
# via a properties file is the only way the DCP extension reads
# them. Without this the connector mints STS tokens without
# scopes and verifying peers reject Presentation Queries with
# HTTP 403 "Invalid query: requested Credentials outside of
# scope." See docs/ONBOARDING.md §Scopes.
- ./config/edc-config.properties:/app/configuration.properties:ro
environment:
<<: *java-env
# --- Vault token (injected directly; avoids env_file timing issue on cold start) ---
EDC_VAULT_HASHICORP_TOKEN: "${EDC_VAULT_HASHICORP_TOKEN}"
# --- File-backed config (hyphenated keys) ---
EDC_FS_CONFIG: "/app/configuration.properties"
# --- Identity ---
EDC_HOSTNAME: "${EDC_HOSTNAME:-controlplane}"
EDC_PARTICIPANT_ID: "${EDC_DID}"
EDC_PARTICIPANT_CONTEXT_ID: "${EDC_PARTICIPANT_CONTEXT_ID}"
TRACTUSX_EDC_PARTICIPANT_BPN: "${EDC_BPN}"
EDC_IAM_ISSUER_ID: "${EDC_DID}"
EDC_IAM_DID_WEB_USE_HTTPS: "true"
EDC_IAM_ACCESSTOKEN_JTI_VALIDATION: "false"
# --- STS (single-step RemoteSecureTokenService) ---
EDC_IAM_STS_OAUTH_TOKEN_URL: "${STS_TOKEN_URL}"
EDC_IAM_STS_OAUTH_CLIENT_ID: "${EDC_DID}"
EDC_IAM_STS_OAUTH_CLIENT_SECRET_ALIAS: "sts-oauth-client-secret"
# --- Credential service ---
TX_EDC_IAM_IATP_CREDENTIALSERVICE_URL: "${CREDENTIAL_SERVICE_URL}"
# --- Default IATP scopes ---
# NOTE: scopes live in config/edc-config.properties because the
# key `tx.edc.iam.iatp.default-scopes.*` contains a hyphen that
# cannot be expressed as an environment variable.
# --- DSP callback (HTTPS, advertised to peers) ---
EDC_DSP_CALLBACK_ADDRESS: "${EDC_DSP_CALLBACK_ADDRESS}"
# --- BDRS ---
TX_EDC_IAM_IATP_BDRS_SERVER_URL: "${BDRS_URL}"
TX_EDC_IAM_IATP_BDRS_CACHE_VALIDITY: "600"
# --- DCP ---
TX_EDC_DID_SERVICE_SELF_REGISTRATION_ENABLED: "false"
# --- Trusted issuers ---
"EDC_IAM_TRUSTED-ISSUER_0-ISSUER_ID": "${TRUSTED_ISSUER_DID}"
"EDC_IAM_TRUSTED-ISSUER_0-ISSUER_SUPPORTEDTYPES": '["MembershipCredential","FrameworkCredential","BpnCredential","DataExchangeGovernanceCredential","DismantlerCredential"]'
# --- Web endpoints ---
WEB_HTTP_PORT: "8080"
WEB_HTTP_PATH: "/api"
WEB_HTTP_MANAGEMENT_PORT: "8081"
WEB_HTTP_MANAGEMENT_PATH: "/management"
WEB_HTTP_MANAGEMENT_AUTH_KEY: "${EDC_API_KEY}"
WEB_HTTP_CATALOG_PORT: "8082"
WEB_HTTP_CATALOG_PATH: "/catalog"
WEB_HTTP_CONTROL_PORT: "8083"
WEB_HTTP_CONTROL_PATH: "/control"
WEB_HTTP_PROTOCOL_PORT: "8084"
WEB_HTTP_PROTOCOL_PATH: "/api/v1/dsp"
WEB_HTTP_VERSION_PORT: "8085"
WEB_HTTP_VERSION_PATH: "/.well-known/api"
# --- Database ---
EDC_DATASOURCE_DEFAULT_URL: "jdbc:postgresql://postgres:5432/edc"
EDC_DATASOURCE_DEFAULT_USER: "${POSTGRES_USER}"
EDC_DATASOURCE_DEFAULT_PASSWORD: "${POSTGRES_PASSWORD}"
# --- Vault (URL + secret path; the token comes from env_file) ---
EDC_VAULT_HASHICORP_URL: "http://vault:8200"
EDC_VAULT_HASHICORP_API_SECRET_PATH: "/v1/secret"
ports:
# Management API: localhost-only. Use SSH port-forwarding or a
# private VPN to reach it from another host.
- "127.0.0.1:${EDC_MANAGEMENT_HOST_PORT:-29181}:8081"
healthcheck:
# EDC's observability API exposes /check/{liveness,readiness,startup}
# but NOT /check/health (404). Use startup, matching the dataplane.
test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/api/check/startup || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
depends_on:
postgres:
condition: service_healthy
vault-init:
condition: service_completed_successfully
# --- EDC data-plane ------------------------------------------------------
dataplane:
image: tractusx/edc-dataplane-hashicorp-vault:${EDC_VERSION}
restart: unless-stopped
environment:
<<: *java-env
# --- Vault token (injected directly; avoids env_file timing issue on cold start) ---
EDC_VAULT_HASHICORP_TOKEN: "${EDC_VAULT_HASHICORP_TOKEN}"
EDC_HOSTNAME: "${EDC_HOSTNAME:-controlplane}"
EDC_PARTICIPANT_ID: "${EDC_BPN}"
EDC_PARTICIPANT_CONTEXT_ID: "${EDC_PARTICIPANT_CONTEXT_ID}"
EDC_IAM_ISSUER_ID: "${EDC_DID}"
EDC_IAM_DID_WEB_USE_HTTPS: "true"
EDC_IAM_STS_OAUTH_TOKEN_URL: "${STS_TOKEN_URL}"
EDC_IAM_STS_OAUTH_CLIENT_ID: "${EDC_DID}"
EDC_IAM_STS_OAUTH_CLIENT_SECRET_ALIAS: "sts-oauth-client-secret"
WEB_HTTP_PORT: "8080"
WEB_HTTP_PATH: "/api"
WEB_HTTP_PUBLIC_PORT: "8081"
WEB_HTTP_PUBLIC_PATH: "/api/public"
WEB_HTTP_CONTROL_PORT: "8083"
WEB_HTTP_CONTROL_PATH: "/api/dataplane/control"
WEB_HTTP_VERSION_PORT: "8084"
WEB_HTTP_VERSION_PATH: "/.well-known/api"
EDC_DATAPLANE_API_PUBLIC_BASEURL: "${EDC_DATAPLANE_PUBLIC_URL}"
EDC_TRANSFER_PROXY_TOKEN_SIGNER_PRIVATEKEY_ALIAS: "token-signer-key"
EDC_TRANSFER_PROXY_TOKEN_VERIFIER_PUBLICKEY_ALIAS: "token-signer-key"
EDC_DPF_SELECTOR_URL: "http://controlplane:8083/control/v1/dataplanes"
EDC_DATASOURCE_DEFAULT_URL: "jdbc:postgresql://postgres:5432/edc"
EDC_DATASOURCE_DEFAULT_USER: "${POSTGRES_USER}"
EDC_DATASOURCE_DEFAULT_PASSWORD: "${POSTGRES_PASSWORD}"
EDC_VAULT_HASHICORP_URL: "http://vault:8200"
EDC_VAULT_HASHICORP_API_SECRET_PATH: "/v1/secret"
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/api/check/startup || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
depends_on:
postgres:
condition: service_healthy
vault-init:
condition: service_completed_successfully
controlplane:
condition: service_healthy
volumes:
postgres-data:
vault-data:
vault-state:
caddy-data:
caddy-config: