Every request from the internet enters a Yard host through one program: Caddy. This chapter explains what Caddy does for Yard, the one file each app gets, and why Yard stays out of Caddy’s way.
What Caddy does#
Caddy is a web server that does HTTPS by default. Give it a domain name, and it gets a certificate from Let’s Encrypt, renews it before it expires, and serves the name over TLS. Nobody runs certbot and nobody gets the expiry email.
On a Yard host, Caddy has three jobs:
It holds port 443, for HTTP/1.1, HTTP/2, and HTTP/3.
It gets and renews the certificates for every name it serves.
It sends each name to a container, by the container’s name on the
yardnetwork.
That is all. Caddy does not know about deploys, repos, or Yard’s records. It knows names and where to send them.
One file per app#
Each app has one site file, its site file. It is plain Caddyfile, the format Caddy’s own docs use. Here is the one yard app new --caddy notes.example.com writes:
notes.example.com { reverse_proxy notes-app-1:8000 { lb_try_duration 10s lb_try_interval 250ms }}Read it as a sentence: requests for notes.example.com go to the container notes-app-1 on port 8000. While that container does not answer, hold each request and try again every 250 ms, for up to ten seconds.
Those last two lines are what make a deploy invisible. During a swap, the old container stops and the new one starts. For a second or two, nothing answers on notes-app-1:8000. Without the retry, a visitor in that window would get a 502. With it, the visitor waits a moment and gets the page.
To print the file or change it:
yard caddy catyard caddy edityard caddy edit opens the file in your $EDITOR. When you save, Yard sends the text to the host, and Caddy loads it. With no terminal, the command reads the new file from stdin, so a script can pipe one in.
Caddy checks every change#
Yard stores the text of each file and never parses it. That is a deliberate choice. Caddy’s format is rich, and only Caddy knows it fully. So Yard lets Caddy judge.
When you save a file, Yard does this:
It writes the file to
/srv/yard/apps/yard/caddy/config/sites/<name>.caddy.It asks Caddy, through Caddy’s admin socket, to load the whole config again.
Caddy reads every file and checks the whole config before it serves any of it.
If Caddy refuses, Yard writes the old file back and shows you Caddy’s error, with the file and the line.
A typo never takes a site down. The old config keeps serving while you fix the new one. When the edit came from yard caddy edit, the command asks whether to edit again and opens the editor on your text, so nothing you typed is lost.
An empty file removes the site. The names stop answering, and the app keeps running.
How Caddy is set up#
Caddy starts from one main file, ops/Caddyfile in the Yard repo:
{ admin unix//run/caddy/admin.sock auto_https disable_redirects cert_issuer acme { disable_http_challenge } servers :443 { protocols h1 h2 h3 }} import /config/sites/*.caddyEach line has a reason:
The admin API is a unix socket, not a TCP port. Only processes that can open the file, Yard among them, can change Caddy’s config.
disable_redirectsturns off Caddy’s usual redirect from port 80 to 443. Port 80 is closed on a Yard host, so there is nothing to redirect.disable_http_challengemakes Caddy prove each domain over port 443, with the TLS-ALPN challenge. That is why port 80 can stay closed.protocols h1 h2 h3turns on HTTP/3 next to HTTP/1.1 and HTTP/2.import /config/sites/*.caddypulls in every app’s file. Adding an app is adding a file.
Caddy runs with --resume. After a restart, it serves the last config it loaded, so a reboot never drops a site. Yard also loads the main file again each time it starts. So the files on disk are always the truth, and Caddy’s memory follows them.
Caddy’s container sees only what it needs: its config directory, its certificates, its admin socket, and the main file. It sees no app’s env file and no app’s data.
Certificates#
Caddy asks for a certificate the moment a name appears in a loaded file. For that to work, the name must already point at the VM. So point the DNS first, then write the file. A wildcard record such as *.example.com points every future name at once, and you never think about DNS again.
yard status --full checks each name for you. It has a dns row, a certificate row, and an https row per host. The https row sends a real request, and an answer of 500 or above fails it. That catches a 502 from an app that does not answer.
Caddy keeps its certificates in /srv/yard/apps/yard/caddy/data, so they survive every deploy of Caddy and of Yard.
The ports#
Caddy listens on port 443 inside its container. Rootless Podman cannot publish a port below 1024, so the container is published on 8443, for TCP and UDP. An nftables rule on the host redirects public 443 to 8443. The host shows the rule.
Why not let Caddy listen on 8443 inside the container too? Because of HTTP/3. Caddy tells each browser that HTTP/3 lives on the port Caddy listens on. If Caddy listened on 8443, browsers would try 8443, which is not the public port. Inside the container, Caddy listens on 443, advertises 443, and the redirect makes 443 true.
More than the default#
The file is plain Caddyfile, so it can do whatever Caddy does. A few things come up often.
A second name, such as www, goes on the first line:
notes.example.com, www.notes.example.com { reverse_proxy notes-app-1:8000 { lb_try_duration 10s lb_try_interval 250ms }}A password in front of an app that has no login:
status.example.com { basic_auth { admin $2a$14$… } reverse_proxy uptime-kuma-app-1:3001}caddy hash-password makes the hash. Run it in Caddy’s container:
sudo -u yard podman exec -it yard-caddy-1 caddy hash-passwordA login service in front of several apps, with forward_auth:
admin.example.com { forward_auth auth-app-1:8000 { uri /verify copy_headers X-User } reverse_proxy admin-app-1:8000}A second container of the same app under a path, with handle_path. Caddy can only reach a container on the yard network, so the files service must join it too:
notes.example.com { handle_path /files/* { reverse_proxy notes-files-1:8080 } reverse_proxy notes-app-1:8000}How --caddy picks the port#
yard app new --caddy <host> writes the default site file after the first good deploy, because only then does Yard know the port. Yard asks Podman which TCP port the running app container exposes, and dials that one. Vaultwarden exposes 80, so its file dials vault-app-1:80. A repo app built from Yard’s files exposes 8000. A repo app that exposes no port gets 8000 too.
An image or compose URL app that exposes no port, or several, gets no file. The command prints the file with PORT in its place, and yard caddy edit fills in the real one. --caddy repeats for several names. With --no-deploy, it is refused, because there is no container to ask.
What Caddy does not do here#
Only HTTPS on 443. A service that needs another public port, such as mail on 25 or a game server, is outside what a Yard host serves.
No login by default. The default file forwards every request. A service with an admin page needs its own login, or a
basic_authorforward_authblock.A deploy never touches Caddy. The file names the container, and the new container keeps the name. If you rename an app or change its port, change the file.
By the way#
yard app rmremoves the app’s site file first, so the names stop answering before the containers go.yard statuslists the hosts of each app. Yard asks Caddy to read each file once, and asks again only when the file changes.A site file is at most 64 KiB.