Caddy returns nothing except 200 in Docker environment

1. Caddy version (caddy version):

Latest (gets downloaded on Docker build)

2. How I run Caddy:

caddy run --config /app/Caddyfile

a. System environment:

Laravel environment I’m trying to run inside Docker.

b. Command:

paste command here

c. Service/unit/compose file:

Relevant Dockerfile part:

FROM php:8.0.3


# Caddy server
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | apt-key add -
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee -a /etc/apt/sources.list.d/caddy-stable.list
RUN apt-get update -y && apt-get install caddy
...

CMD caddy run --config /app/Caddyfile

Dockerfile:

version: '3.5'

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    volumes:
      - ./api:/app/
    depends_on:
      - db

d. My complete Caddyfile or JSON config:

http://0.0.0.0:8080 {
        root * /app/public

        log
        encode gzip

        file_server
}

3. The problem I’m having:

I get absolutely no error (please check next part), only an empty content. I was expecting at least a 500 error from Laravel.

I don’t use FastCGI nor FPM because this project is a small blog API, not aiming to reach 10k visitors a day, so I didn’t feel the need to use any of those. Please tell me if I’m wrong / stupid.

I also don’t want HTTPS since HTTPS will be available from a Traefik reverse proxy when my website will go to production.

4. Error messages and/or full log output:

