Proxy contents of an S3 bucket (serving a React web app) entirely configured through labels

We typically ship web applications that feature an API built using Python + FastAPI and the front end built using React. We deploy the production version of the React application to S3 (or compatible buckets) configured to serve a web site and then proxy everything using traefik.

Our simplest use cases use docker-compose and have had a configuration working partially through labels and files where the API (in a container) is configured via labels and the S3 bucket through a toml file.

All references source code is available on this repository

An example looks like traefik-dynamic.toml:

[http]
    [http.middlewares]
        [http.middlewares.host.headers]
            [http.middlewares.host.headers.customRequestHeaders]
                Host = "bucket-name.website-ap-south-1.linodeobjects.com"

    [http.routers]
        [http.routers.website]
            entryPoints = ["https"]
            service = "bucket"
            middlewares = "host"
            rule = "Host(`productiondomain.com`)"
            priority=1
            [http.routers.website.tls]
                certResolver = "letsencrypt"

    [http.services]
        [http.services.bucket.loadBalancer]
            [[http.services.bucket.loadBalancer.servers]]
                url = "http://bucker-name.website-ap-south-1.linodeobjects.com"

which works with along with:

services:

  reverse-proxy:
    image: traefik:v2.8
    command:
      - "--providers.docker"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.file"
      - "--providers.file.filename=/opt/traefik/traefik-dynamic.toml"
      - "--entrypoints.http.address=:80"
      - "--entrypoints.http.http.redirections.entryPoint.to=:443"
      - "--entrypoints.http.http.redirections.entryPoint.scheme=https"
      - "--entrypoints.http.http.redirections.entrypoint.permanent=true"
      - "--entrypoints.https.address=:443"
      - "--certificatesResolvers.letsencrypt.acme.email=${SOA_EMAIL}"
      - "--certificatesResolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesResolvers.letsencrypt.acme.httpChallenge.entrypoint=http"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /opt/anomaly-lab/data/letsencrypt:/letsencrypt
      - /opt/anomaly-lab/traefik-dynamic.toml:/opt/traefik/traefik-dynamic.toml
    restart: unless-stopped

I am trying to move towards completely configuring this via labels and have been able to port the API configuration. I am obviously missing something where the top level domain will not proxy the contents of the bucket and gives me a 404.

As part of this I am trying to strengthen the setup by adding HSTS headers etc.

My configuration currently looks like:

services:

  reverse-proxy:
    image: traefik:v3.0
    command:
      # Remove this for production, this exposes the web UI
      - "--providers.docker"
      - "--log.level=DEBUG"
      - "--providers.docker.exposedbydefault=false"
      # Listen to port 80 solely to redirect it
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.web.http.redirections.entrypoint.permanent=true"
      # Listen to https for all other
      - "--entrypoints.websecure.address=:443"
      # Enable HTTP3 which is no longer experimental in t3.0
      - "--entrypoints.websecure.http3"
      # This allows us to use the staging server for development
      # We could potentially move this to a variable name
      #- "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
      - "--certificatesResolvers.letsencrypt.acme.email=${SOA_EMAIL}"
      - "--certificatesResolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesResolvers.letsencrypt.acme.httpChallenge.entrypoint=web"
    ports:
      - 80:80
      - 443:443/tcp
      - 443:443/udp
    labels:
      - "traefik.enable"
      # We start off the TLS configuration
      # Ensure that the SSL version is set to a minimum of 1.2
      - "traefik.tls.options.default.minVersion=VersionTLS12"
      # Send X-Frame-Options to DENY
      - "traefik.http.middlewares.header-security.headers.frameDeny=true"
      # HSTS security headers
      # the time has been set to one non-leap year
      - "traefik.websecure.middlewares.header-security.headers.stsSeconds=315360000"
      - "traefik.websecure.middlewares.header-security.headers.stsIncludeSubdomains=true"
      - "traefik.websecure.middlewares.header-security.headers.stsPreload=true"
      # set the hsts header even in http - see if this required
      - "traefik.http.middlewares.header-security.headers.forceSTSHeader=true"
      - "traefik.http.routers.${PROJ_NAME}-root.entrypoints=websecure"
      - "traefik.http.routers.${PROJ_NAME}-root.rule=Host(`${PROJ_FQDN}`)"
      - "traefik.http.routers.${PROJ_NAME}-root.tls"
      - "traefik.http.routers.${PROJ_NAME}-root.tls.passthrough=true"
      - "traefik.http.routers.${PROJ_NAME}-root.tls.certResolver=letsencrypt"
      - "traefik.http.routers.${PROJ_NAME}-root.priority=1"
      - "traefik.http.routers.${PROJ_NAME}-root.entryPoints=websecure"
      - "traefik.http.routers.${PROJ_NAME}-root.services=${PROJ_NAME}-root"
      - "traefik.http.routers.${PROJ_NAME}-root.middlewares=header-bucket"
      - "traefik.http.routers.${PROJ_NAME}-root.middlewares=header-security"
      # Proxy the bucket or another container for the web client
      - "traefik.http.middlewares.header-bucket.headers.customrequestheaders.host=${BUCKET_FQDN}"
      # Declare a service to reverer proxy
      - "traefik.http.services.${PROJ_NAME}-root.loadbalancer.servers.url=${BUCKET_FQDN}"
      - "traefik.http.services.${PROJ_NAME}-root.loadbalancer.passhostheaders=true"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /opt/data/letsencrypt:/letsencrypt
      - /opt/elsaf2/traefik-dynamic.toml:/opt/traefik/traefik-dynamic.toml
    restart: unless-stopped

What works:

  • SSL is provisioned fine
  • HTTP to HTTPS redirect works fine
  • The API is mounted on /api from another containers and serves fine

What does not work:

  • Serving the contents of the S3 bucket on the root of the domain

What am I missing here? I have searched around not to find a configuration that is trying to achieve the same thing.

Any pointers are very welcome and thanks for sparing your time.

My WIP is also available at this repository

You can not only use labels, Docker Configuration Discovery does not support loadBalancer.servers.url.

There is already a Github issue, which you could upvote.

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