Diff
patch ebfae720dc00959da239021353e1012c0021c41a
Author: fritjof@alokat.org
Date: Mon Apr 20 12:11:07 UTC 2026
* Redesign page header and replace nginx with Caddy
hunk ./Caddyfile 10
-# Or with docker compose (replace the nginx service):
-# caddy:
-# image: caddy:2-alpine
-# ports: ["80:80", "443:443", "443:443/udp"]
-# volumes:
-# - ./Caddyfile:/etc/caddy/Caddyfile:ro
-# - caddy_data:/data
-# - caddy_config:/config
+# Or via the bundled docker-compose.yml:
+# docker compose up -d
+#
+# Note: the `rate_limit` directive below requires the caddy-ratelimit
+# module. The stock `caddy:2-alpine` image does not include it -- either
+# build a custom image (`FROM caddy:builder` then `xcaddy build --with
+# github.com/mholt/caddy-ratelimit`), use a prebuilt image that bundles
+# the module, or remove the `rate_limit { ... }` block to run with the
+# stock image.
hunk ./README.md 228
-The included `docker-compose.yml` and `nginx.conf` add an nginx reverse proxy
-with automatic Let's Encrypt certificates via certbot.
-
-1. Replace `darcs.example.com` with your domain in both `nginx.conf` and the
- certbot command below.
+The included `docker-compose.yml` and `Caddyfile` add a Caddy reverse proxy
+that automatically provisions and renews Let's Encrypt certificates via ACME.
+
+1. Replace `darcs.example.com` with your domain in `Caddyfile`.
hunk ./README.md 234
-3. Start nginx and darcsweb (certbot needs nginx running for the ACME
- challenge):
+3. Point DNS for your domain at the host and ensure ports 80 and 443 (TCP +
+ UDP) are reachable -- Caddy needs both for the ACME HTTP-01 challenge and
+ HTTP/3.
+4. Start the stack:
hunk ./README.md 240
-docker compose up -d nginx darcsweb
+docker compose up -d
hunk ./README.md 243
-4. Obtain the initial certificate:
-
-```
-docker compose run --rm certbot certonly --webroot \
- -w /var/www/certbot -d darcs.example.com
-```
-
-5. Reload nginx to pick up the new certificate, then start the certbot
- renewal sidecar:
-
-```
-docker compose exec nginx nginx -s reload
-docker compose up -d certbot
-```
-
-The certbot container renews certificates automatically every 12 hours (a
-no-op when they are not yet due). After renewal nginx must be reloaded; a
-cron entry on the host handles this:
-
-```
-0 */12 * * * docker compose exec nginx nginx -s reload
-```
-
-Certificate data is stored in the `certbot_conf` volume and persists across
+Caddy obtains the certificate on first launch and renews automatically.
+Certificate data is stored in the `caddy_data` volume and persists across
hunk ./docker-compose.yml 10
- nginx:
- image: nginx:stable-alpine
+ caddy:
+ image: caddy:2-alpine
hunk ./docker-compose.yml 15
+ - "443:443/udp"
hunk ./docker-compose.yml 17
- - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- - certbot_www:/var/www/certbot:ro
- - certbot_conf:/etc/letsencrypt:ro
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
+ - caddy_data:/data
+ - caddy_config:/config
hunk ./docker-compose.yml 24
- certbot:
- image: certbot/certbot
- volumes:
- - certbot_www:/var/www/certbot
- - certbot_conf:/etc/letsencrypt
- entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"
-
hunk ./docker-compose.yml 25
- certbot_www:
- certbot_conf:
+ caddy_data:
+ caddy_config:
hunk ./nginx.conf 1
-# Rate limiting zone: 10 requests/second per IP, 10MB zone
-limit_req_zone $binary_remote_addr zone=darcsweb:10m rate=10r/s;
-
-server {
- listen 80;
- server_name darcs.example.com;
- server_tokens off;
-
- location /.well-known/acme-challenge/ {
- root /var/www/certbot;
- }
-
- location / {
- return 301 https://$host$request_uri;
- }
-}
-
-server {
- listen 443 ssl;
- http2 on;
- server_name darcs.example.com;
- server_tokens off;
-
- ssl_certificate /etc/letsencrypt/live/darcs.example.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/darcs.example.com/privkey.pem;
-
- ssl_protocols TLSv1.2 TLSv1.3;
- ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
- ssl_prefer_server_ciphers off;
-
- # OCSP stapling
- ssl_stapling on;
- ssl_stapling_verify on;
-
- # Session resumption
- ssl_session_timeout 1d;
- ssl_session_cache shared:SSL:10m;
- ssl_session_tickets off;
-
- # HSTS: 2 years, include subdomains
- add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
-
- # Limit request body and URI length
- client_max_body_size 1k;
- large_client_header_buffers 4 8k;
-
- location / {
- limit_req zone=darcsweb burst=20 nodelay;
-
- proxy_pass http://darcsweb:3000;
- 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;
-
- # Timeouts to prevent slow-loris attacks
- proxy_connect_timeout 5s;
- proxy_read_timeout 30s;
- proxy_send_timeout 10s;
- }
-}
rmfile ./nginx.conf
hunk ./src/DarcsWeb/Html.hs 40
- , "<a href=\"/\"><img src=\"/static/darcs-logo.png\" alt=\"darcs\" class=\"header-logo\"></a>\n"
- , breadcrumbs
+ , "<a href=\"/\" class=\"brand\" aria-label=\"darcs home\">"
+ , "<img src=\"/static/darcs-logo.png\" alt=\"darcs\" class=\"header-logo\">"
+ , "</a>\n"
+ , "<nav class=\"breadcrumbs\">", breadcrumbs, "</nav>\n"
hunk ./static/style.css 190
- gap: var(--space-2);
+ flex-wrap: wrap;
+ gap: var(--space-4);
hunk ./static/style.css 204
- opacity: 0.75;
hunk ./static/style.css 208
- opacity: 1;
hunk ./static/style.css 211
+/* Brand chip: light background so the original dark-on-light logo
+ reads clearly against the dark header. */
+.page-header .brand {
+ background: #ffffff;
+ padding: 4px 12px;
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ opacity: 1;
+ flex-shrink: 0;
+}
+
+.page-header .brand:hover {
+ background: #f8f6f2;
+}
+
hunk ./static/style.css 227
- height: 24px;
+ height: 32px;
hunk ./static/style.css 229
- vertical-align: middle;
- filter: brightness(0) invert(1);
- opacity: 0.9;
+ display: block;
hunk ./static/style.css 232
+.breadcrumbs {
+ color: var(--text-inverse);
+ opacity: 0.85;
+ display: inline-flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0;
+ font-size: var(--size-sm);
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.breadcrumbs a {
+ opacity: 0.85;
+}
+
+.breadcrumbs a:hover {
+ opacity: 1;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
hunk ./static/style.css 805
+ gap: var(--space-2);
+ }
+
+ .page-header .brand {
+ padding: 3px 8px;
+ }
+
+ .header-logo {
+ height: 26px;