TLS End of file error using docker compose

1. The problem I’m having:

I set up Caddy through docker compose and added a simple index.html page for testing. It appears that it isn’t getting a TLS certificate.

2. Error messages and/or full log output:

On my local machine:

[ruby@nixos:~]$ curl -vL https://madcatter.dev/
* Host madcatter.dev:443 was resolved.
* IPv6: 2a0f:f01:206:1ec::
* IPv4: 92.113.145.235
*   Trying [2a0f:f01:206:1ec::]:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (OUT), TLS alert, decode error (562):
* TLS connect error: error:0A000126:SSL routines::unexpected eof while reading
* closing connection #0
curl: (35) TLS connect error: error:0A000126:SSL routines::unexpected eof while reading

On the VPS:

ruby@madcatter:~/caddy$ docker compose run caddy
[+]  1/1te 1/1
 ✔ Network caddy_default Created                                                                                                                                                    0.0s
Container caddy-caddy-run-70ee879c0a6e Creating 
Container caddy-caddy-run-70ee879c0a6e Created 
2026/03/11 22:54:34.389 INFO    maxprocs: Leaving GOMAXPROCS=6: CPU quota undefined
2026/03/11 22:54:34.389 INFO    GOMEMLIMIT is updated   {"GOMEMLIMIT": 8655486566, "previous": 9223372036854775807}
2026/03/11 22:54:34.389 INFO    using config from file  {"file": "/etc/caddy/Caddyfile"}
2026/03/11 22:54:34.389 INFO    adapted config to JSON  {"adapter": "caddyfile"}
2026/03/11 22:54:34.389 INFO    admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2026/03/11 22:54:34.390 WARN    http.auto_https server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server {"server_name": "srv0", "http_port": 80}
2026/03/11 22:54:34.390 INFO    tls.cache.maintenance   started background certificate maintenance      {"cache": "0x25050e0fd500"}
2026/03/11 22:54:34.390 WARN    http    HTTP/2 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2026/03/11 22:54:34.390 WARN    http    HTTP/3 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2026/03/11 22:54:34.390 INFO    http.log        server running  {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/03/11 22:54:34.390 INFO    autosaved config (load with --resume flag)      {"file": "/config/caddy/autosave.json"}
2026/03/11 22:54:34.390 INFO    serving initial configuration
2026/03/11 22:54:34.391 INFO    tls     storage cleaning happened too recently; skipping for now        {"storage": "FileStorage:/data/caddy", "instance": "19b51cbf-0d78-4e65-beae-874cf473674a", "try_again": "2026/03/12 22:54:34.391", "try_again_in": 86399.99999972}
2026/03/11 22:54:34.391 INFO    tls     finished cleaning storage units
journalctl -u caddy --no-pager | less +G

3. Caddy version:

ruby@madcatter:~/caddy$ docker compose exec caddy caddy version
v2.11.2 h1:iOlpsSiSKqEW+SIXrcZsZ/NO74SzB/ycqqvAIEfIm64=

4. How I installed and ran Caddy:

a. System environment:

ruby@madcatter:~/caddy$ uname -a
Linux madcatter.dev 6.12.73+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.73-1 (2026-02-17) x86_64 GNU/Linux
ruby@madcatter:~/caddy$ docker -v
Docker version 29.3.0, build 5927d80
ruby@madcatter:~/caddy$ docker info
Client: Docker Engine - Community
 Version:    29.3.0
 Context:    rootless
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.31.1
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v5.1.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 2
  Running: 2
  Paused: 0
  Stopped: 0
 Images: 4
 Server Version: 29.3.0
 Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
  /home/ruby/.config/cdi
  /run/user/1001/cdi
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: dea7da592f5d1d2b7755e3a161be07f43fad8f75
 runc version: v1.3.4-0-gd6d73eb8
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  rootless
  cgroupns
 Kernel Version: 6.12.73+deb13-amd64
 Operating System: Debian GNU/Linux 13 (trixie)
 OSType: linux
 Architecture: x86_64
 CPUs: 6
 Total Memory: 8.957GiB
 Name: madcatter.dev
 ID: 0615bdcd-7635-4510-8687-53bf562caeec
 Docker Root Dir: /home/ruby/.local/share/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false
 Firewall Backend: iptables

WARNING: No cpuset support
WARNING: No io.weight support
WARNING: No io.weight (per device) support
WARNING: No io.max (rbps) support
WARNING: No io.max (wbps) support
WARNING: No io.max (riops) support
WARNING: No io.max (wiops) support

b. Command:

cd ~/caddy
docker compose up -d

c. Service/unit/compose file:

services:
  caddy:
    image: caddy:2.11.2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./conf:/home/ruby/caddy/conf
      - ./site:/home/ruby/caddy/site
      - caddy_data:/data
      - caddy_config:/config
    stdin_open: true # docker run -i
    tty: true        # docker run -t

volumes:
  caddy_data:
  caddy_config:

d. My complete Caddy config:

{
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
    debug
}

madcatter.dev {
    root /home/ruby/caddy/site
    file_server
}

5. Links to relevant resources:

Can you please run:

docker exec -ti caddy cat /config/caddy/autosave.json /etc/caddy/Caddyfile

and share the result?

Ohhh, now I see what’s wrong, but I’m not sure how to fix it.

{"apps":{"http":{"servers":{"srv0":{"listen":[":80"],"routes":[{"handle":[{"handler":"vars","root":"/usr/share/caddy"},{"handler":"file_server","hide":["/etc/caddy/Caddyfile"]}]}]}}}}}# The Caddyfile is an easy way to configure your Caddy web server.
#
# Unless the file starts with a global options block, the first
# uncommented line is always the address of your site.
#
# To use your own domain name (with automatic HTTPS), first make
# sure your domain's A/AAAA DNS records are properly pointed to
# this machine's public IP, then replace ":80" below with your
# domain name.

:80 {
        # Set this path to your site's directory.
        root * /usr/share/caddy

        # Enable the static file server.
        file_server

        # Another common task is to set up a reverse proxy:
        # reverse_proxy localhost:8080

        # Or serve a PHP site through php-fpm:
        # php_fastcgi localhost:9000
}

# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile

Also, I am not sure how the container ended up with the name caddy-caddy-1.

Here’s the directory structure I’m using:

ruby@madcatter:~$ pwd
/home/ruby
ruby@madcatter:~$ ls -R caddy/
caddy/:
compose.yaml  conf  site

caddy/conf:
Caddyfile

caddy/site:
index.html

Does the docker image use preset paths somehow?