SSH Reverse proxy with Caddy on Docker container

1. The problem I’m having:

I am currently using Caddy on a VPS, running it in a Docker container. So far, I have successfully configured it as a reverse proxy to route traffic from different FQDNs to various Docker containers on the same VPS. Everything is working smoothly.

Now, I would like to set up Caddy to act as a reverse proxy for another server that connects to this VPS via reverse tunneling.

2. Error messages and/or full log output:

caddy  | {"level":"info","ts":1743593370.2042847,"msg":"using config from file","file":"/etc/caddy/Caddyfile"}
caddy  | Error: adapting config using caddyfile: ambiguous site definition: plane.k.b4m.jp

3. Caddy version:

v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=

4. How I installed and ran Caddy:

a. System environment:

Ubuntu 24.04.2 LTS
Docker version 28.0.4, build b8034c0

b. Command:

docker compose up -d

c. Service/unit/compose file:

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: always
    ports:
      - "9000:9000" 
      - "9443:9443" 
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - caddy_network

  caddy:
    image: caddy:latest
    container_name: caddy
    restart: always
    ports:
      - "80:80"   # HTTP
      - "443:443" # HTTPS
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy_network
volumes:
  portainer_data:
  caddy_data:
  caddy_config:

networks:
  caddy_network:
    driver: bridge

d. My complete Caddy config:

{
    email REDACTED
    debug
}

docker.k.b4m.jp {
    reverse_proxy portainer:9000
}

n8n.k.b4m.jp {
    reverse_proxy n8n:5678
}

bitwarden.k.b4m.jp {
    reverse_proxy vaultwarden:80
}

git.k.b4m.jp {
    reverse_proxy gitea:3000
}

obsidian.k.b4m.jp {
    reverse_proxy couchdb:5984
}

photo.k.kohki.org {
    reverse_proxy photoprism:2342
}

# This one is only not working fine.
plane.k.b4m.jp {
    reverse_proxy localhost:81
}

5. Links to relevant resources:

kohki@mbam3 ~ % curl -vL https://plane.k.b4m.jp/
* Host plane.k.b4m.jp:443 was resolved.
* IPv6: (none)
* IPv4: 163.44.110.55
*   Trying 163.44.110.55:443...
* Connected to plane.k.b4m.jp (163.44.110.55) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=plane.k.b4m.jp
*  start date: Mar 27 06:28:55 2025 GMT
*  expire date: Jun 25 06:28:54 2025 GMT
*  subjectAltName: host "plane.k.b4m.jp" matched cert's "plane.k.b4m.jp"
*  issuer: C=US; O=Let's Encrypt; CN=E5
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://plane.k.b4m.jp/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: plane.k.b4m.jp]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: plane.k.b4m.jp
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 502
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Wed, 02 Apr 2025 11:41:44 GMT
<
* Connection #0 to host plane.k.b4m.jp left intact

Don’t mount the Caddyfile. There’s a note about this on the Docker Hub page.

https://hub.docker.com/_/caddy/

I suspect the mounting issue is hitting you.

Hi Mohammed90, thanks for your information.

I have changed config files like this. Not mount Caddyfile directory, mount the directory contains Caddyfile.

New compose.yml

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: always
    ports:
      - "9000:9000"
      - "9443:9443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - caddy_network

  caddy:
    image: caddy:latest
    container_name: caddy
    restart: always
    ports:
      - "80:80"   # HTTP
      - "443:443" # HTTPS
    volumes:
      - ./conf:/etc/caddy/
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy_network
volumes:
  portainer_data:
  caddy_data:

Caddyfile

./conf/Caddyfile
Only directory is changed, content of file is no modified.

{
    email REDACTED
    debug
}

docker.k.b4m.jp {
    reverse_proxy portainer:9000
}

n8n.k.b4m.jp {
    reverse_proxy n8n:5678
}

bitwarden.k.b4m.jp {
    reverse_proxy vaultwarden:80
}

git.k.b4m.jp {
    reverse_proxy gitea:3000
}

obsidian.k.b4m.jp {
    reverse_proxy couchdb:5984
}

plane.k.b4m.jp {
    reverse_proxy localhost:81
}

photo.k.kohki.org {
    reverse_proxy photoprism:2342
}

Debug

