Solve infinite 301 loop (then 502) with Ghost and Caddy

1. My Caddy version (caddy version):

Docker image caddy/caddy:alpine (2.0.0-beta.17)

2. How I run Caddy:

With Docker Compose.

a. System environment:

Ubuntu 18.04
Docker 19.03.8
docker-compose version 1.25.4

b. Command:

None, the Docker image as is.

c. Service/unit/compose file:

.env file:

GHOST=itsallsotireso.me
ISSO=marginalia.itsallsotireso.me
SHAARLI=normco.re
DEFAULT_NETWORK=firefly

Caddy docker-compose.yml:

version: "3.7"

services:
  caddy:
    container_name: caddy
    image: caddy/caddy:alpine
    restart: always
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./config:/config/caddy
      - ./data:/data/caddy
    ports:
      - "80:80"
      - "443:443"
    environment:
      - GHOST
      - ISSO
      - SHAARLI

networks:
  default:
    external:
      name: $DEFAULT_NETWORK

Ghost docker-compose.yml:

version: "3.7"

services:
  ghost:
    container_name: ghost
    image: ghost:alpine
    restart: always
    volumes:
      - ./content:/var/lib/ghost/content
    environment:
      url: https://itsallsotireso.me

networks:
  default:
    external:
      name: firefly

d. My complete Caddyfile or JSON config:

{
        http_port 80
        https_port 443
        experimental_http3
        debug
}

{$GHOST} {
    reverse_proxy http://ghost:2368
}

{$ISSO} {
    reverse_proxy http://isso:8080
}

{$SHAARLI} {
    reverse_proxy http://shaarli:80
}

3. The problem I’m having:

First of all, I am very new to Caddy (this is why my Caddyfile is very minimalist for now). I am still in a learning phase, and the v2 documentation is a bit tedious for me (I learnt Traefik v2 from scratch faster, imho) but I am really excited by all the Caddy features.

I used to run a Ghost blog in Docker alongside a couple of accessory services with nginx and acmesh (for the certificates), then with Traefik.

While randomly browsing though r/selfhosted at Reddit, I came across a few posts of Matt Holt defending Caddy v2 and he was very passionate and pretty convincing. Convincing enough to make me want to experiment Caddy on my VPS. In the near future, I would like to publish a few static files. I cannot do that with Traefik and I do not want to stack tons of services if one like Caddy can do everything I need.

Sorry for the off topic. Now back to my actual issue. It is pretty simple: when I access my Ghost url in a brower, it gets into an infinite loop of 301 redirects and it ends with a too many redirects error.

Which is weird to me, because this is the first time I encounter this issue, I have always managed to run Ghost either with nginx or Traefik with minimal config.

Thank you in advance for your kind help!

4. Error messages and/or full log output:

I have dozen of logs like that in a very short span:

caddy | 2020/03/15 16:07:08.919 DEBUG http.handlers.reverse_proxy upstream roundtrip {"request": {"method": "GET", "uri": "/", "proto": "HTTP/2.0", "remote_addr": "172.19.0.1:50194", "host": "itsallsotireso.me", "headers": {"Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "X-Forwarded-For": ["172.19.0.1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"], "Sec-Fetch-User": ["?1"], "Accept-Language": ["fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,pt;q=0.6,es;q=0.5,pt-PT;q=0.4,it;q=0.3"], "Cache-Control": ["max-age=0"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"], "Sec-Fetch-Dest": ["document"]}, "tls": {"resumed": false, "version": 772, "ciphersuite": 4865, "proto": "h2", "proto_mutual": true, "server_name": "itsallsotireso.me"}}, "headers": {"Cache-Control": ["public, max-age=31536000"], "Location": ["https://itsallsotireso.me/"], "Vary": ["Accept, Accept-Encoding"], "Content-Type": ["text/html; charset=utf-8"], "Content-Length": ["108"], "Date": ["Sun, 15 Mar 2020 16:07:08 GMT"], "Connection": ["keep-alive"], "X-Powered-By": ["Express"]}, "duration": 0.001513961, "status": 301}

5. What I already tried:

