# Caddy Configuration # Managed by Ansible - do not edit directly # Global options { email {{ caddy_email }} acme_ca {{ caddy_acme_ca }} # Admin API endpoint admin 0.0.0.0:{{ caddy_admin_port }} # Global server options servers { protocols h1 h2 h3 } # Logging log { output file /var/log/caddy/caddy.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format {{ caddy_log_format }} level {{ caddy_log_level }} # See rsvp-errors below. exclude http.log.error.rsvp } # {{ rsvp_server_name }}: when a proxied request fails, Caddy's error log # records the raw request URI -- which for /i/ is a working invite # link. Send those through the same redaction as the site's access log # instead of the default log above. log rsvp-errors { output file /var/log/caddy/rsvp.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format filter { wrap json request>uri regexp ^/i/[^/?#]+ /i/REDACTED request>headers>Referer delete } include http.log.error.rsvp } } # Import snippet for common security headers (common_headers) { header { {% for header, value in caddy_security_headers.items() %} {{ header }} "{{ value }}" {% endfor %} -Server -X-Powered-By } } # Jinja2 macro for IP-restricted sites with redirect {% macro ip_restricted_site(networks=caddy_local_networks) %} @local { remote_ip {{ networks | join(' ') }} } @denied { not remote_ip {{ networks | join(' ') }} } # Redirect non-local traffic handle @denied { redir https://debyl.io{uri} 302 } {% endmacro %} # ============================================================================ # SITE CONFIGURATIONS # ============================================================================ # Simple redirect: {{ base_server_name }} -> {{ base_server_name_io }} {{ base_server_name }} { redir https://{{ base_server_name_io }}{uri} 302 } # ============================================================================ # SIMPLE REVERSE PROXIES # ============================================================================ # Photos service - redirect old to new {{ photos_server_name }} { redir https://{{ photos_server_name_io }}{uri} 302 } # Photos service - {{ photos_server_name_io }} {{ photos_server_name_io }} { import common_headers reverse_proxy localhost:8088 log { output file /var/log/caddy/photos.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Wiki/BookStack - {{ bookstack_server_name }} redirect to new domain {{ bookstack_server_name }} { redir https://{{ bookstack_server_name_new }}{uri} 302 } # Wiki/BookStack - {{ bookstack_server_name_new }} (new primary domain) {{ bookstack_server_name_new }} { import common_headers reverse_proxy localhost:6875 log { output file /var/log/caddy/wiki.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # ============================================================================ # IP-RESTRICTED SITES # ============================================================================ # Home Assistant - redirect old to new {{ assistant_server_name }} { redir https://{{ assistant_server_name_io }}{uri} 302 } # Home Assistant - {{ assistant_server_name_io }} {{ assistant_server_name_io }} { {{ ip_restricted_site() }} handle @local { reverse_proxy localhost:8123 { # WebSocket support is automatic flush_interval -1 } header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" Referrer-Policy "same-origin" } } log { output file /var/log/caddy/assistant.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # CI/Drone - REMOVED # ci.bdebyl.net configuration removed - Drone CI infrastructure decommissioned # Home server - {{ home_server_name_io }} {{ home_server_name_io }} { {{ ip_restricted_site() }} handle @local { respond "Home Server Access OK" 200 } } # Parts/Partsy - redirect old to new {{ parts_server_name }} { redir https://{{ parts_server_name_io }}{uri} 302 } # Parts/Partsy - {{ parts_server_name_io }} {{ parts_server_name_io }} { {{ ip_restricted_site() }} handle @local { import common_headers reverse_proxy localhost:8081 } log { output file /var/log/caddy/parts.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Partsy Skudak (authenticated) - {{ parts_skudak_server_name }} {{ parts_skudak_server_name }} { import common_headers reverse_proxy localhost:8082 log { output file /var/log/caddy/parts-skudak.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # RSVP - {{ rsvp_server_name }} (public invite links, LAN-only /admin) # # Does NOT import common_headers: its Referrer-Policy "same-origin" would # replace the app's "no-referrer", and invite tokens live in the URL path. {{ rsvp_server_name }} { header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Robots-Tag "noindex, nofollow" Referrer-Policy "no-referrer" Content-Security-Policy "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'" X-Content-Type-Options "nosniff" -Server } # Admin is a second listener with no login. It is only published on # loopback, and only proxied here for LAN clients -- two independent controls. @admin_local { path /admin /admin/* remote_ip {{ caddy_local_networks | join(' ') }} } handle @admin_local { reverse_proxy 127.0.0.1:{{ rsvp_admin_port }} } handle /admin* { respond "Not found" 404 } handle { reverse_proxy 127.0.0.1:{{ rsvp_public_port }} } # The invite token is the credential and it is in the URL path, so redact # it from the request URI and drop the redirect Location (POSTs 303 back to # /i/) before anything is written. Named "rsvp" so its error logger # (http.log.error.rsvp) can be redirected in the global options. log rsvp { output file /var/log/caddy/rsvp.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format filter { wrap json request>uri regexp ^/i/[^/?#]+ /i/REDACTED request>headers>Referer delete resp_headers>Location delete } } } # Uptime Kuma (Debyltech) - {{ uptime_kuma_server_name }} {{ uptime_kuma_server_name }} { {{ ip_restricted_site() }} handle @local { import common_headers reverse_proxy localhost:3001 { # WebSocket support for live updates flush_interval -1 } } log { output file /var/log/caddy/uptime-kuma.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Uptime Kuma (Personal) - {{ uptime_kuma_personal_server_name }} {{ uptime_kuma_personal_server_name }} { {{ ip_restricted_site() }} handle @local { import common_headers reverse_proxy localhost:3002 { # WebSocket support for live updates flush_interval -1 } } log { output file /var/log/caddy/uptime-kuma-personal.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Graylog Logs - {{ logs_server_name }} {% if not (graylog_enabled | bool) %} # Graylog is disabled (see graylog_enabled in inventories/home/hosts.yml). The # vhost is kept rather than dropped so the name keeps resolving and its # certificate keeps renewing; both handlers answer 503 instead of proxying to a # port nothing is listening on, which would hang and then 502. # # NOTE: this means the external Lambda that POSTs to /gelf for the # `debyltech-api` stream is being turned away. Its events are dropped, not # queued -- nothing else records them. {{ logs_server_name }} { handle /gelf { respond "Log ingest disabled" 503 } handle { respond "Graylog is disabled" 503 } log { output file /var/log/caddy/graylog.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } {% else %} {{ logs_server_name }} { # GELF HTTP endpoint - open for Lambda (auth via header) # Must come BEFORE ip_restricted_site to allow external access @gelf_authorized { path /gelf header X-Gelf-Token "{{ gelf_auth_token }}" } handle @gelf_authorized { reverse_proxy localhost:12202 } # Reject unauthorized GELF requests handle /gelf { respond "Unauthorized" 401 } # IP restriction for Graylog web UI (excludes /gelf which is handled above) @local { remote_ip {{ caddy_local_networks | join(' ') }} } @denied { not remote_ip {{ caddy_local_networks | join(' ') }} not path /gelf } handle @denied { redir https://debyl.io{uri} 302 } handle @local { import common_headers reverse_proxy localhost:9000 } log { output file /var/log/caddy/graylog.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } {% endif %} # ============================================================================ # COMPLEX CONFIGURATIONS # ============================================================================ # Nextcloud - redirect old to new {{ cloud_server_name }} { redir https://{{ cloud_server_name_io }}{uri} 302 } # Nextcloud - {{ cloud_server_name_io }} {{ cloud_server_name_io }} { request_body { max_size {{ caddy_max_request_body_mb }}MB } reverse_proxy localhost:8089 { header_up Host {host} header_up X-Real-IP {remote} # X-Forwarded-For and X-Forwarded-Proto are automatic } header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" Referrer-Policy "same-origin" -X-Powered-By } # Nextcloud specific redirects redir /.well-known/carddav /remote.php/dav 301 redir /.well-known/caldav /remote.php/dav 301 log { output file /var/log/caddy/cloud.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Skudak Nextcloud - serve both domains (migration period) {{ cloud_skudak_server_name }}, {{ cloud_skudak_server_name_new }} { request_body { max_size {{ caddy_max_request_body_mb }}MB } reverse_proxy localhost:8090 { header_up Host {host} header_up X-Real-IP {remote} } header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" Referrer-Policy "same-origin" -X-Powered-By } # Nextcloud specific redirects redir /.well-known/carddav /remote.php/dav 301 redir /.well-known/caldav /remote.php/dav 301 log { output file /var/log/caddy/cloud-skudak.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Gitea - {{ gitea_debyl_server_name }} {{ gitea_debyl_server_name }} { import common_headers reverse_proxy localhost:3100 { flush_interval -1 } log { output file /var/log/caddy/gitea-debyl.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Gitea Skudak - {{ gitea_skudak_server_name }} {{ gitea_skudak_server_name }} { import common_headers reverse_proxy localhost:3101 { flush_interval -1 } log { output file /var/log/caddy/gitea-skudak.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Fulfillr - {{ fulfillr_server_name }} (Static + API with IP restrictions) {{ fulfillr_server_name }} { {{ ip_restricted_site() }} @api { path /api/* } # Handle API requests with CORS for local development handle @api { header { Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" Access-Control-Allow-Credentials "true" } # Handle preflight requests @options { method OPTIONS } handle @options { respond "" 204 } reverse_proxy localhost:9054 } # Serve static files with SPA fallback handle { root * /usr/local/share/fulfillr-site try_files {path} {path}/ /index.html file_server } header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" Referrer-Policy "same-origin" } log { output file /var/log/caddy/fulfillr.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # Fulfillr DEV/staging - {{ fulfillr_dev_server_name }} (Static + API with IP restrictions) {{ fulfillr_dev_server_name }} { {{ ip_restricted_site() }} @api { path /api/* } # Handle API requests with CORS for local development handle @api { header { Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" Access-Control-Allow-Credentials "true" } # Handle preflight requests @options { method OPTIONS } handle @options { respond "" 204 } reverse_proxy localhost:9055 } # Serve static files with SPA fallback handle { root * /usr/local/share/fulfillr-site-dev try_files {path} {path}/ /index.html file_server } header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" Referrer-Policy "same-origin" } log { output file /var/log/caddy/fulfillr-dev.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json } } # ============================================================================ # TEST/STAGING SITES # ============================================================================ # Test Site - test.debyl.io (Public static site hosting, no caching) test.debyl.io { import common_headers root * /srv/test-site try_files {path} {path}/ /index.html file_server # Disable all caching for test sites header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" header Pragma "no-cache" log { output file /var/log/caddy/test.log { roll_size {{ caddy_log_roll_size }} roll_keep {{ caddy_log_roll_keep }} roll_keep_for {{ caddy_log_roll_keep_for }} } format json level {{ caddy_log_level }} } }