root@vm-a981b02d-0b:~/docker/caddy# curl -vL http://localhost:81
* Host localhost:81 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:81...
* Connected to localhost (::1) port 81
> GET / HTTP/1.1
> Host: localhost:81
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.25.0
< Date: Thu, 03 Apr 2025 03:11:27 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 15753
< Connection: keep-alive
< X-Frame-Options: SAMEORIGIN
< Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding
< x-nextjs-cache: HIT
< X-Powered-By: Next.js
< Cache-Control: s-maxage=31536000, stale-while-revalidate
< ETag: "158sof022xyc3m"
< X-Content-Type-Options: nosniff
< Referrer-Policy: no-referrer-when-downgrade
< Permissions-Policy: interest-cohort=()
< Strict-Transport-Security: max-age=31536000; includeSubDomains
< X-Forwarded-Proto: http
< X-Forwarded-Host: localhost
< X-Forwarded-For: 172.19.0.1
< X-Real-IP: 172.19.0.1
<
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover, user-scalable=no"/><link rel="stylesheet" href="/_next/static/css/25cb2d93346d4c5a.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/4f8b36b31c991eb9.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-920d560d1aef6858.js"/>
...

Curl from client machine

kohki@mbam3 ~ % curl -vL https://plane.k.b4m.jp/
* Host plane.k.b4m.jp:443 was resolved.
* IPv6: (none)
* IPv4: 163.44.110.55
*   Trying 163.44.110.55:443...
* Connected to plane.k.b4m.jp (163.44.110.55) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=plane.k.b4m.jp
*  start date: Mar 27 06:28:55 2025 GMT
*  expire date: Jun 25 06:28:54 2025 GMT
*  subjectAltName: host "plane.k.b4m.jp" matched cert's "plane.k.b4m.jp"
*  issuer: C=US; O=Let's Encrypt; CN=E5
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://plane.k.b4m.jp/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: plane.k.b4m.jp]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: plane.k.b4m.jp
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 502
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Thu, 03 Apr 2025 03:17:42 GMT
<
* Connection #0 to host plane.k.b4m.jp left intact

It may certifications issues are no problem. I guess main issue is Caddy on Docker is not reseolve to localhost:81 cause some missing config or problem, right?

The localhost in containers means this container. If you want to proxy to the host machine, use docker.internal.host.

I appriciate your suggest, I learned host.docker.internal at this time.

So, I changed configs like below.

Caddyfile

{
    email kohki.shikata@gmail.com
    debug
}

docker.k.b4m.jp {
    reverse_proxy portainer:9000
}

n8n.k.b4m.jp {
    reverse_proxy n8n:5678
}

bitwarden.k.b4m.jp {
    reverse_proxy vaultwarden:80
}

git.k.b4m.jp {
    reverse_proxy gitea:3000
}

obsidian.k.b4m.jp {
    reverse_proxy couchdb:5984
}

plane.k.b4m.jp {
    reverse_proxy host.docker.internal:81
}

photo.k.kohki.org {
    reverse_proxy photoprism:2342
}

compose.yml

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: always
    ports:
      - "9000:9000"  # PortainerのWeb UI
      - "9443:9443"  # HTTPS用
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - caddy_network

  caddy:
    image: caddy:latest
    container_name: caddy
    restart: always
    ports:
      - "80:80"   # HTTP
      - "443:443" # HTTPS
    volumes:
      - ./conf:/etc/caddy/
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy_network
    extra_hosts:
      - "host.docker.internal:host-gateway"

volumes:
  portainer_data:
  caddy_data:
  caddy_config:

networks:
  caddy_network:
    driver: bridge

Caddy’s log

Then, access plane.k.b4m.jp, docker compose logs caddy says so

caddy  | {"level":"error","ts":1743999893.1796706,"logger":"http.log.error","msg":"dial tcp: lookup host.docker.internal on 127.0.0.11:53: no such host","request":{"remote_ip":"175.131.59.185","remote_port":"53043","client_ip":"175.131.59.185","proto":"HTTP/2.0","method":"GET","host":"plane.k.b4m.jp","uri":"/","headers":{"Sec-Fetch-Dest":["document"],"Priority":["u=0, i"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"],"Sec-Ch-Ua":["\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"Google Chrome\";v=\"134\""],"Sec-Fetch-Site":["none"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Cookie":["REDACTED"],"Sec-Ch-Ua-Mobile":["?0"],"Sec-Fetch-Mode":["navigate"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-User":["?1"],"Upgrade-Insecure-Requests":["1"],"Accept-Language":["ja,en-US;q=0.9,en;q=0.8"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"plane.k.b4m.jp"}},"duration":0.003598529,"status":502,"err_id":"ddz2twkzc","err_trace":"reverseproxy.statusError (reverseproxy.go:1373)"}

I think this shifts the focus of the problem to the reverse proxy itself, right?

No, now you have a DNS issue within Docker. For some reason it fails to resolve host.docker.internal. Your Docker setup may have issues.

OK, thanks. I’ll try it.