DEV Community

jhot
jhot

Posted on

Caddy Docker Proxy, Like Traefik But Better?

When I first got serious about self-hosting and spun up a bunch of services, I quickly realized that I needed a reverse proxy (it took me some Ducking to realize what a reverse proxy was) so that I could host multiple services without needing to use weird ports. And at that time I chose Caddy v1 because it had a simple configuration spec and automatic HTTPS. As I was migrating to hosting everything in Docker, I kept seeing Traefik recommended but didn't like that it seemed more complicated than Caddy to me. I did, however, like that Traefik allows for defining rules in Docker Compose right alongside the rest of your configuration, so I began to search around for alternatives.

I quickly stumbled upon Caddy-Docker-Proxy and knew it was just what I was looking for. It makes setting up a basic reverse proxy rule a breeze, but allows for the full power of Caddy for services that require a bit beyond the basics. On top of that, it constantly monitors for changes to docker labels so no restarts are needed to pick up changes. To give you a taste of its power and simplicity, let me give some examples (pretty much straight from my personal docker-compose.yml.

Define the caddy container:

services:

  caddy:
    image: lucaslorentz/caddy-docker-proxy:2.3
    ports:
      - 80:80
      - 443:443
    networks:
      - caddy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - caddy_data:/data
    deploy:
      placement:
        constraints:
          - node.role == manager
      replicas: 1
      restart_policy:
        condition: any
    labels:
      caddy.email: my-email@example.com
Enter fullscreen mode Exit fullscreen mode

A basic reverse proxy:

  miniflux:
    image: miniflux/miniflux:latest
    restart: always
    ports:
      - 8014:8080
    networks:
      - caddy
      - internal
    depends_on:
      - postgres
    environment:
      - DATABASE_URL=postgres://miniflux:${MINIFLUX_DB_PASSWORD}@postgres/miniflux?sslmode=disable
      # ...
    labels:
      caddy: rss.example.com
      caddy.reverse_proxy: "{{upstreams 8080}}"
Enter fullscreen mode Exit fullscreen mode

Equivalent in a Caddyfile:

rss.example.com {
  reverse_proxy 172.XXX.XXX.XXX:8080
}
Enter fullscreen mode Exit fullscreen mode

The only gotcha here is to make sure to use the port that the service is running on in the container, not the port you expose to the host. So in this case, Miniflux is running on port 8080 in the container but is exposed to the host machine on port 8014 (for internal testing purposes), so I tell caddy to point to the container's IP on port 8080. {{upstreams}} is a helper provided by caddy-docker-proxy to get the container's IP within the caddy Docker network.

A more complex example:

  nextcloud:
    image: nextcloud:latest
    restart: always
    networks:
      - caddy
      - internal
    ports:
      - 8001:80
    depends_on:
      - mariadb
      - redis
    volumes:
      - nextcloud_data:/var/www/html/data
      - ./apps/nextcloud/config:/var/www/html/config
      - nextcloud_apps:/var/www/html/apps
    environment:
      - MYSQL_PASSWORD=${NEXTCLOUD_DB_PASSWORD}
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_HOST=mariadb
      - REDIS_HOST=redis
      - TRUSTED_PROXIES=172.XXX.XXX.XXX/24 # this is the IP range of my caddy network
      - APACHE_DISABLE_REWRITE_IP=1
    labels:
      caddy: nextcloud.example.com
      caddy.reverse_proxy: "{{upstreams 80}}"
      caddy.0_redir: "/.well-known/carddav /remote.php/dav 301"
      caddy.1_redir: "/.well-known/caldav /remote.php/dav 301"
      caddy.header: "Strict-Transport-Security max-age=15552000"
Enter fullscreen mode Exit fullscreen mode

Equivalent in a Caddyfile:

nextcloud.example.com {
  redir /.well-known/carddav /remote.php/dav 301
  redir /.well-known/caldav /remote.php/dav 301
  header Strict-Transport-Security max-age=15552000
  reverse_proxy 172.XXX.XXX.XXX:80
}
Enter fullscreen mode Exit fullscreen mode

Interestingly, the Nextcloud documentation now includes a Caddy v2 example, but when I set up my configuration I based the rules off the nginx example. The only weird thing about this is the number prefixes before the redir directives. This prevents them from grouping together and also orders them (although it doesn't matter in this case).

Local-only access

Lets say you want the benefits of automatic HTTPS, but don't actually want a service to be accessible from outside your network. Caddy allows for only matched requests to be proxied:

  local-only-service:
    image: asdfasdf:latest
    labels:
      caddy: local-only.example.com
      caddy.@local.remote_ip: 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
      caddy.reverse_proxy: "@local {{upstreams 80}}"
Enter fullscreen mode Exit fullscreen mode

Equivalent in a Caddyfile:

local-only.example.com {
  @local {
    remote_ip 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
  }
  reverse_proxy @local 172.XXX.XXX.XXX:80
}
Enter fullscreen mode Exit fullscreen mode

These examples should cover many of your potential use cases, and there's always the Caddy community forum for any questions on more complex configurations. If you have any helper containers that you can't live without, I would love to hear about them in the comments.

Discussion (0)