Vũ Văn HảiFull-stack · AI-native
GuidesBlog
Discuss a project

© 2026 Vu Van Hai · Written from real deployment experience.

HomeGuidesBlogRSS
  1. Guides
  2. /VPS
  3. /Set up Caddy as a reverse proxy on a VPS with Docker

Set up Caddy as a reverse proxy on a VPS with Docker

Run Caddy as a Dockerized reverse proxy on a fresh VPS: create a shared network, write the Caddyfile and docker-compose file, get HTTPS automatically, then add new domains with a single zero-downtime reload.

Updated: Sep 21, 20265 min read
CaddyDockerVPSNetworking
On this page
  • Why Caddy
  • 1. Install Caddy for the first time
  • Create the Caddy config folder
  • Create a shared Docker network
  • Create the Caddyfile
  • Create docker-compose.yml for Caddy
  • Start Caddy
  • 2. Add a new app / domain
  • Step 1: Attach the new container to Caddy's network
  • Step 2: Declare the domain in the Caddyfile
  • Step 3: Reload Caddy (zero downtime)
  • Important rule: never use localhost in reverse_proxy
  • Troubleshooting

Caddy is a reverse proxy whose standout feature is that it issues and renews SSL (HTTPS) certificates automatically with almost no configuration (zero-config SSL). This guide runs Caddy with Docker on a fresh VPS, then gives you a 3-step workflow for adding a new app/domain without downtime.

Throughout, replace example.com, app.example.com, api.example.com with your own domains, and the container names (my-new-app, my-nextjs-container, my-backend-api) with your real container names.

Why Caddy

  • Gets SSL certificates from Let's Encrypt / ZeroSSL automatically.
  • The config file is extremely short and easy to read.
  • Reloads configuration without interrupting traffic (zero-downtime).

1. Install Caddy for the first time

Create the Caddy config folder

Keep the config files together in one folder so they are easy to manage, edit and back up later.

mkdir -p ~/services/caddy
cd ~/services/caddy

Create a shared Docker network

For Caddy to "see" other containers and proxy traffic to them, they must all sit on the same network. Create a network named caddy-net (or keep the name nginx-proxy if you already use it and want backward compatibility):

docker network create caddy-net

Create the Caddyfile

The Caddyfile is where you declare domains and the services behind them.

cat > ~/services/caddy/Caddyfile << 'EOF'
# =============================================
# Caddy Reverse Proxy Configuration
# =============================================
# Just declare the domain, Caddy takes care of SSL!

# Example 1: proxy to a Next.js app
# app.example.com {
#     reverse_proxy my-nextjs-container:3000
# }

# Example 2: proxy to an API backend
# api.example.com {
#     reverse_proxy my-backend-api:8080
# }
EOF

When you use it for real, swap example.com and the container names for your own domain and app.

Create docker-compose.yml for Caddy

This file defines the Caddy container, the ports to open, and the folders to persist (volumes).

The caddy_data volume matters: it keeps your SSL certificates across restarts. Lose it and Caddy must request them again, which can hit the Let's Encrypt rate limit.

cat > ~/services/caddy/docker-compose.yml << 'EOF'
services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy-proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp" # UDP port 443 must be open if you want HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy-net

volumes:
  caddy_data:
  caddy_config:

networks:
  caddy-net:
    external: true
EOF

Start Caddy

Before starting Caddy, make sure nothing else on the VPS is holding ports 80 and 443 (for example Nginx, Apache, or an old proxy).

If Nginx Proxy Manager is running, stop it to free the ports:

docker stop nginx-proxy-manager-app-1

Start Caddy:

cd ~/services/caddy
docker compose up -d

To check that Caddy is healthy and has obtained its SSL certificate, follow the logs:

docker compose logs -f

Once you see certificate obtained successfully in the log, press Ctrl+C to exit.

2. Add a new app / domain

Whenever you have a new app (say a Docker service listening on port 3000), follow these 3 steps.

Step 1: Attach the new container to Caddy's network

Because both Caddy and your app run in Docker, the best practice is to have the app join Caddy's network (caddy-net) automatically, straight from its docker-compose.yml.

Option 1 (recommended): declare it in docker-compose

Add this networks configuration to the new app's docker-compose.yml:

