Files
ulicraft-server-v1/plan/02-caddy.md
T
oierandClaude Opus 4.8 16523e4f62 feat(files): move filestash to upload., serve files. as read-only static browse
Filestash cannot be served under a subpath (its SPA hardcodes /api, /assets,
/login at root), so it moves to its own subdomain upload.${BASE_DOMAIN}. The
apex-level ask was a static, no-auth file browser at files.${BASE_DOMAIN}: caddy
now file_server browse's the same ${FILES_DATA_DIR} share read-only there.

- filestash general.host -> upload.; nginx upload. vhost keeps the 2g body +
  upload streaming + /admin throttle; new files. vhost is a plain static proxy.
- caddy: 60-files.caddy split into 60-upload.caddy (filestash) + 62-files.caddy
  (file_server browse of /srv/files); caddy mounts FILES_DATA_DIR :ro.
- LE_SUBDOMAINS += upload (both upload. and files. are vhosts needing certs).

Security: files. is unconditionally public read (no auth), independent of
FILES_AUTH. The htpasswd login only gates writes via upload. Documented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:03:33 +02:00

3.0 KiB

Caddy — internal router behind host nginx

Summary

caddy is the stack's internal HTTP router, sitting behind the host's own nginx (which terminates TLS — see 15-letsencrypt.md). caddy is published only on a localhost-only port (CADDY_HTTP_BIND=127.0.0.1, CADDY_HTTP_PORT=8880); nginx reverse-proxies every public vhost to it by Host header. Roles:

  1. Reverse proxy — auth. → drasl, avatar. → nmsr, status. → uptime-kuma, upload. → filestash.
  2. Static — apex serves the landing page + /launcher/ downloads; distribution. serves a static site from another repo; files. serves a read-only browse of the upload share.

Image: caddy:alpine. caddy natively reads {$BASE_DOMAIN} env placeholders — no template render needed.

Network aliases (internal resolution)

caddy:
  networks:
    mcnet:
      aliases:
        - "auth.${BASE_DOMAIN}"

Lets minecraft reach http://auth.ulicraft.net/authlib-injector from inside the stack.

conf.d snippets

The Caddyfile imports per-vhost snippets from caddy/conf.d/:

  • 00-core.caddy — auth. → drasl.
  • 10-static.caddy — apex landing (/srv/www) + /launcher/* (/srv/launcher).
  • 30-distribution.caddy — distribution. → /srv/distribution (static, from DISTRIBUTION_WEB_ROOT, another repo).
  • 40-avatar.caddy — avatar. → nmsr:8080 (skin/avatar renderer).
  • 50-status.caddy — status. → uptime-kuma:3001 (status page).
  • 60-upload.caddy — upload. → filestash:8334 (player file share, 20-files.md).
  • 62-files.caddy — files. → static /srv/files (read-only, no-auth file_server browse of the same FILES_DATA_DIR filestash writes to).

All vhosts are plain http://…; TLS is nginx's job. Mounts: ./www:/srv/www:ro, ./launcher:/srv/launcher:ro, ${DISTRIBUTION_WEB_ROOT}:/srv/distribution:ro, ${FILES_DATA_DIR}:/srv/files:ro.

Don't rewrite the Host header for upload. — filestash rejects any request whose Host doesn't match its configured general.host, with "only traffic from X is allowed" (a 403 that reads like an auth failure). nginx forwards the original Host and caddy passes it through untouched. Keep it that way.

Notes / risks

  • drasl behind a proxy: with matching BaseURL and nginx terminating TLS it is fine; nginx passes the original Host through to caddy → drasl.
  • /launcher/ is a sibling mount (/srv/launcher), not nested under the read-only /srv/www; handle_path strips the prefix.

Tasks

  • caddy service in docker-compose.yml, published 127.0.0.1:${CADDY_HTTP_PORT}:80
  • Pass BASE_DOMAIN + DISTRIBUTION_WEB_ROOT into caddy env
  • Caddyfile imports conf.d/*.caddy
  • Network alias for auth. subdomain
  • Mount ./www, ./launcher, ${DISTRIBUTION_WEB_ROOT}
  • Confirm reverse_proxy to drasl works (auth web UI loads, Yggdrasil API responds)
  • Verify internal alias resolution from minecraft container (curl authlib-injector)
  • Verify nmsr + uptime-kuma proxied (avatar. renders, status. loads)