Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
197 lines (187 loc) · 8.04 KB
/
Copy pathdocker-compose.yml
File metadata and controls
197 lines (187 loc) · 8.04 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
x-logging: &logging
driver: json-file
options: { max-size: "10m", max-file: "3" }
# Bigger for postfix and dovecot: their logs are the record of an abuse run, and what a host-side fail2ban reads.
# During the 2026-08-31 incident 3x10MB rotated away in about two hours and took the start of the run with it.
x-mail-logging: &mail-logging
driver: json-file
options: { max-size: "50m", max-file: "10" }
x-domains: &domains
DOMAIN: ${DOMAIN}
MAIL_DOMAIN: ${MAIL_DOMAIN:-${DOMAIN}}
services:
caddy:
profiles: ["edge"]
image: &frontend-image ${EIGEN_FRONTEND_IMAGE:-ghcr.io/eigen-is/eigen/frontend:local}
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy-data:/data
- ./data/certs:/shared-certs
environment:
<<: *domains
CADDY_ACME_EMAIL: "email ${ACME_EMAIL:-admin@${MAIL_DOMAIN:-${DOMAIN}}}"
command: sh -c "/usr/local/bin/export-certs.sh & caddy run --config /etc/caddy/Caddyfile --adapter caddyfile"
restart: unless-stopped
networks: [eigen]
logging: *logging
eigen-api:
image: ${EIGEN_API_IMAGE:-ghcr.io/eigen-is/eigen/api:local}
user: "1000:1000"
# A real PID 1: forwards SIGTERM to bun and reaps its ffmpeg/weasyprint children.
init: true
# No host port binding — caddy / eigen-static / postfix / dovecot all reach the API via
# the docker network as `eigen-api:8000`. A host port for debugging: docs/SELF-HOSTING.md.
volumes:
- ./data:/app/data
# Backup artifacts live outside data/ so a wipe of the data directory cannot take them with
# it. The API creates the folder only when the first backup runs; Docker creates the mount point itself.
- ./backups:/app/backups
# For the server backup. ./eigen gives the file group 1000 and mode 0640 before every start. A single-file
# mount keeps the inode it started with. Configure replaces the file only when a value changes, and env_file
# then changes this service's environment, so the next start recreates it on the new file.
- ./.env.production:/app/install/.env.production:ro
# Lets the API reach a relay on the host (SMTP_RELAY_HOST=host.docker.internal) when Eigen hosts no mail.
extra_hosts:
- "host.docker.internal:host-gateway"
env_file: .env.production
environment:
PRODUCTION: 1
# Conventional production signal for third-party libs that gate on it (e.g. better-auth
# rate limiting + secure cookies). Set at runtime only — never as a Dockerfile build ENV,
# which would make `bun install` skip devDependencies. Dev overrides this back (dev compose).
NODE_ENV: production
EIGEN_DATA_ROOT: /app/data
EIGEN_BACKUPS_DIR: /app/backups
EIGEN_ENV_FILE: /app/install/.env.production
# Demo-instance mode (hourly wipe + reseed via scripts/demo-reset.sh). Unset on real
# instances → 0, so the demo entry route + auth guards stay inert.
EIGEN_DEMO: ${EIGEN_DEMO:-0}
# Off (0) on a deployment whose COMPOSE_PROFILES omits `mail`, so the apps hide every Mail entry point.
MAIL_ENABLED: ${MAIL_ENABLED:-1}
# Hosting mail, the API hands its mail to postfix, which relays through SMTP_RELAY_*. With MAIL_ENABLED=0
# the API ignores these and sends through SMTP_RELAY_* (from env_file) itself.
SMTP_HOST: postfix
SMTP_PORT: 25
# Loopback plus the docker bridge subnet (same EIGEN_SUBNET the network and postfix use), so
# Postfix/Dovecot bridge callers are trusted without the whole 172.16/12 range. The API leaves out
# the subnet's gateway, which a user on the host arrives from (lib/auth/trusted-networks.ts).
TRUSTED_NETWORKS: "127.0.0.0/8,::1,${EIGEN_SUBNET}"
restart: unless-stopped
# On SIGTERM the API gives a running backup job 30 s (SHUTDOWN_JOB_BUDGET_MS, jobs.ts), then
# drains pending S3 uploads for 20 s (SHUTDOWN_DRAIN_BUDGET_MS, server.ts): 50 s, and room for
# closing the homes. An upload still in flight past this replays from pending_uploads on boot.
stop_grace_period: 90s
# Each open Home costs ~25-30 fds (~6 WAL SQLite DBs × 3 files each + ~10 maildir fs.watch
# handles). Distro/Docker defaults (often 1024 soft) cap concurrent open homes at ~35, and
# cross-home fan-outs (e.g. sharing to a large team) then fail with SQLITE_IOERR and
# silently skip recipients. Pin the limit high — fds are cheap kernel objects.
ulimits:
nofile:
soft: 1048576
hard: 1048576
networks: [eigen]
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8000/health"]
interval: 30s
timeout: 3s
retries: 3
logging: *logging
eigen-static:
profiles: ["static"]
image: *frontend-image
command: caddy run --config /etc/caddy/Caddyfile.static --adapter caddyfile
# Long-form bind so EIGEN_STATIC_HOST/PORT substitute cleanly. Avoid the short-form
# `${IP}:${PORT}:8080` shape — the colon between vars confuses some Compose versions.
ports:
- target: 8080
published: "${EIGEN_STATIC_PORT:-8080}"
host_ip: "${EIGEN_STATIC_HOST:-127.0.0.1}"
protocol: tcp
environment: *domains
depends_on:
eigen-api:
condition: service_healthy
restart: unless-stopped
networks: [eigen]
logging: *logging
unbound:
profiles: ["mail"]
image: ${EIGEN_UNBOUND_IMAGE:-ghcr.io/eigen-is/eigen/unbound:local}
restart: unless-stopped
networks:
eigen:
ipv4_address: ${EIGEN_UNBOUND_IP}
healthcheck:
test: ["CMD", "drill", "@127.0.0.1", "cloudflare.com"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
logging: *logging
postfix:
profiles: ["mail"]
image: ${EIGEN_POSTFIX_IMAGE:-ghcr.io/eigen-is/eigen/postfix:local}
# IPv4 only. The `eigen` network has no IPv6, so docker-proxy would hand every IPv6 client to the mail services from
# the gateway's one address, and one of them could lock all of them out of AUTH or hold every connection slot. An
# IPv6 sender falls back to the A record. See docs/MAIL.md.
ports:
- "0.0.0.0:25:25"
- "0.0.0.0:465:465"
- "0.0.0.0:587:587"
dns: ["${EIGEN_UNBOUND_IP}"]
volumes:
- ./data/dkim:/data/dkim
- ./data/certs:/certs:ro
- postfix-queue:/var/spool/postfix
environment:
<<: *domains
SMTP_RELAY_HOST: ${SMTP_RELAY_HOST:-}
SMTP_RELAY_PORT: ${SMTP_RELAY_PORT:-}
SMTP_RELAY_USER: ${SMTP_RELAY_USER:-}
SMTP_RELAY_PASSWORD: ${SMTP_RELAY_PASSWORD:-}
# queue-monitor.sh: notify the org owner when the queue backs up (spam-run early warning).
QUEUE_CHECK_INTERVAL: ${QUEUE_CHECK_INTERVAL:-300}
QUEUE_ALERT_THRESHOLD: ${QUEUE_ALERT_THRESHOLD:-200}
QUEUE_ALERT_COOLDOWN: ${QUEUE_ALERT_COOLDOWN:-21600}
# The docker bridge subnet of the `eigen` network below. entrypoint.sh scopes Postfix `mynetworks` +
# OpenDKIM `InternalHosts` to loopback plus this value, so eigen-api can submit and get its mail
# DKIM-signed without trusting the whole 172.16/12 range. Unset → loopback only (fail closed).
EIGEN_SUBNET: ${EIGEN_SUBNET}
depends_on:
eigen-api:
condition: service_healthy
dovecot:
condition: service_started
unbound:
condition: service_healthy
restart: unless-stopped
networks: [eigen]
logging: *mail-logging
dovecot:
profiles: ["mail"]
image: ${EIGEN_DOVECOT_IMAGE:-ghcr.io/eigen-is/eigen/dovecot:local}
# IPv4 only, as postfix's ports.
ports:
- "0.0.0.0:993:993"
volumes:
- ./data:/data
- ./data/certs:/certs
environment: *domains
depends_on:
eigen-api:
condition: service_healthy
restart: unless-stopped
networks: [eigen]
logging: *mail-logging
volumes:
postfix-queue:
networks:
eigen:
driver: bridge
ipam:
config:
# ./eigen setup writes EIGEN_SUBNET, a /24 no other Docker network uses, and EIGEN_UNBOUND_IP inside it. No
# default and no :?: setup asks Compose for the project, and a local build builds, before it writes them.
- subnet: ${EIGEN_SUBNET}