services:
  my-new-app:
    # ... (image, ports and other settings...)
    networks:
      - caddy-net

networks:
  caddy-net:
    external: true

The external: true flag tells Docker that the caddy-net network already exists outside this project (you created it in part 1). When you run docker compose up -d, the container joins it automatically. That way the app works correctly on any VPS you move it to, with no manual commands.

Option 2 (temporary): connect the network by hand

If the app is already running and you cannot edit its docker-compose right now, run:

docker network connect caddy-net my-new-app

This is only a stopgap: the container loses the network connection as soon as it is recreated or rebuilt.

Step 2: Declare the domain in the Caddyfile

Open the Caddyfile:

nano ~/services/caddy/Caddyfile

Add a new block at the bottom of the file:

app.example.com {
    reverse_proxy my-new-app:3000
}

Step 3: Reload Caddy (zero downtime)

Caddy can reload its config file without restarting the container. Visitors using other sites served by the same Caddy will not notice anything and will not be disconnected.

docker exec caddy-proxy caddy reload --config /etc/caddy/Caddyfile

Done. With this command Caddy picks up the new domain app.example.com, requests an SSL certificate from Let's Encrypt on its own, and starts forwarding requests right away.

Important rule: never use localhost in reverse_proxy

Caddy runs in its own Docker container, so localhost in the Caddyfile is the Caddy container itself, NOT your VPS.

  • Do NOT use reverse_proxy localhost:3000. Here localhost is not the VPS, it is the container running Caddy. Caddy looks for a service on port 3000 inside the Caddy container itself, which produces a 502 Bad Gateway.
  • The right way: reverse_proxy <container-name>:3000. Once Caddy and the app share the caddy-net network, Docker provides internal DNS. Just use the exact container_name declared in the app's docker-compose.yml as the hostname, for example reverse_proxy my-new-app:3000.

Troubleshooting

SymptomFix
502 Bad Gateway even though the app is runningThe Caddyfile points to localhost, or the app has not joined caddy-net - use reverse_proxy <container-name>:<port> and check the network
Caddy will not start because port 80/443 is takenStop Nginx, Apache or the old proxy (e.g. docker stop nginx-proxy-manager-app-1), then run docker compose up -d again
The app loses its connection to Caddy after a rebuildThe network was attached with docker network connect - declare networks in the app's docker-compose.yml instead
Rate limited when requesting SSL certificatesDo not delete the caddy_data volume; it keeps certificates across restarts

Want visitors to see a proper maintenance page instead of a 502 on every deploy? Read Automatic maintenance page with Caddy next.

PreviousSet up a new VPS: the essential secure stepsNextAutomatic maintenance page with Caddy during Docker deploys

Related articles

  • Fix Docker containers that cannot reach PostgreSQL on a VPS

    Docker picks a new network range every time you run docker-compose down and up, so PostgreSQL on the host rejects the container. Diagnose the subnet, open UFW and pg_hba.conf, then allow the whole 172.16.0.0/12 range to fix it once.

    Database

    Database
  • Where to put apps on a VPS: the /opt/apps directory layout

    A simple convention for app code on a VPS: keep each app in its own folder under /opt/apps, chown the parent folder once, and git clone, git pull and .env edits never need sudo again.

    VPS

    VPS
  • Quick-check a VPS's specs with a single command

    A one-line bash script that checks a VPS's OS, CPU, RAM, disk and network in about 15 seconds, plus reference tables that tell you whether each number is weak, fine or strong.

    VPS

    VPS

Written by Vu Van Hai

I'm Hai, a full-stack developer based in Ho Chi Minh City. These guides come from systems I built and run myself. Need to build or untangle something similar? Get in touch.

Discuss a projectMore guides

Spot a mistake or a command that no longer works? Let me know

On this page

  • Why Caddy
  • 1. Install Caddy for the first time
  • Create the Caddy config folder
  • Create a shared Docker network
  • Create the Caddyfile
  • Create docker-compose.yml for Caddy
  • Start Caddy
  • 2. Add a new app / domain
  • Step 1: Attach the new container to Caddy's network
  • Step 2: Declare the domain in the Caddyfile
  • Step 3: Reload Caddy (zero downtime)
  • Important rule: never use localhost in reverse_proxy
  • Troubleshooting