Files
ulicraft-server-v1/nginx/ulicraft-caddy.conf.tmpl
Oier Bravo Urtasun 25df9f9398 feat(files): add Filestash player file share at files.${BASE_DOMAIN}
One shared login (htpasswd) for all players, writable. Config is rendered
from a template and mounted read-only, so git stays authoritative — the
admin console loads but cannot save, by design.

Filestash's OIDC/SAML middlewares are enterprise-only, so Keycloak SSO is
out; FILES_AUTH keeps htpasswd/passthrough switchable for later. Auth-off
(passthrough) is only valid once the host is off the public internet —
render-config.sh warns loudly when rendering it.

Three traps, all found by testing against the pinned image:
- the `local` backend is admin-gated: without LOCAL_BACKEND_SECRET in the
  connection params every login fails with "backend error - Not Allowed"
- the htpasswd plugin only accepts $2a$ bcrypt, so a $2y$ hash from
  `htpasswd -B` fails every login silently — use `openssl passwd -6`.
  render-config.sh rejects the wrong format rather than shipping it
- APPLICATION_URL/ADMIN_PASSWORD env make filestash rewrite config.json at
  boot, which fails on the :ro mount — both live in the rendered config

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:08:46 +02:00

192 lines
7.7 KiB
Cheetah

# ──────────────────────────────────────────────────────────────────
# Ulicraft host nginx — TLS terminator in FRONT of caddy.
# ──────────────────────────────────────────────────────────────────
# caddy (in docker-compose.yml) is the single internal ingress for ALL vhosts;
# this host nginx terminates TLS and reverse-proxies everything to caddy by Host
# header. Prefer tooling/render-nginx.sh over hand-rendering.
#
# Caddy must be published on a localhost port — set in .env:
# CADDY_HTTP_PORT=8880
# CADDY_HTTP_BIND=127.0.0.1
# and the stack brought up:
# docker compose up -d
#
# Render with the vars then install:
# CADDY_HTTP_PORT=8880 BASE_DOMAIN=ulicraft.net APP_DIR=/home/ubuntu/mc/ulicraft-server-v1 \
# envsubst '$CADDY_HTTP_PORT $BASE_DOMAIN $APP_DIR' \
# < nginx/ulicraft-caddy.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf
# sudo ln -sf /etc/nginx/sites-available/ulicraft.conf /etc/nginx/sites-enabled/
# sudo nginx -t && sudo systemctl reload nginx
#
# Prereqs:
# - certs issued: tooling/issue-letsencrypt.sh -> $APP_DIR/certs/<name>/{cert,key}.pem
# ──────────────────────────────────────────────────────────────────
upstream ulicraft_caddy {
server 127.0.0.1:${CADDY_HTTP_PORT};
}
# Brute-force throttle for the publicly-exposed Filestash /admin console (it is
# the only credential on files.), applied in its location block below.
limit_req_zone $binary_remote_addr zone=files_admin:10m rate=6r/m;
# HTTP -> HTTPS for every name.
server {
listen 80;
listen [::]:80;
server_name ${BASE_DOMAIN} www.${BASE_DOMAIN} auth.${BASE_DOMAIN} distribution.${BASE_DOMAIN} avatar.${BASE_DOMAIN} status.${BASE_DOMAIN} files.${BASE_DOMAIN};
return 301 https://$host$request_uri;
}
# One TLS server block per name (each has its own LE cert). All proxy to caddy;
# caddy routes by the forwarded Host header (apex -> www, auth -> drasl).
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
client_max_body_size 4m; # skin uploads (auth) reuse the same proxy block
location / {
proxy_pass http://ulicraft_caddy;
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;
}
}
# www -> apex canonical redirect (needs its own cert; covered by LE_SUBDOMAINS).
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/www.${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/www.${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
return 301 https://${BASE_DOMAIN}$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name auth.${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/auth.${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/auth.${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
client_max_body_size 4m; # skin uploads
location / {
proxy_pass http://ulicraft_caddy;
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;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name distribution.${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://ulicraft_caddy;
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;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name avatar.${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/avatar.${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/avatar.${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://ulicraft_caddy;
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;
}
}
# Filestash player file share. Uploads need a real body limit (nginx defaults to
# 1m, which rejects almost anything worth sharing) and streaming rather than
# spooling — a 2g upload buffered to disk would write it twice on a host whose
# disk is already the shared failure domain for Minecraft + Drasl + backups.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name files.${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/files.${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/files.${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
client_max_body_size 2g; # uploads; other vhosts stay at 4m
proxy_request_buffering off; # stream uploads, don't spool them to disk
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# /admin is reachable on purpose, so throttle it: it is the only password
# on this vhost. 6 req/min, small burst.
location /admin {
limit_req zone=files_admin burst=3 nodelay;
proxy_pass http://ulicraft_caddy;
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;
}
location / {
proxy_pass http://ulicraft_caddy;
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;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name status.${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/status.${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/status.${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://ulicraft_caddy;
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;
# Uptime Kuma uses websockets (socket.io) for live status updates.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}