initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthias Johnson 2026-02-27 15:09:25 -07:00
commit 75891c3271
129 changed files with 8046 additions and 0 deletions

View file

@ -0,0 +1,14 @@
---
# Caddy metrics port (scraped by Prometheus)
caddy_metrics_port: 9000
# Static sites served by Caddy — override in config.yml
# Each entry gets /srv/caddy/sites/<domain>/ and www → apex redirect
caddy_sites:
- "{{ domain }}"
# Service domain defaults — override individually in config.yml or overrides.yml
webmail_domain: "webmail.{{ domain }}"
rspamd_domain: "rspamd.{{ domain }}"
goaccess_domain: "stats.{{ domain }}"
grafana_domain: "watch.{{ domain }}"

View file

@ -0,0 +1,8 @@
---
- name: Restart Caddy
# full restart (not caddy reload) avoids stale bind-mount inodes when Caddyfile changes on host
community.docker.docker_compose_v2:
project_src: /srv/caddy
state: present
build: never

104
roles/caddy/tasks/main.yml Normal file
View file

@ -0,0 +1,104 @@
- name: Allow HTTP traffic
ufw:
rule: allow
port: 80
proto: tcp
- name: Allow HTTPS traffic
ufw:
rule: allow
port: 443
proto: tcp
- name: Allow HTTPS/QUIC (HTTP/3) traffic
ufw:
rule: allow
port: 443
proto: udp
- name: Create Caddy directories
file:
path: "/srv/caddy/{{ item }}"
state: directory
owner: root
group: docker
mode: "0755"
loop:
- ""
- data
- config
- sites
- name: Create site roots
file:
path: "/srv/caddy/sites/{{ item }}"
state: directory
owner: root
group: docker
mode: "0775" # also allow members of the docker group to write
loop: "{{ caddy_sites }}"
- name: Install Caddyfile
template:
src: Caddyfile.j2
dest: /srv/caddy/Caddyfile
owner: root
group: docker
mode: "0644"
notify: Restart Caddy
tags: config
- name: Check for cached goaccess hash
ansible.builtin.stat:
path: /srv/caddy/.goaccess_hash
register: _goaccess_hash_stat
when: enable_goaccess | default(true)
- name: Read goaccess hash from cache
ansible.builtin.slurp:
src: /srv/caddy/.goaccess_hash
register: _goaccess_hash_file
when: enable_goaccess | default(true) and _goaccess_hash_stat.stat.exists
- name: Set goaccess hash fact from cache
ansible.builtin.set_fact:
caddy_goaccess_hash_stdout: "{{ _goaccess_hash_file.content | b64decode | trim }}"
when: enable_goaccess | default(true) and _goaccess_hash_stat.stat.exists
- name: Generate goaccess password hash
ansible.builtin.command:
argv:
- docker
- run
- --rm
- "caddy:{{ caddy_version }}"
- caddy
- hash-password
- --plaintext
- "{{ goaccess_password }}"
register: _goaccess_hash_result
changed_when: false
no_log: true
when: enable_goaccess | default(true) and not _goaccess_hash_stat.stat.exists
- name: Cache goaccess hash
ansible.builtin.copy:
content: "{{ _goaccess_hash_result.stdout }}"
dest: /srv/caddy/.goaccess_hash
mode: "0600"
when: enable_goaccess | default(true) and not _goaccess_hash_stat.stat.exists
- name: Set goaccess hash fact from generation
ansible.builtin.set_fact:
caddy_goaccess_hash_stdout: "{{ _goaccess_hash_result.stdout }}"
when: enable_goaccess | default(true) and not _goaccess_hash_stat.stat.exists
- name: Deploy Caddy compose.yml
template:
src: compose.yml.j2
dest: /srv/caddy/compose.yml
owner: root
group: docker
mode: "0644"
notify: Restart Caddy
tags: config

View file

@ -0,0 +1,124 @@
{
email {{ admin_user }}@{{ domain }}
log {
output stdout
}
metrics {
per_host
}
}
(access_log) {
log
}
:{{ caddy_metrics_port }} {
metrics
}
{% for site in caddy_sites %}
# Redirect www → apex
www.{{ site }} {
import access_log
redir https://{{ site }}{uri} permanent
}
{{ site }} {
import access_log
root * /srv/sites/{{ site }}
encode zstd gzip
file_server
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
{% if site == domain and enable_tuwunel | default(false) %}
handle /.well-known/matrix/server {
header Content-Type application/json
respond `{"m.server": "{{ tuwunel_domain }}:443"}`
}
handle /.well-known/matrix/client {
header Content-Type application/json
header Access-Control-Allow-Origin *
respond `{"m.homeserver": {"base_url": "https://{{ tuwunel_domain }}"}}`
}
{% endif %}
}
{% endfor %}
{% if enable_mail | default(false) %}
{{ webmail_domain }} {
import access_log
reverse_proxy rainloop:{{ rainloop_port }}
}
{{ rspamd_domain }} {
import access_log
reverse_proxy mailserver:{{ rspamd_port }}
}
{% endif %}
{% if enable_forgejo | default(false) %}
{{ forgejo_domain }} {
import access_log
reverse_proxy forgejo:{{ forgejo_port }}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
{% endif %}
{% if enable_monitoring | default(false) %}
{{ grafana_domain }} {
import access_log
reverse_proxy grafana:{{ grafana_port }} {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
{% endif %}
{% if enable_tuwunel | default(false) %}
{{ tuwunel_domain }} {
import access_log
reverse_proxy tuwunel:{{ tuwunel_port }}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
{% endif %}
{% if enable_radicale | default(false) %}
{{ radicale_domain }} {
import access_log
redir /.well-known/caldav / permanent
redir /.well-known/carddav / permanent
reverse_proxy radicale:{{ radicale_port }}
}
{% endif %}
{% if enable_goaccess | default(false) %}
{{ goaccess_domain }} {
import access_log
root * /srv/goaccess/reports
file_server browse
basic_auth {
{$GOACCESS_USER} {$GOACCESS_HASH}
}
}
{% endif %}

View file

@ -0,0 +1,67 @@
services:
caddy:
image: caddy:{{ caddy_version }}
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost:{{ caddy_metrics_port }}/metrics || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
volumes:
- /srv/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- /srv/caddy/data:/data
- /srv/caddy/config:/config
- /srv/caddy/sites:/srv/sites:ro
- /srv/goaccess/reports:/srv/goaccess/reports:ro
environment:
{% if enable_goaccess | default(true) %}
GOACCESS_USER: "{{ goaccess_user }}"
GOACCESS_HASH: "{{ caddy_goaccess_hash_stdout | replace('$', '$$') }}"
{% endif %}
networks:
- caddy
{% if enable_mail | default(true) %}
- webmail
{% endif %}
{% if enable_forgejo | default(true) %}
- git
{% endif %}
{% if enable_monitoring | default(true) %}
- monitoring
{% endif %}
{% if enable_tuwunel | default(true) %}
- tuwunel
{% endif %}
{% if enable_radicale | default(false) %}
- radicale
{% endif %}
networks:
caddy:
external: true
{% if enable_mail | default(true) %}
webmail:
external: true
{% endif %}
{% if enable_forgejo | default(true) %}
git:
external: true
{% endif %}
{% if enable_monitoring | default(true) %}
monitoring:
external: true
{% endif %}
{% if enable_tuwunel | default(true) %}
tuwunel:
external: true
{% endif %}
{% if enable_radicale | default(false) %}
radicale:
external: true
{% endif %}