docker-compose logs api         
Attaching to siteweb_api_1
api_1      | {"level":"info","ts":1615755327.2930388,"msg":"using provided configuration","config_file":"/app/Caddyfile","config_adapter":""}
api_1      | {"level":"info","ts":1615755327.298195,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
api_1      | {"level":"info","ts":1615755327.2983677,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0003dd490"}
api_1      | {"level":"info","ts":1615755327.299239,"logger":"tls","msg":"cleaned up storage units"}
api_1      | {"level":"info","ts":1615755327.299237,"msg":"autosaved config","file":"/root/.config/caddy/autosave.json"}
api_1      | {"level":"info","ts":1615755327.2992644,"msg":"serving initial configuration"}
api_1      | {"level":"info","ts":1615755336.061243,"logger":"http.log.access","msg":"handled request","request":{"remote_addr":"172.23.0.1:46622","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"Connection":["keep-alive"],"Cookie":["__stripe_mid=09a24952-fb81-4bbb-a250-e2e496522237; Phpstorm-765b08c8=6b43f34d-7868-46b8-85bb-c8b3d4c6264f; i18n_redirected=fr"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],"Accept-Language":["fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"],"Accept-Encoding":["gzip, deflate"]}},"common_log":"172.23.0.1 - - [14/Mar/2021:20:55:36 +0000] \"GET / HTTP/1.1\" 0 0","duration":0.000003241,"size":0,"status":0,"resp_headers":{"Server":["Caddy"]}}
api_1      | {"level":"info","ts":1615755337.5498781,"logger":"http.log.access","msg":"handled request","request":{"remote_addr":"172.23.0.1:46622","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Cookie":["__stripe_mid=09a24952-fb81-4bbb-a250-e2e496522237; Phpstorm-765b08c8=6b43f34d-7868-46b8-85bb-c8b3d4c6264f; i18n_redirected=fr"],"Cache-Control":["max-age=0"],"Accept-Language":["fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"],"User-Agent":["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],"Upgrade-Insecure-Requests":["1"]}},"common_log":"172.23.0.1 - - [14/Mar/2021:20:55:37 +0000] \"GET / HTTP/1.1\" 0 0","duration":0.000003757,"size":0,"status":0,"resp_headers":{"Server":["Caddy"]}}
api_1      | {"level":"info","ts":1615755581.5253675,"logger":"http.log.access","msg":"handled request","request":{"remote_addr":"172.23.0.1:47648","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"Connection":["keep-alive"],"Cookie":["__stripe_mid=09a24952-fb81-4bbb-a250-e2e496522237; Phpstorm-765b08c8=6b43f34d-7868-46b8-85bb-c8b3d4c6264f; i18n_redirected=fr"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],"Accept-Language":["fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"],"Accept-Encoding":["gzip, deflate"]}},"common_log":"172.23.0.1 - - [14/Mar/2021:20:59:41 +0000] \"GET / HTTP/1.1\" 0 0","duration":0.000003752,"size":0,"status":0,"resp_headers":{"Server":["Caddy"]}}

5. What I already tried:

I’ve seen quite a lot of subjects having this kind of problem, but their answers didn’t sove mine.

I tried to copy some other people’s configuration, but no success so far

6. Links to relevant resources:

Thank you in advance

Ah, this is your problem. Technically 0.0.0.0 as a host matcher will only match a request with the hostname 0.0.0.0, which would only be valid from localhost. Just remove that from your host matcher and it should work fine.

:8080 {
	root * /app/public

	log
	encode gzip

	file_server
}

Well, Caddy can’t run PHP code itself, so if you’re serving a Laravel app, you’ll need php-fpm. If it’s actually a static site with just HTML files, then you won’t need it, but that doesn’t sound like it’s the case.

Why not just use Caddy as your proxy? What value is Traefik adding?

If you’re using Traefik for configuration via docker labels, Caddy can do that too with this plugin:

Caddy has an official docker image, I recommend you use that instead rather than installing it from the apt repo: Docker

Thank you for your quick reply, I edited my Dockerfile like so:

FROM php:8.0.3-fpm

...

EXPOSE 8080 9000

And my docker-compose file like so:

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
      - "9000:9000"
    volumes:
      - ./api:/app/
    depends_on:
      - db

(I indeed won’t serve static files.)

However, I struggle how I can use Caddy 2’s official image instead of installing it while keeping PHP, should I COPY it?

Anyway, I also edited my Caddyfile like so:

:8080 {
    root * /app/public

    php_fastcgi api:9000

    log
    encode gzip

    file_server
}

However, I’m now getting a 502 error when trying to reach for localhost:8080. I also tried to change to 127.0.0.1 or localhost, but there was no difference.

My logs have also changed:

docker-compose logs api         
Attaching to siteweb_api_1
api_1      | {"level":"info","ts":1615759242.8040078,"msg":"using provided configuration","config_file":"/app/Caddyfile","config_adapter":""}
api_1      | {"level":"info","ts":1615759242.80688,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
api_1      | {"level":"info","ts":1615759242.8079212,"msg":"autosaved config","file":"/root/.config/caddy/autosave.json"}
api_1      | {"level":"info","ts":1615759242.8079374,"msg":"serving initial configuration"}
api_1      | {"level":"info","ts":1615759242.808065,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0003d2f50"}
api_1      | {"level":"info","ts":1615759242.8081481,"logger":"tls","msg":"cleaned up storage units"}
api_1      | {"level":"error","ts":1615759253.6325016,"logger":"http.log.error","msg":"dialing backend: dial tcp 172.23.0.3:9000: connect: connection refused","request":{"remote_addr":"172.23.0.1:35060","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"Accept-Language":["fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"],"User-Agent":["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Cookie":["__stripe_mid=09a24952-fb81-4bbb-a250-e2e496522237; Phpstorm-765b08c8=6b43f34d-7868-46b8-85bb-c8b3d4c6264f; i18n_redirected=fr"],"Upgrade-Insecure-Requests":["1"],"Cache-Control":["max-age=0"]}},"duration":0.000685128,"status":502,"err_id":"8wt0kc7de","err_trace":"reverseproxy.statusError (reverseproxy.go:783)"}
api_1      | {"level":"error","ts":1615759253.6326258,"logger":"http.log.access","msg":"handled request","request":{"remote_addr":"172.23.0.1:35060","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"User-Agent":["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"],"Accept-Language":["fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"],"Cache-Control":["max-age=0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Cookie":["__stripe_mid=09a24952-fb81-4bbb-a250-e2e496522237; Phpstorm-765b08c8=6b43f34d-7868-46b8-85bb-c8b3d4c6264f; i18n_redirected=fr"],"Upgrade-Insecure-Requests":["1"]}},"common_log":"172.23.0.1 - - [14/Mar/2021:22:00:53 +0000] \"GET / HTTP/1.1\" 502 0","duration":0.000685128,"size":0,"status":502,"resp_headers":{"Server":["Caddy"]}}

So now I’m a bit confused about what I should do next. With my Dockerfile / docker-compose file, Caddy runs from inside my ‘api’ container. However, when trying to reach either localhost:9000, 127.0.0.1:9000 or api:9000 (the container’s name), I still get errors showing that Caddy can’t connect to the FPM. Is there something I’m missing?

P.S. : I’m not a huge fan of moving from Traefik to Caddy Reverse Proxy because I have quite a lot of websites running with this configuration, everything works, I’d like to keep it that way :sweat_smile:

Thanks in advance.

Typically when you run Caddy in Docker to serve PHP apps, you have the caddy container separate from your php-fpm container, then you mount the site code in both containers as a volume (both containers need to read the files; Caddy needs it to make routing decisions based on the files that exist, and to serve static files like JS and CSS, etc), then finally in the Caddyfile, use php_fastcgi php-fpm:9000 to proxy to the php-fpm when PHP needs to be run.

The reason it’s split into two containers is because of separation of concerns – it’s a core ideal in containerization; and also because both php-fpm and Caddy are long-running processes that should be PID 1 in their respective containers.

I can almost certainly guarantee that your config will be simpler with Caddy than with Traefik. Cutting out an entire piece of technology from your stack will only make things easier in the long run.

Thank you for your advices, but I need to bother you again,

I created a new container dedicated to Caddy where, as you suggested, I set up a volume that shared the code to the /data directory and used my custom configuration. Here it is:

  caddy:
    image: caddy:2.3.0-alpine
    ports:
      - "8080:8080"
    volumes:
      - ./api/Caddyfile:/etc/caddy/Caddyfile
      - ./api:/data

good news: No more 502 error
bad news: I’m getting a File not found error. At least it’s not an empty, dark page anymore.

I’ve dug around and it looks like it’s either because it can’t find my index.php or a permission problem. I have edited my Caddyfile so it says that the root is now /data/public to match how I set my files in the Caddy container, because the Caddy Dockerhub’s doc was showing exemples using /data as a destination for data volumes. Is it requiring a specific chmod, or my files to be somewhere else?

Thank you again in advance

P.S. : I found the documentation for the reverse proxy… I now cannot NOT give it a try.

/data is the storage location for Caddy, for various things like certificates and such. That storage should be persisted, otherwise you risk hitting rate limits from Let’s Encrypt. You shouldn’t be putting your site files there. I recommend using /srv for mounting your site.

Yep. It’s as simple as this:

app1.example.com {
	reverse_proxy app1:8080
}

app2.example.com {
	reverse_proxy app2:8080
}

That gets you managed certificates from Let’s Encrypt for those domains, and a proxy with good defaults for each.

And for your PHP site:

app3.example.com {
	root * /srv/public
	log
	encode gzip
	php_fastcgi php-fpm:9000
	file_server
}

Hello, sorry for the slow answer! I managed to make Caddy work, and I honestly don’t know how.

I was typing “the only differences between my configuration before and now is that…” but I started to “roll back” my configurations as it was at your first reply, and it keeps working. I have no clue how and why. I guess I’m gonna stop wondering and just enjoy the fact that it works.

Thanks a lot for your answers, your heads up and your patience!

Well don’t leave us hanging, what did you land on that works? :sweat_smile:

It’s better if you describe what you have, in case someone else finds this thread in the future.

Rebooting the whole system and check again to be safe.
Also double check docker container is using the config you roll back.

Sorry for the late reply, I didn’t see I got answers here!

@francislavoie Well the ONLY difference I’ve manage to see between my configuration and the working one was that there was a problem with the volumes: it didn’t seem to work as long as I was sharing my local code with /serv inside the Caddy container but I ALSO needed to share it with /serv inside my PHP container. I will run some tests tomorrow to be sure but I found out that using /app in PHP and /serv in Caddy returned me a “404 not found” systematically.

@John_Siu I did that, and it still works! Even better, it works on my other computer (the Ubuntu one) on which I started having the issue at first. I have permission issues though, but I don’t think it will be that hard to handle. (I hope I didn’t jinx myself)

That’s correct, their paths need to match. This is because Caddy sends the absolute path of the files in the fastcgi message to php-fpm, so the paths need to match on both sides for it to work.

1 Like

… I’m actually so glad I found this out by myself!

1 Like

Learning :smiley:

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