orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1name: orchard-edge
2
3# The shared edge: one tunnel and one Caddy in front of every site in this repo,
4# plus the ntfy every alert is published to. cloudflared's state lives in a named
5# volume seeded by setup-tunnel.sh rather than a bind mount off the host.
6
7services:
8 caddy:
9 build: ./caddy
10 container_name: orchard-caddy
11 restart: unless-stopped
12 networks: [edge]
13 security_opt:
14 - no-new-privileges:true
15 # Caddy access-logs every request for every hostname, so without this the
16 # container log is the one unbounded thing in the system.
17 logging:
18 driver: json-file
19 options:
20 max-size: "10m"
21 max-file: "3"
22
23 # The alert path. status and logging publish by container name on this bridge
24 # with a write-only token, and a phone reads over the tunnel with a read-only
25 # account. setup-ntfy.sh creates both, and the config is in ntfy/server.yml.
26 ntfy:
27 build: ./ntfy
28 container_name: orchard-ntfy
29 restart: unless-stopped
30 command: serve
31 volumes:
32 - ntfy:/var/lib/ntfy
33 # No published port, like every site here. Caddy allows the subscribe routes
34 # on the public hostname and refuses every publish route.
35 networks: [edge]
36 security_opt:
37 - no-new-privileges:true
38 healthcheck:
39 test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:8000/v1/health"]
40 interval: 30s
41 timeout: 5s
42 retries: 3
43 start_period: 10s
44 logging:
45 driver: json-file
46 options:
47 max-size: "10m"
48 max-file: "3"
49
50 cloudflared:
51 image: cloudflare/cloudflared:2026.8.3@sha256:9be48e4b4e996da851bf78f7782bfab150dd4d8889e469d004802e7d7afb63b1
52 container_name: orchard-cloudflared
53 restart: unless-stopped
54 command: tunnel --config /etc/cloudflared/config.yml run
55 volumes:
56 - cloudflared:/etc/cloudflared:ro
57 networks: [edge]
58 depends_on: [caddy]
59 security_opt:
60 - no-new-privileges:true
61 logging:
62 driver: json-file
63 options:
64 max-size: "10m"
65 max-file: "3"
66
67networks:
68 edge:
69 name: orchard-edge
70
71volumes:
72 # Created and populated by setup-tunnel.sh before this stack first runs, so
73 # compose attaches to it rather than owning it.
74 cloudflared:
75 name: orchard-cloudflared
76 external: true
77
78 # Data rather than cache: cache.db holds alerts already delivered so an
79 # offline phone catches up, and user.db holds the two accounts. Lose this and
80 # setup-ntfy.sh has to run again and both publishers need a new token.
81 ntfy:
82 name: orchard-ntfy-data