Traefik redirects https to http and trailing slash issue [traefik, nginx, gatsby]

Hey,

I didn't find a solution for my problem yet.

I use Traefik as a reverse proxy to route to multiple sites. One is an nginx server hosting a static Gatsby site. And I don't understand how to set it up properly. I actually don't care whether there is a trailing slash or not, I just want it to run without issues :wink: So assuming all pages need a trailing slash, I want the redirects to look as follows:

http://example.com/page -> http://example.com/page/
https://example.com/page -> https://example.com/page/
...

Here is my current setting:

Traefik: docker-compose.yml

version: '3.7'

services:
  traefik:
    image: traefik:v2.2.1
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    ports:
      - 80:80
      - 443:443
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik.yml:/traefik.yml:ro
      - ./data/dynamic_conf.yml:/dynamic_conf.yml
      - ./data/acme.json:/acme.json
    environment:
      # Here are my API_KEY and API_SECRET
  labels:
      - "traefik.enable=true"

      # add trailing slash
      - "traefik.http.middlewares.add-trailing-slash.chain.middlewares=strip-prefix-1,strip-prefix-2"
      - "traefik.http.middlewares.strip-prefix-1.redirectregex.regex=^(https?://[^/]+/[a-z0-9_]+)$$"
      - "traefik.http.middlewares.strip-prefix-1.redirectregex.replacement=$${1}/"
      - "traefik.http.middlewares.strip-prefix-1.redirectregex.permanent=true"
      - "traefik.http.middlewares.strip-prefix-2.stripprefixregex.regex=/[a-z0-9_]+"

      # remove trailing slash
      - "traefik.http.middlewares.remove-trailing-slash.chain.middlewares=strip-prefix-3,strip-prefix-4"
      - "traefik.http.middlewares.strip-prefix-3.redirectregex.regex=^(https?://[^/]+/[a-z0-9_]+)/$$"
      - "traefik.http.middlewares.strip-prefix-3.redirectregex.replacement=$${1}"
      - "traefik.http.middlewares.strip-prefix-3.redirectregex.permanent=true"
      - "traefik.http.middlewares.strip-prefix-4.stripprefixregex.regex=/[a-z0-9_]+"

      # global wildcard certificates for our godaddy domains
      - "traefik.http.routers.wildcard-certs.tls.certresolver=godaddy"
      - "traefik.http.routers.wildcard-certs.tls.domains[0].main=example.com"
      - "traefik.http.routers.wildcard-certs.tls.domains[0].sans=*.example.com"

networks:
  proxy:
    external: true

traefik.yml

api:
  dashboard: true
entryPoints:
  http:
    address: ":80"
    #compress: false
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https
          permanent: true
  https:
    address: ":443"
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: "/dynamic_conf.yml"
certificatesResolvers:
  ...

nginx: docker-compose.yml

version: '3.7'

services:
  nginx:
    image: nginx:latest
    restart: always
    volumes:
      - ./web-data/public:/usr/share/nginx/html/
      - ./nginx/gatsby-nginx.conf:/etc/nginx/conf.d/default.conf

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nginx-secure.entrypoints=https"
      - "traefik.http.routers.nginx-secure.rule=Host(`example.com`, `www.example.com`)"
      - "traefik.http.routers.nginx-secure.tls=true"
      - "traefik.docker.network=proxy"
      #- "traefik.http.routers.nginx-secure.middlewares=add-trailing-slash"

    networks:
      - proxy
      - default
   
networks:
  proxy:
    external: true

And finally my nginx default.conf looks as follows:

