Why caddy?
-
Easy to configure: Caddy’s configuration language is simple and easy to use.
-
Automatic HTTPS: Caddy is designed to make HTTPS as easy as possible. Caddy will automatically obtain and renew SSL certificates for your sites using Let’s Encrypt no config or work is needed.
-
Lightweight: Caddy is extremely lightweight and fast. Caddy can handle a lot of traffic while using minimal resources. yes even less than Nginx in some instances, and I say that as a lifelong Nginx fan.
-
Security: Caddy is designed with security in mind. It uses secure defaults like HTTPS and hsts etc by default.
-
Extensibility: Caddy has a plugin system that allows you to extend its functionality. There are many plugins available on their site.
-
Reverse proxy: Caddy’s built-in reverse proxy functionality is powerful and easy to use. It supports load balancing, URL rewriting, and other advanced features.
These are just some of Caddy’s amazing features!
Install Docker and Docker Compose if you haven’t already. View our docker guide herehere & our docker rootless guide herehere.
Scenario: you have a new app you wrote or installed via docker called mycoolapp you want to allow the outside world to connect to this but have a reverse proxy in the middle so you can apply e.g. rate limiting, ssl cert, basic auth etc.
Create a new directory for your Caddy configuration and change to it:
mkdir caddycd caddyI prefer to make these in /opt/docker so for me its:
mkdir -p /opt/docker/caddycd /opt/docker/caddyThis is purely personal preference and you are the one to decide on this location.
Create a file called Caddyfile in this directory with the following contents:
example.com { reverse_proxy mycoolapp:80}If you don’t want to get an SSL cert for this domain automatically for free then use this
example.com:80 { reverse_proxy mycoolapp:80}The above configuration tells Caddy to proxy all requests to example.com to a web server running on port 80 at the hostname mycoolapp. Change these values to match your own configuration.
Create a docker-compose.yml file in the same directory with the following contents:
version: '3'services: caddy: image: caddy container_name: caddy ports: - "80:80" - "443:443" environment: - ACME_AGREE=true restart: unless-stopped volumes: - ./Caddyfile:/etc/caddy/Caddyfile - ./caddy_data:/data - ./caddy_config:/config networks: - public
networks: public: external: trueThis configuration sets up a Docker container running Caddy with the Caddyfile we created earlier mounted as a volume along with two other folders needed by caddy. the env variable of acme_agree lets you accept the letsencrypt EULA.
You need to create a new network for caddy and for any other containers you want accessible via this reverse proxy.
docker network create publicdocker network connect public caddydocker network connect public mycoolappThe first command will create a new network named public, the second command will connect your caddy to the public network you just created and the third command will connect your app named mycoolapp to this network also so caddy can access it.
Start the Caddy container via the following command which will start the Caddy container in detached mode, meaning it will run in the background. and it will show you the logs from caddy so you can make sure there were no errors. hit CTRL + C to close the logs.
docker compose up -d && docker compose logs -fOpen a web browser and navigate to http://example.com. You should see the content served by the web server running on port 80 at the mycoolapp hostname.
When It Doesn’t Work: Caddy Can’t Reach Your App
The most common thing that trips people up here is Caddy throwing a dial tcp: lookup mycoolapp: no such host error. You’ve got the Caddyfile right, the Compose file looks fine, but Caddy just can’t see your app. Nine times out of ten it’s a Docker network problem.
Here’s what to check:
1. Make sure the network actually exists and both containers are on it.
docker network inspect publicLook for your caddy and mycoolapp containers in the Containers section. If either one is missing, it’s not on the network — connect it:
docker network connect public mycoolapp2. Use the container name, not the host IP.
In your Caddyfile, mycoolapp:80 works because Docker’s internal DNS resolves container names within the same network. If you put 127.0.0.1:3000 or localhost:3000 there instead, you’re pointing at Caddy’s own loopback, not your app. Container name is the right move.
3. Watch the actual port your app listens on.
If your app’s Compose file exposes port 3000 internally but you’ve written mycoolapp:80 in the Caddyfile, Caddy will get connection refused. Check what port the app actually binds to inside the container — that’s the port you use in the reverse proxy directive, not the host-side port.
example.com { # app listens on 3000 inside the container, not 80 reverse_proxy mycoolapp:3000}4. Reload Caddy after changes, don’t just restart.
Caddy supports hot-reload — you don’t need to bounce the whole container every time you tweak the Caddyfile:
docker exec caddy caddy reload --config /etc/caddy/CaddyfileThis is faster and doesn’t cause a blip in active connections. Save the full restart for when you’re changing volumes or environment variables.