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