Subdomains and Caddy
A guide on how to setup subdomains with Caddy
3 Minute Read
Henry Chen
8 March 2026
Overview
- subdomains handy for separate apps on the same domain
- can be deployed on any server setup; opted for docker cos its easier to explain how things are networked together
- Caddy is a web server and reverse proxy built as a replacement for NGINX; it also does automatic lets encrypt certificate renewals out of the box
- Caddy acts as a reverse proxy that TLS terminates public packets. it assumes encryption is only needed for hte public leg of the packet journey
- public packets will never see the internal workings of hte server
- This guide will outline the minimal configuration changes needed to get subdomains working
Top level Example
Domain Configuration
- top level domain configured to shuttle traffic to my static IP.
- i set the subdomain to also go to my server
- in my case, i had to wait overnight for the DNS records to update before i could test... you might have the same problem
Sample Docker Compose Setup
this config sets up the caddy server then two example servers (app1 and app2). the webservers are configured as flask apps however, this can be replicated with any application that has an exposed port.
- you can infer the server setup; this is just for simplicity when explaining how the system is networked together;
- apps run on speicific ports; exposed within the network but hidden to the public (i.e. not an exposed port on the server)
- caddy is aware of the exposed ports but the public user isnt (either by
ufwand or by design) - caddy shuttles any traffic coming from port 80 and 443 into
app1andapp2
services:
caddy:
image: caddy:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/caddy_data:/data
- ./caddy/caddy_config:/config
depends_on:
- app1
- app2
app1:
build: ./app1
restart: unless-stopped
expose:
- 3000
app2:
build: ./app2
restart: unless-stopped
expose:
- 4000
Caddy File Config
this configures how caddy moves packets between your servers. i.e. when it detects something on port 80/443, it then looks at the subdomain its trying to shuttle to? then if it detects app1, it moves it to the locally hosted app1:3000 url. you can opt to change app1:3000 to any.ip.addr:port you want.
// ./caddy/Caddyfile
app1.yourdomain.xyz { // change this to the actual domain name you'll use
reverse_proxy app1:3000
}
app2.yourdomain.xyz {
reverse_proxy app2:4000
}