Add shared public edge (nginx + certbot)

One nginx owns host ports 80/443 for every project (Gitea, Musify) and
proxies by container name over infra-net and gitea_gitea; a certbot
service renews the Let's Encrypt certificates.
This commit is contained in:
2026-09-23 16:56:28 +02:00
parent deefb6d0cd
commit 24f801cf2f
10 changed files with 234 additions and 1 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
#
# Replace every CHANGE_ME with a strong random value (`openssl rand -hex 24`;
# the Zitadel masterkey needs exactly 32 characters) and auth.example.com
# with the real hostname the edge (e.g. Musify's nginx) terminates TLS for.
# with the real hostname the shared edge (edge/) terminates TLS for.
# --- PostgreSQL ---------------------------------------------------------------
POSTGRES_USER=postgres
+3
View File
@@ -10,3 +10,6 @@ zitadel/.output/
# OS
Thumbs.db
.DS_Store
# Edge runtime state (Let's Encrypt account, certificates, ACME challenges)
edge/certbot/
+29
View File
@@ -95,6 +95,34 @@ with an example value that assumes these repos happen to sit as sibling
folders. If yours don't, change it. Not versioned, and regenerated whenever
the Zitadel volume is reset.
## The public edge
`edge/compose.yml` is the only thing that publishes host ports 80/443: one
nginx that terminates TLS for every project's domains (Gitea, Musify, ...)
and proxies to their containers by name, plus a certbot container that
renews the Let's Encrypt certificates (webroot) and lets nginx reload them
every 6 hours. It joins `infra-net` and the `gitea_gitea` network (both
external, created by the stacks that own them).
```bash
docker compose -f edge/compose.yml up -d
```
- **Vhosts** live in `edge/conf.d/` (one file per project; `00-common.conf`
has the shared resolver). Every upstream is a `set $upstream ...`, resolved
per request, so a project's stack being down never stops the edge from
starting. Shared bits are in `edge/snippets/`. Check a change with
`docker exec edge-nginx nginx -t`, then `docker exec edge-nginx nginx -s reload`.
- **Certificates** are state, not versioned (`edge/certbot/`). To issue one
for new hostnames (their DNS has to already point at the host):
```bash
docker compose -f edge/compose.yml run --rm --entrypoint certbot certbot certonly --webroot -w /var/www/certbot -d host1.example.com -d host2.example.com --non-interactive --agree-tos -m you@example.com
```
Then add the `server` blocks pointing at
`/etc/letsencrypt/live/<first -d name>/`.
## Resetting
```bash
@@ -111,6 +139,7 @@ re-creates its piece on the next `up`, since both are idempotent.
```
Infrastructure/
├─ compose.yml # postgres, zitadel, seaweedfs, rabbitmq, jaeger: the prod shape
├─ edge/ # shared public nginx + certbot (prod only): compose.yml, conf.d/, snippets/
├─ compose.dev.yml # dev overlay: publishes ports, adds pgAdmin
├─ .env.example / .env.prod.example
├─ postgres-init/ # creates the per-project application databases
+51
View File
@@ -0,0 +1,51 @@
# Shared public edge: the only thing that publishes host ports 80/443. One
# nginx terminates TLS for every project's domains (Gitea, Musify, ...) and
# proxies to their containers by name, over the networks below. Certificates
# come from Let's Encrypt (webroot), renewed by the certbot service.
#
# docker compose -f edge/compose.yml up -d
#
# Requires `infra-net` (created by ../compose.yml) and `gitea_gitea` (created
# by the Gitea stack) to already exist.
name: edge
networks:
infra-net:
name: infra-net
external: true
gitea:
name: gitea_gitea
external: true
services:
nginx:
image: nginx:1.27-alpine
container_name: edge-nginx
# The subshell reloads nginx every 6h so renewed certificates get picked up.
command:
- /bin/sh
- -c
- "(while :; do sleep 6h; nginx -s reload; done) & exec nginx -g 'daemon off;'"
ports:
- "80:80"
- "443:443"
volumes:
- ./conf.d:/etc/nginx/conf.d:ro
- ./snippets:/etc/nginx/snippets:ro
- ./certbot/www:/var/www/certbot:ro
- ./certbot/conf:/etc/letsencrypt:ro
networks: [gitea, infra-net]
restart: unless-stopped
certbot:
image: certbot/certbot
container_name: edge-certbot
entrypoint:
- /bin/sh
- -c
- "trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot --quiet; sleep 12h & wait $${!}; done"
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
restart: unless-stopped
+8
View File
@@ -0,0 +1,8 @@
# Docker's embedded DNS. Every upstream below is a `set $upstream ...` so
# nginx resolves it per request and starts even if a project's stack is down.
resolver 127.0.0.11 valid=10s;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
+24
View File
@@ -0,0 +1,24 @@
server {
listen 80;
listen [::]:80;
server_name gitea.ikerdemo.cat;
include /etc/nginx/snippets/acme-redirect.conf;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name gitea.ikerdemo.cat;
client_max_body_size 512M;
ssl_certificate /etc/letsencrypt/live/gitea.ikerdemo.cat/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitea.ikerdemo.cat/privkey.pem;
include /etc/nginx/snippets/tls.conf;
location / {
set $upstream http://gitea:3000;
proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf;
}
}
+102
View File
@@ -0,0 +1,102 @@
# One certificate (live/musify.ikerdemo.cat) covers the five hostnames.
server {
listen 80;
listen [::]:80;
server_name musify.ikerdemo.cat musifyapi.ikerdemo.cat
musifystream.ikerdemo.cat auth.ikerdemo.cat s3.ikerdemo.cat;
include /etc/nginx/snippets/acme-redirect.conf;
}
# Web player. Uploads are posted to its own form actions.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name musify.ikerdemo.cat;
client_max_body_size 1024m;
ssl_certificate /etc/letsencrypt/live/musify.ikerdemo.cat/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/musify.ikerdemo.cat/privkey.pem;
include /etc/nginx/snippets/tls.conf;
location / {
set $upstream http://web-player:3000;
proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name musifyapi.ikerdemo.cat;
ssl_certificate /etc/letsencrypt/live/musify.ikerdemo.cat/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/musify.ikerdemo.cat/privkey.pem;
include /etc/nginx/snippets/tls.conf;
location / {
set $upstream http://api:5111;
proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf;
}
}
# Audio bytes. Buffering off keeps range requests (seeking) responsive.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name musifystream.ikerdemo.cat;
ssl_certificate /etc/letsencrypt/live/musify.ikerdemo.cat/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/musify.ikerdemo.cat/privkey.pem;
include /etc/nginx/snippets/tls.conf;
location / {
set $upstream http://gateway:8081;
proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf;
proxy_buffering off;
}
}
# Zitadel, shared with the other projects (hence the generic hostname).
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name auth.ikerdemo.cat;
ssl_certificate /etc/letsencrypt/live/musify.ikerdemo.cat/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/musify.ikerdemo.cat/privkey.pem;
include /etc/nginx/snippets/tls.conf;
location / {
set $upstream http://zitadel:8080;
proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf;
grpc_set_header Host $host;
}
}
# SeaweedFS S3: presigned PUT/GET straight from the browser.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name s3.ikerdemo.cat;
client_max_body_size 1024m;
ssl_certificate /etc/letsencrypt/live/musify.ikerdemo.cat/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/musify.ikerdemo.cat/privkey.pem;
include /etc/nginx/snippets/tls.conf;
location / {
set $upstream http://seaweedfs:8333;
proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf;
proxy_request_buffering off;
}
}
+7
View File
@@ -0,0 +1,7 @@
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
+7
View File
@@ -0,0 +1,7 @@
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
+2
View File
@@ -0,0 +1,2 @@
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;