-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
184 lines (178 loc) · 5.98 KB
/
Copy pathdocker-compose.yml
File metadata and controls
184 lines (178 loc) · 5.98 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
# Common Thread -- MySQL, ingest + PDF containers, and Cloudflare Tunnel.
#
# All services share common-thread-network. Workers VPC Hyperdrive reaches
# MySQL at mysql:3306; VPC_INGEST → json-ingest:8080; VPC_PDF → json-pdf:8081.
#
# Usage:
# cp docker-compose.env.example .env
# cp cloudflared/config.yml.example cloudflared/config.yml
# # credentials.json in cloudflared/
# docker compose up -d
#
# Schema is applied on first MySQL boot (empty mysql_data volume). To re-apply:
# MYSQL_URL='mysql://commonthread:<MYSQL_PASSWORD>@localhost:3306/common_thread' npm run db:migrate
#
# No service publishes a host port (#29): Docker port-publishing writes iptables
# rules that bypass UFW, so a published port would expose MySQL / the containers
# on the box's public interface regardless of the firewall. The Worker reaches
# json-ingest / json-pdf and MySQL (via Hyperdrive) through the cloudflared VPC
# connector on this shared network, by service name. If a host-local debug port
# is ever truly needed, bind loopback explicitly (127.0.0.1:3306:3306), never the
# bare 3306:3306 form.
#
# Worker (wrangler.toml):
# INGEST_WORKER_URL = "http://json-ingest:8080/trigger"
# PDF_WORKER_URL = "http://json-pdf:8081/render"
# INGEST_SECRET = same value as INGEST_SECRET in .env
# PDF_SECRET = same value as PDF_SECRET in .env (wrangler secret put PDF_SECRET)
services:
mysql:
image: mysql:8.0
container_name: common-thread-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env}
MYSQL_DATABASE: ${MYSQL_DATABASE:-common_thread}
MYSQL_USER: ${MYSQL_USER:?set MYSQL_USER in .env}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD in .env}
TZ: UTC
volumes:
- mysql_data:/var/lib/mysql
- ./mysql-schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
command: >
--default-authentication-plugin=caching_sha2_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max_connections=500
--max_allowed_packet=256M
--innodb_buffer_pool_size=1G
--innodb_log_file_size=256M
# MySQL needs a writable data dir (the named volume) so it is not read_only,
# but it still drops all caps it does not need and gets no-new-privileges.
# It runs as the mysql user inside the official image already.
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
# mysqld drops privileges to the mysql user at startup and the entrypoint
# fixes data-dir ownership; these are the only capabilities it needs.
- CHOWN
- SETGID
- SETUID
- DAC_OVERRIDE
healthcheck:
test:
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-u",
"root",
"-p${MYSQL_ROOT_PASSWORD}",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- common-thread
json-ingest:
image: ghcr.io/skyphusion-labs/common-thread-ingest:0.1.0
container_name: common-thread-ingest
hostname: json-ingest
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
environment:
PORT: "8080"
CONTAINER_NAME: json-ingest
MYSQL_URL: mysql://${MYSQL_USER:?set MYSQL_USER in .env}:${MYSQL_PASSWORD:?set MYSQL_PASSWORD in .env}@mysql:3306/${MYSQL_DATABASE:-common_thread}
R2_ACCOUNT_ID: ${R2_ACCOUNT_ID:?set R2_ACCOUNT_ID in .env}
R2_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID:?set R2_ACCESS_KEY_ID in .env}
R2_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY:?set R2_SECRET_ACCESS_KEY in .env}
R2_BUCKET_NAME: ${R2_BUCKET_NAME:-common-thread-archive}
INGEST_SECRET: ${INGEST_SECRET:?set INGEST_SECRET in .env}
# Hardening (#33): non-root, read-only rootfs with a tmpfs scratch, no caps,
# no privilege escalation. The server only needs to read its bundled code and
# talk out over the network; it writes nothing to disk.
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
networks:
- common-thread
json-pdf:
image: ghcr.io/skyphusion-labs/common-thread-pdf:0.1.0
container_name: common-thread-pdf
hostname: json-pdf
restart: unless-stopped
environment:
PORT: "8081"
CONTAINER_NAME: json-pdf
PDF_SECRET: ${PDF_SECRET:?set PDF_SECRET in .env}
# Hardening (#33): non-root, read-only rootfs, no caps, no privilege
# escalation. wkhtmltopdf + ghostscript write only under /tmp (render-pdfa.ts
# uses os.tmpdir()), which is a writable tmpfs here; the rootfs stays
# read-only. tmpfs sized generously for large evidence-packet renders.
user: "1000:1000"
read_only: true
tmpfs:
- /tmp:size=512m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:8081/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- common-thread
tunnel:
image: cloudflare/cloudflared:latest
container_name: cloudflared_tunnel
restart: unless-stopped
depends_on:
json-ingest:
condition: service_healthy
json-pdf:
condition: service_healthy
volumes:
- ./cloudflared:/etc/cloudflared
command: tunnel --config /etc/cloudflared/config.yml run
networks:
- common-thread
volumes:
mysql_data:
networks:
common-thread:
name: common-thread-network
driver: bridge