First thing I did was to remove the environment variable url: https://itsallsotireso.me from my Ghost docker-compose.yml. This is not a valid solution because then most of my internal links in Ghost became localhost:2368 instead of the actual URL. You can imagine this is not a very practical experience for visitors haha.

Then I googled, and I found out an old post (link below) about a similar issue.

After I had finished the Ghost Blog (https://ghost.org/) HTTPS setup on my server, an infinite redirection loop which prevented the blog from being displayed.

So I tried to transpose his solution into my Caddy context and I changed the initial Caddyfile to:

{$GHOST} {
    reverse_proxy ghost:2368
    header {
        Strict-Transport-Security max-age=31536000; includeSubDomains; preload
        Host {http.request.host}
        X-Forwarded-Proto {http.request.scheme}
        X-Forwarded-For {http.request.remote}
        X-Real-IP {http.request.remote}
    }

Now I do not have the infinite 301 loop anymore but I get a 502 error:

caddy | 2020/03/15 17:38:13.400 ERROR http.log.error dial tcp: lookup ghost on 127.0.0.11:53: no such host {"request": {"method": "GET", "uri": "/", "proto": "HTTP/2.0", "remote_addr": "172.19.0.1:50256", "host": "itsallsotireso.me", "headers": {"Cache-Control": ["max-age=0"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-User": ["?1"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,pt;q=0.6,es;q=0.5,pt-PT;q=0.4,it;q=0.3"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"], "Sec-Fetch-Dest": ["document"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"], "Sec-Fetch-Mode": ["navigate"]}, "tls": {"resumed": false, "version": 772, "ciphersuite": 4865, "proto": "h2", "proto_mutual": true, "server_name": "itsallsotireso.me"}}, "status": 502, "err_id": "5ccqgm5m5", "err_trace": "reverseproxy.(*Handler).ServeHTTP (reverseproxy.go:363)"}

Additional remarks:

Regarding the other services, Isso is OK:

caddy    | 2020/03/15 17:48:51.802      DEBUG   http.handlers.reverse_proxy     upstream roundtrip      {"request": {"method": "GET", "uri": "/admin", "proto": "HTTP/2.0", "remote_addr": "172.19.0.1:50282", "host": "marginalia.itsallsotireso.me", "headers": {"Sec-Fetch-Dest": ["document"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"], "Sec-Fetch-User": ["?1"], "Accept-Language": ["fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,pt;q=0.6,es;q=0.5,pt-PT;q=0.4,it;q=0.3"], "X-Forwarded-For": ["172.19.0.1"], "Upgrade-Insecure-Requests": ["1"]}, "tls": {"resumed": false, "version": 772, "ciphersuite": 4865, "proto": "h2", "proto_mutual": true, "server_name": "marginalia.itsallsotireso.me"}}, "headers": {"Access-Control-Allow-Credentials": ["true"], "Access-Control-Allow-Methods": ["HEAD, GET, POST, PUT, DELETE"], "Access-Control-Expose-Headers": ["X-Set-Cookie, Date"], "Server": ["gunicorn/20.0.4"], "Content-Type": ["text/html; charset=utf-8"], "Access-Control-Allow-Origin": ["https://itsallsotireso.me"], "Access-Control-Allow-Headers": ["Origin, Referer, Content-Type"], "Date": ["Sun, 15 Mar 2020 17:48:51 GMT"], "Connection": ["keep-alive"], "Content-Length": ["881"]}, "duration": 0.023124365, "status": 200}
caddy    | 2020/03/15 17:48:51.947      DEBUG   http.handlers.reverse_proxy     upstream roundtrip      {"request": {"method": "GET", "uri": "/img/isso.svg", "proto": "HTTP/2.0", "remote_addr": "172.19.0.1:50282", "host": "marginalia.itsallsotireso.me", "headers": {"If-Modified-Since": ["Thu, 04 Oct 2018 04:30:14 GMT"], "Accept": ["image/webp,image/apng,image/*,*/*;q=0.8"], "Accept-Encoding": ["gzip, deflate, br"], "If-None-Match": ["\"wzsdm-1538627414-11600-246809405\""], "Accept-Language": ["fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,pt;q=0.6,es;q=0.5,pt-PT;q=0.4,it;q=0.3"], "X-Forwarded-For": ["172.19.0.1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["no-cors"]}, "tls": {"resumed": false, "version": 772, "ciphersuite": 4865, "proto": "h2", "proto_mutual": true, "server_name": "marginalia.itsallsotireso.me"}}, "headers": {"Access-Control-Allow-Origin": ["https://itsallsotireso.me"], "Access-Control-Allow-Credentials": ["true"], "Access-Control-Allow-Methods": ["HEAD, GET, POST, PUT, DELETE"], "Access-Control-Allow-Headers": ["Origin, Referer, Content-Type"], "Access-Control-Expose-Headers": ["X-Set-Cookie, Date"], "Server": ["gunicorn/20.0.4"], "Cache-Control": ["max-age=43200, public"], "Etag": ["\"wzsdm-1538627414-11600-246809405\""], "Date": ["Sun, 15 Mar 2020 17:48:51 GMT"], "Connection": ["keep-alive"]}, "duration": 0.00232259, "status": 304}

But Shaarli gets the same 502 issue as Ghost:

caddy | 2020/03/15 17:39:26.470 ERROR http.log.error dial tcp: lookup shaarli on 127.0.0.11:53: no such host {"request": {"method": "GET", "uri": "/", "proto": "HTTP/2.0", "remote_addr": "86.195.85.85:60073", "host": "normco.re", "headers": {"Upgrade-Insecure-Requests": ["1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"], "Accept-Encoding": ["gzip, deflate, br"], "Cookie": ["shaarli_staySignedIn=7ccd68afd09563710f6691fb1c0d6b6776e86d6c; shaarli=sgi7bt66r7n26l94e840pm1mj0"], "Sec-Fetch-User": ["?1"], "Accept-Language": ["fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,pt;q=0.6,es;q=0.5,pt-PT;q=0.4,it;q=0.3"], "Cache-Control": ["max-age=0"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"], "Sec-Fetch-Dest": ["document"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"]}, "tls": {"resumed": false, "version": 772, "ciphersuite": 4865, "proto": "h2", "proto_mutual": true, "server_name": "normco.re"}}, "status": 502, "err_id": "94drhacpw", "err_trace": "reverseproxy.(*Handler).ServeHTTP (reverseproxy.go:363)"}

6. Links to relevant resources:

FYI the volume paths just changed as of this morning, so you should probably update those: Fix VOLUME instructions to point to right locations by hairyhenderson · Pull Request #48 · caddyserver/caddy-docker · GitHub

I think what you’re actually looking for is the header_up and header_down options for the reverse_proxy directive. See reverse_proxy (Caddyfile directive) — Caddy Documentation

Example:

    reverse_proxy ghost:2368 {
        header_up Strict-Transport-Security max-age=31536000; includeSubDomains; preload

        # this one is done automatically for you by Caddy
        # header_up Host {host}

        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-For {remote}
        header_up X-Real-IP {remote}
    }
1 Like

Thank you @francislavoie :ok_hand: :v: :+1: :muscle: Everything is properly working now!

I guess I should have read more carefully the doc and not follow blindly outdated posts!

I have other minor issues and a few more questions about Caddy, but I will post in due time.

On a side note:

    header_up Strict-Transport-Security max-age=31536000; includeSubDomains; preload

is crashing my Caddyfile:

caddy | 2020/03/15 19:17:44.030 INFO using provided configuration {“config_file”: “/etc/caddy/Caddyfile”, “config_adapter”: “caddyfile”}
caddy | run: adapting config using caddyfile: parsing caddyfile tokens for ‘reverse_proxy’: /etc/caddy/Caddyfile:10 - Error during parsing: Wrong argument count or unexpected line ending after ‘preload;’

I do not see what I am missing (same error with or without the semicolon after preload)

1 Like

Ah right, I think you need to quote wrap the value. The error message is decently clear, it’s reading each segment of the value as a separate argument which doesn’t match the amount of arguments it expects.

header_up Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

1 Like

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