server {
    listen 80; #443 ssl http2;

    server_name example.com;

    root /usr/share/nginx/html;
    index index.html;

    autoindex off;
    charset utf-8;

    error_page 404 /404.html;

    location ~* \.(?:html)$ {
        add_header Cache-Control "no-store";
        expires    off;
    }

    location /page-data {
        add_header Cache-Control "public, max-age=0, must-revalidate";
    }

    location = /sw.js {
        add_header Cache-Control "public, max-age=0, must-revalidate";
    }

    location /static {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    location ~* \.(?:js|css)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    #rewrite ^([^.\?]*[^/])$ $1/ permanent;
    #try_files \$uri \$uri/ \$uri/index.html =404
    #try_files \$uri \$uri/index.html =404
    try_files $uri $uri/ $uri/index.html =404;
}

So this configuration actually works. I can browse all sites, but there is one issue. Traefik redirects https to http if there is no trailing slash. For instance:

ben@happiness:~/public$ curl -I https://example.com/contact
HTTP/1.1 301 Moved Permanently
Content-Length: 169
Content-Type: text/html
Date: Sat, 08 Aug 2020 09:36:25 GMT
Location: http://example.com/contact/
Server: nginx/1.19.1

Which in turn redirects to the final destination:

ben@happiness:~/public$ curl -I http://example.com/contact/
HTTP/1.1 308 Permanent Redirect
Location: https://example.com/contact/
Date: Sat, 08 Aug 2020 09:37:33 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

Not only that it is ugly to redirect https to http and probably insecure, but also my Mautic form doesn't work properly (i.e., doesn't redirect to the thank-you page and doesn't show if a mandatory field has not been filled).

So I played with adding a trailing slash (cf. the two docker-compose.yml). Specifically, if I uncomment in my nginx docker-compose.yml the following line:

- "traefik.http.routers.nginx-secure.middlewares=add-trailing-slash"

With forced trailing slashes redirects look okay:

ben@happiness:~/public$ curl -I https://example.com/contact
HTTP/1.1 308 Permanent Redirect
Location: https://example.com/contact/
Date: Sat, 08 Aug 2020 09:42:20 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

Yet, I don't know how to setup nginx in this case. In particular, nginx runs into several 404s when accessing the site:

VM684:1 GET https://example.com/page-data/app-data.json 404
(anonymous) @ VM684:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
C @ app-fd0fd20afca3d57009ab.js:1
t.memoizedGet @ app-fd0fd20afca3d57009ab.js:1
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
t.loadPage @ app-fd0fd20afca3d57009ab.js:1
loadPage @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
UxWs @ app-fd0fd20afca3d57009ab.js:1
a @ webpack-runtime-703898b324de4f9dc223.js:1
t @ webpack-runtime-703898b324de4f9dc223.js:1
r @ webpack-runtime-703898b324de4f9dc223.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
VM684:1 GET https://example.com/page-data/index/page-data.json 404
(anonymous) @ VM684:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
C @ app-fd0fd20afca3d57009ab.js:1
t.memoizedGet @ app-fd0fd20afca3d57009ab.js:1
t.fetchPageDataJson @ app-fd0fd20afca3d57009ab.js:1
t.loadPageDataJson @ app-fd0fd20afca3d57009ab.js:1
r.loadPageDataJson @ app-fd0fd20afca3d57009ab.js:1
t.loadPage @ app-fd0fd20afca3d57009ab.js:1
loadPage @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
UxWs @ app-fd0fd20afca3d57009ab.js:1
a @ webpack-runtime-703898b324de4f9dc223.js:1
t @ webpack-runtime-703898b324de4f9dc223.js:1
r @ webpack-runtime-703898b324de4f9dc223.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
VM684:1 GET https://example.com/page-data/app-data.json 404
(anonymous) @ VM684:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
C @ app-fd0fd20afca3d57009ab.js:1
t.memoizedGet @ app-fd0fd20afca3d57009ab.js:1
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
t.loadPage @ app-fd0fd20afca3d57009ab.js:1
loadPage @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
UxWs @ app-fd0fd20afca3d57009ab.js:1
a @ webpack-runtime-703898b324de4f9dc223.js:1
t @ webpack-runtime-703898b324de4f9dc223.js:1
r @ webpack-runtime-703898b324de4f9dc223.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
VM684:1 GET https://example.com/page-data/404.html/page-data.json 404
(anonymous) @ VM684:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
C @ app-fd0fd20afca3d57009ab.js:1
t.memoizedGet @ app-fd0fd20afca3d57009ab.js:1
t.fetchPageDataJson @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
t.fetchPageDataJson @ app-fd0fd20afca3d57009ab.js:1
t.loadPageDataJson @ app-fd0fd20afca3d57009ab.js:1
r.loadPageDataJson @ app-fd0fd20afca3d57009ab.js:1
t.loadPage @ app-fd0fd20afca3d57009ab.js:1
loadPage @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
UxWs @ app-fd0fd20afca3d57009ab.js:1
a @ webpack-runtime-703898b324de4f9dc223.js:1
t @ webpack-runtime-703898b324de4f9dc223.js:1
r @ webpack-runtime-703898b324de4f9dc223.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
VM684:1 GET https://example.com/page-data/app-data.json 404
(anonymous) @ VM684:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
C @ app-fd0fd20afca3d57009ab.js:1
t.memoizedGet @ app-fd0fd20afca3d57009ab.js:1
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
t.loadPage @ app-fd0fd20afca3d57009ab.js:1
loadPage @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
UxWs @ app-fd0fd20afca3d57009ab.js:1
a @ webpack-runtime-703898b324de4f9dc223.js:1
t @ webpack-runtime-703898b324de4f9dc223.js:1
r @ webpack-runtime-703898b324de4f9dc223.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
VM684:1 GET https://example.com/page-data/app-data.json 404
(anonymous) @ VM684:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
C @ app-fd0fd20afca3d57009ab.js:1
t.memoizedGet @ app-fd0fd20afca3d57009ab.js:1
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
t.loadAppData @ app-fd0fd20afca3d57009ab.js:1
t.loadPage @ app-fd0fd20afca3d57009ab.js:1
loadPage @ app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
UxWs @ app-fd0fd20afca3d57009ab.js:1
a @ webpack-runtime-703898b324de4f9dc223.js:1
t @ webpack-runtime-703898b324de4f9dc223.js:1
r @ webpack-runtime-703898b324de4f9dc223.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
app-fd0fd20afca3d57009ab.js:1 Uncaught (in promise) Error: page resources for / not found. Not rendering React
    at app-fd0fd20afca3d57009ab.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
Promise.then (async)
UxWs @ app-fd0fd20afca3d57009ab.js:1
a @ webpack-runtime-703898b324de4f9dc223.js:1
t @ webpack-runtime-703898b324de4f9dc223.js:1
r @ webpack-runtime-703898b324de4f9dc223.js:1
(anonymous) @ app-fd0fd20afca3d57009ab.js:1
manifest.json:1 GET https://example.com/icons-2282555dbdb424e391e15f39684cacbd/manifest.json 404
manifest.json:1 Manifest: Line: 1, column: 1, Syntax error.

I highly appreciate any pointer on how to fix these issues!

It would also be great to learn how to setup http2 in this scenario!

Thanks!
Ben

Did you solve the problem?

Unfortunately, no :confused:

I solved my case adding redirect http to https on Traefik config:

            - "--entrypoints.web.address=:80"
            - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
            - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
            - "--entrypoints.websecure.address=:443"

My reference was: https://doc.traefik.io/traefik/routing/entrypoints/#http-options

Thanks, this is what I'm already doing in the traefik.yml

entryPoints:
  http:
    address: ":80"
    #compress: false
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https
          permanent: true
  https:
    address: ":443"

Unfortunately, the issue still persists...

Problem has been resolved in nginx.conf by adding:

rewrite ^([^.]*[^/])$ https://example.com$1/ permanent;