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.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.