Subdomains and Caddy

A guide on how to setup subdomains with Caddy

Subdomains and 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 ufw and or by design)
  • caddy shuttles any traffic coming from port 80 and 443 into app1 and app2
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
}

Built by me.