Repository navigation
Expand file tree
/
Copy pathcompose.prod.yaml
More file actions
162 lines (151 loc) · 5.46 KB
/
Copy pathcompose.prod.yaml
File metadata and controls
162 lines (151 loc) · 5.46 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
# Production stack: Caddy terminates TLS in front of gunicorn.
#
# fnox exec -- docker compose -f compose.prod.yaml up -d --build
#
# fnox injects the secrets as environment variables, which is where compose's
# ${VAR} interpolation reads them from — so no --env-file is needed, and every
# compose subcommand (exec, logs, down) wants the same `fnox exec --` prefix.
#
# Only Caddy publishes ports. Postgres and Valkey stay on the internal network,
# reachable from the host only through `docker compose exec`.
name: blackpythondevs-prod
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
caddy:
logging: *default-logging
image: caddy:2-alpine
restart: unless-stopped
environment:
# The real site's values, so a missing .env.production cannot silently
# fall back to base.py's localhost ALLOWED_HOSTS and 400 every request.
DOMAIN: ${DOMAIN:-blackpythondevs.org}
ACME_EMAIL: ${ACME_EMAIL:-contact@blackpythondevs.org}
# Set to "tls internal" to self-sign for local testing. See the Caddyfile.
CADDY_TLS_INTERNAL: ${CADDY_TLS_INTERNAL:-}
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data # issued certificates — losing this re-issues, and
- caddy_config:/config # Let's Encrypt rate-limits repeat issuance
- media_data:/srv/media:ro
depends_on:
- web
networks:
- frontend
web:
logging: *default-logging
build:
context: .
target: production
restart: unless-stopped
environment:
DJANGO_SETTINGS_MODULE: bpd.settings.production
DATABASE_URL: postgres://${POSTGRES_USER:-bpd}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-bpd}
VALKEY_URL: redis://valkey:6379/0
SECRET_KEY: ${SECRET_KEY:?set SECRET_KEY}
ALLOWED_HOSTS: ${DOMAIN:-blackpythondevs.org},www.${DOMAIN:-blackpythondevs.org}
CSRF_TRUSTED_ORIGINS: https://${DOMAIN:-blackpythondevs.org},https://www.${DOMAIN:-blackpythondevs.org}
WAGTAILADMIN_BASE_URL: https://${DOMAIN:-blackpythondevs.org}
CARTO_API_KEY: ${CARTO_API_KEY:-}
DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL:-noreply@blackpythondevs.org}
# Required, not defaulted: allauth is passwordless, so an empty SMTP
# config locks every user out while the healthcheck still reports green.
# Failing at `up` is the loud version of that.
EMAIL_HOST: ${EMAIL_HOST:?set EMAIL_HOST}
EMAIL_PORT: ${EMAIL_PORT:-587}
EMAIL_HOST_USER: ${EMAIL_HOST_USER:?set EMAIL_HOST_USER}
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD:?set EMAIL_HOST_PASSWORD}
AWS_STORAGE_BUCKET_NAME: ${AWS_STORAGE_BUCKET_NAME:-}
AWS_S3_ACCESS_KEY_ID: ${AWS_S3_ACCESS_KEY_ID:-}
AWS_S3_SECRET_ACCESS_KEY: ${AWS_S3_SECRET_ACCESS_KEY:-}
AWS_S3_REGION_NAME: ${AWS_S3_REGION_NAME:-us-east-1}
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-}
AWS_S3_CUSTOM_DOMAIN: ${AWS_S3_CUSTOM_DOMAIN:-}
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID:-}
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET:-}
DISCORD_GUILD_ID: ${DISCORD_GUILD_ID:-}
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-}
DISCORD_MEMBER_ROLE_ID: ${DISCORD_MEMBER_ROLE_ID:-}
DISCORD_INVITE_URL: ${DISCORD_INVITE_URL:-https://discord.gg/XUc3tFqCT3}
volumes:
# No source bind-mount in production: the image carries the code and the
# static files collected at build time.
- media_data:/app/media
healthcheck:
# SECURE_SSL_REDIRECT would 301 a plain internal request, and the Host
# must be one Django allows — so the probe mimics what Caddy sends.
test:
- CMD
- python
- -c
- |
import os, urllib.request
host = os.environ["ALLOWED_HOSTS"].split(",")[0]
req = urllib.request.Request(
"http://127.0.0.1:8000/",
headers={"Host": host, "X-Forwarded-Proto": "https"},
)
urllib.request.urlopen(req, timeout=5)
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
depends_on:
db:
condition: service_healthy
valkey:
condition: service_healthy
networks:
- frontend
- backend
db:
logging: *default-logging
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-bpd}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-bpd}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bpd} -d ${POSTGRES_DB:-bpd}"]
interval: 5s
timeout: 5s
retries: 10
networks:
- backend
valkey:
logging: *default-logging
image: valkey/valkey:8-alpine
restart: unless-stopped
command: valkey-server --save 60 1 --loglevel warning
volumes:
- valkey_data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
networks:
- backend
networks:
frontend:
# internal: no gateway, so nothing here is reachable from outside the stack
# and containers on it have no route out. Only web joins it alongside
# frontend, which is what gives web its egress (SMTP, S3, Discord).
backend:
internal: true
volumes:
postgres_data:
valkey_data:
media_data:
caddy_data:
caddy_config: