Empty reply from server with Docker with Caddyfile

1. Caddy version (caddy version):

Latest (pull by docker)

2. How I run Caddy:

I run caddy via docker. When I do so with the setup to serve a single file all is good. When I use a Caddy file the results are empty"

A. This way works fine

echo "Hello world" > index.html
docker run -p 80:80 -v $PWD/index.html:/usr/share/caddy/index.html caddy

In another terminal

curl -v http://localhost/

B. This second way with a Caddyfile returns curl: (52) Empty reply from server

docker run -p 80:80 -v $PWD/Caddyfile:/etc/caddy/Caddyfile caddy

Caddyfile

{
    auto_https off
}

localhost

respond "Hello, world!"

a. System environment:

Docker version : Docker version 19.03.12, build 48a66213fe
Mac 10.13.6

Docker application is configured to access the local working directory. Docker … Preferences … Resources … File Sharing … add local directory and restart.

b. Command:

See #2 above

c. Service/unit/compose file:

NA

d. My complete Caddyfile or JSON config:

See above

3. The problem I’m having:

When docker runs caddy with a simple file it works. When I switch to using a simple docker file it does not.

4. Error messages and/or full log output:

Working version:

/r/dev/npuser/temp$ docker run -p 80:80 -v $PWD/index.html:/usr/share/caddy/index.html caddy
{"level":"info","ts":1601583133.332981,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
{"level":"info","ts":1601583133.3369324,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
{"level":"info","ts":1601583133.338265,"logger":"http","msg":"server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server","server_name":"srv0","http_port":80}
{"level":"info","ts":1601583133.3388834,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc000308930"}
{"level":"info","ts":1601583133.3407876,"logger":"tls","msg":"cleaned up storage units"}
{"level":"info","ts":1601583133.3423831,"msg":"autosaved config","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1601583133.3430257,"msg":"serving initial configuration"}
/r/dev/npuser/temp$ curl -v http://localhost/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Content-Length: 12
< Content-Type: text/html; charset=utf-8
< Etag: "qhjfccc"
< Last-Modified: Thu, 01 Oct 2020 19:26:36 GMT
< Server: Caddy
< Date: Thu, 01 Oct 2020 20:12:20 GMT
< 
hello world
* Connection #0 to host localhost left intact

Non working version

/r/dev/npuser/temp$ docker run -p 80:80 -v $PWD/Caddyfile:/etc/caddy/Caddyfile caddy
{"level":"info","ts":1601583225.4984696,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
{"level":"info","ts":1601583225.5005987,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
{"level":"info","ts":1601583225.5013092,"msg":"autosaved config","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1601583225.501362,"msg":"serving initial configuration"}
{"level":"info","ts":1601583225.5018826,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc000226ee0"}
{"level":"info","ts":1601583225.5020576,"logger":"tls","msg":"cleaned up storage units"}
/r/dev/npuser/temp$ curl -v http://localhost/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
> 
* Empty reply from server
* Connection #0 to host localhost left intact
curl: (52) Empty reply from server

5. What I already tried:

The above is a major reduction from my main application configuration. I’m will be using docker compose with reverse proxy and static Vue web site.

I also tried without the global disable TLS but that just makes local requests redirect to https which fails because there are no certs for localhost. This caddy file

localhost

respond "Hello, world!"

Gets the following logs from using the following curl commands

/r/dev/npuser/temp$ docker run -p 80:80 -v $PWD/Caddyfile:/etc/caddy/Caddyfile caddy
{"level":"info","ts":1601583510.320734,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
{"level":"info","ts":1601583510.3239393,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
{"level":"info","ts":1601583510.3247647,"logger":"http","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
{"level":"info","ts":1601583510.3256257,"logger":"http","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
{"level":"info","ts":1601583510.3291945,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0002da850"}
{"level":"info","ts":1601583510.3399036,"logger":"tls","msg":"setting internal issuer for automation policy that has only internal subjects but no issuer configured","subjects":["localhost"]}
{"level":"info","ts":1601583510.3410902,"logger":"tls","msg":"cleaned up storage units"}
{"level":"warn","ts":1601583510.400767,"logger":"pki.ca.local","msg":"installing root certificate (you might be prompted for password)","path":"storage:pki/authorities/local/root.crt"}
2020/10/01 20:18:30 Warning: "certutil" is not available, install "certutil" with "apt install libnss3-tools" or "yum install nss-tools" and try again
2020/10/01 20:18:30 define JAVA_HOME environment variable to use the Java trust
2020/10/01 20:18:30 certificate installed properly in linux trusts
{"level":"info","ts":1601583510.439247,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["localhost"]}
{"level":"info","ts":1601583510.4404404,"msg":"autosaved config","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1601583510.4420805,"msg":"serving initial configuration"}
{"level":"info","ts":1601583510.4417844,"logger":"tls.obtain","msg":"acquiring lock","identifier":"localhost"}
{"level":"info","ts":1601583510.4436636,"logger":"tls.obtain","msg":"lock acquired","identifier":"localhost"}
{"level":"info","ts":1601583510.4489083,"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"localhost"}
{"level":"info","ts":1601583510.4493704,"logger":"tls.obtain","msg":"releasing lock","identifier":"localhost"}
{"level":"warn","ts":1601583510.4509451,"logger":"tls","msg":"stapling OCSP","error":"no OCSP stapling for [localhost]: no OCSP server specified in certificate"}

With:

/r/dev/npuser/temp$ curl -v http://localhost/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://localhost/
< Server: Caddy
< Date: Thu, 01 Oct 2020 20:18:54 GMT
< Content-Length: 0
< 
* Closing connection 0
/r/dev/npuser/temp$ curl -v https://localhost/
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 443 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connection failed
* connect to 127.0.0.1 port 443 failed: Connection refused
* Failed to connect to localhost port 443: Connection refused
* Closing connection 0
curl: (7) Failed to connect to localhost port 443: Connection refused

6. Links to relevant resources:

https://hub.docker.com/_/caddy?tab=description

To help I got inside the docker container and captured the caddy config
Once again: Caddyfile

{
    auto_https off
}

localhost

respond "Hello, world!"

Run command:

docker run -p 80:80 -v $PWD/Caddyfile:/etc/caddy/Caddyfile caddy

Logs

{"level":"info","ts":1601586419.2108712,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
{"level":"info","ts":1601586419.21418,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
{"level":"info","ts":1601586419.215525,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0002d87e0"}
{"level":"info","ts":1601586419.2161489,"msg":"autosaved config","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1601586419.216588,"msg":"serving initial configuration"}
{"level":"info","ts":1601586419.2174118,"logger":"tls","msg":"cleaned up storage units"}
{"level":"info","ts":1601586489.4902449,"logger":"admin.api","msg":"received request","method":"GET","host":"localhost:2019","uri":"/config/","remote_addr":"127.0.0.1:35590","headers":{"Accept":["*/*"],"User-Agent":["curl/7.69.1"]}}
{"level":"info","ts":1601586538.9471707,"logger":"admin.api","msg":"received request","method":"GET","host":"localhost:2019","uri":"/config/","remote_addr":"127.0.0.1:35592","headers":{"Accept":["*/*"],"User-Agent":["curl/7.69.1"]}}

Results

/r/dev/npuser/temp$ curl http://localhost/
curl: (52) Empty reply from server
/r/dev/npuser/temp$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                   NAMES
9b662af1048d        caddy               "caddy run --config …"   29 seconds ago      Up 27 seconds       443/tcp, 0.0.0.0:80->80/tcp, 2019/tcp   blissful_boyd
/r/dev/npuser/temp$ docker exec -it 9b662af1048d /bin/sh
/srv # apk add curl jq
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
(1/5) Installing nghttp2-libs (1.41.0-r0)
(2/5) Installing libcurl (7.69.1-r1)
(3/5) Installing curl (7.69.1-r1)
(4/5) Installing oniguruma (6.9.5-r1)
(5/5) Installing jq (1.6-r1)
Executing busybox-1.31.1-r16.trigger
OK: 8 MiB in 21 packages
/srv # curl http://localhost:2019/config/
{"apps":{"http":{"servers":{"srv0":{"automatic_https":{"disable":true},"listen":[":443"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"body":"Hello, world!","handler":"static_response"}]}]}],"match":[{"host":["localhost"]}],"terminal":true}],"tls_connection_policies":[{}]}}}}}

Caddy Config

/srv # curl http://localhost:2019/config/ | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   298  100   298    0     0  29800      0 --:--:-- --:--:-- --:--:-- 33111
{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "automatic_https": {
            "disable": true
          },
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "body": "Hello, world!",
                          "handler": "static_response"
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "localhost"
                  ]
                }
              ],
              "terminal": true
            }
          ],
          "tls_connection_policies": [
            {}
          ]
        }
      }
    }
  }
}

I found the solution. Caddyfile:

{
    auto_https off
}

localhost:80

respond "Hello, world! Caddy here with caddy file"

Note the port number. We need to specify the port caddy listens to. Maybe a good idea for the docs?

Caddy defaults to port 443/HTTPS unless specified. You didn’t specify either http:// nor :80, so it did as such.

It’s documented right here:

Caddy serves all sites over HTTPS by default.

I typically recommend people just use :80 when testing locally, because that will allow connections from any hostname, i.e. connections from other devices in your LAN if they specify the IP of your machine. If you use localhost:80 then Caddy will not accept requests with 192.168.1.50 or whatever, as the hostname.

You can read more about the site addresses Caddy supports here:

1 Like

Thank you. I’m following the caddy tutorials here

and they don’t show to use ‘localhost:80’.
I agree that using port 80 for dev makes sense. I found that I needed the global ‘auto_https off’ setting to prevent automatic redirects. But, more immediately …

The above working solution uses a simple respond directive. When I switch to file_server the responses are now really empty. Like nothing is returned. Just the 200 status.

{
    auto_https off
}

localhost:80

# respond "Hello, world! Caddy here with caddy file."
file_server
docker run -p 80:80 -v $PWD/index.html:/usr/share/caddy/index.html -v $PWD/Caddyfile:/etc/caddy/Caddyfile caddy
$ curl -v http://localhost:80
* Rebuilt URL to: http://localhost:80/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: Caddy
< Date: Thu, 01 Oct 2020 23:31:40 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

The above should have returned the contents of the index.html file created in the local launch directory and mounted into the docker container.

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "automatic_https": {
            "disable": true
          },
          "listen": [
            ":80"
          ],
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "file_server",
                          "hide": [
                            "/etc/caddy/Caddyfile"
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "localhost"
                  ]
                }
              ],
              "terminal": true
            }
          ]
        }
      }
    }
  }
}

Those tutorials are written from the perspective of running Caddy on your host machine, where Caddy is able to install its CA certificate to your system’s trust store.

When running in Docker, it can’t install the cert because it doesn’t have access to the appropriate paths on your system, nor does it have permissions to.

For file_server to work, you need to tell Caddy where to look for files. You use the root directive for that. Again, the tutorial is assuming you’re running on the host machine, and Caddy uses the PWD as the root. When in Docker, PWD is /srv so that’s where it’s looking for files.

The default Caddyfile inside of the container specified root * /usr/share/caddy. You can see the full Caddyfile here:

You should read the instructions on the Docker Hub page, it explains how to use Caddy running in Docker: Docker Hub

1 Like

I started with a much larger docker file that did declare the root. It did not work. I’m very grateful for your help providing one key piece of information that I’ve not seen in documentation.

The default Caddyfile inside of the container specified root * /usr/share/caddy. You can see the full Caddyfile here:…

So, in a container Caddy looks to /usr/share/caddy for the site root. When I returned to my original larger project and replaced root * /usr/var/www with root * /usr/share/caddy everything worked fine.

Much gratitude to the whole Caddy team. It’s wonderful.

1 Like

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