♻️ Simplify Traefik deployment, make it part of the stack (#2412)

This commit is contained in:
Sebastián Ramírez
2026-08-09 20:21:42 +02:00
committed by GitHub
parent 75c388ba1c
commit 0137c3f2c3
11 changed files with 109 additions and 267 deletions
-1
View File
@@ -15,7 +15,6 @@ FRONTEND_HOST=http://localhost:5173
ENVIRONMENT=local
PROJECT_NAME="Full Stack FastAPI Project"
STACK_NAME=full-stack-fastapi-project
# Backend
BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://localhost.tiangolo.com"
+4 -5
View File
@@ -18,9 +18,8 @@ jobs:
- production
env:
ENVIRONMENT: production
DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }}
FRONTEND_HOST: https://${{ secrets.DOMAIN_PRODUCTION }}
STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }}
DOMAIN: ${{ secrets.DOMAIN }}
FRONTEND_HOST: https://${{ secrets.DOMAIN }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
@@ -35,5 +34,5 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d
- run: docker compose -f compose.yml -f compose.deploy.yml build
- run: docker compose -f compose.yml -f compose.deploy.yml up -d
+4 -5
View File
@@ -18,9 +18,8 @@ jobs:
- staging
env:
ENVIRONMENT: staging
DOMAIN: ${{ secrets.DOMAIN_STAGING }}
FRONTEND_HOST: https://${{ secrets.DOMAIN_STAGING }}
STACK_NAME: ${{ secrets.STACK_NAME_STAGING }}
DOMAIN: ${{ secrets.DOMAIN }}
FRONTEND_HOST: https://${{ secrets.DOMAIN }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
@@ -35,5 +34,5 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d
- run: docker compose -f compose.yml -f compose.deploy.yml build
- run: docker compose -f compose.yml -f compose.deploy.yml up -d
+1 -2
View File
@@ -24,7 +24,7 @@
- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development.
- ✅ Tests with [Pytest](https://pytest.org).
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
- 🚢 Deployment instructions using Docker Compose, including how to set up Traefik to handle automatic HTTPS certificates.
- 🚢 Deployment instructions using Docker Compose with automatic HTTPS provided by Traefik.
- 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions.
### Dashboard Login
@@ -196,7 +196,6 @@ But don't worry, you can just update any of that in the `.env` files afterwards.
The input variables, with their default values (some auto generated) are:
- `project_name`: (default: `"FastAPI Project"`) The name of the project, shown to API users (in .env).
- `stack_name`: (default: `"fastapi-project"`) The name of the stack used for Docker Compose labels and project name (no spaces, no periods) (in .env).
- `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above.
- `first_superuser`: (default: `"admin@example.com"`) The email of the first superuser (in .env).
- `first_superuser_password`: (default: `"changethis"`) The password of the first superuser (in .env).
+59
View File
@@ -0,0 +1,59 @@
services:
proxy:
restart: always
ports:
# Listen on port 80, default for HTTP, necessary to redirect to HTTPS
- "80:80"
# Listen on port 443, default for HTTPS
- "443:443"
volumes:
# Mount the volume to store the certificates
- traefik-certificates:/certificates
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80
# Redirect all HTTP traffic to HTTPS
- --entrypoints.http.http.redirections.entrypoint.to=https
- --entrypoints.http.http.redirections.entrypoint.scheme=https
# Create an entrypoint "https" listening on port 443
- --entrypoints.https.address=:443
# Store the Let's Encrypt certificates in the mounted volume
- --certificatesresolvers.le.acme.storage=/certificates/acme.json
# Use the TLS Challenge for Let's Encrypt
- --certificatesresolvers.le.acme.tlschallenge=true
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
db:
restart: always
adminer:
restart: always
labels:
# Route HTTPS traffic for the Adminer subdomain
- traefik.http.routers.adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`)
- traefik.http.routers.adminer-https.entrypoints=https
- traefik.http.routers.adminer-https.tls=true
# Use the Let's Encrypt resolver
- traefik.http.routers.adminer-https.tls.certresolver=le
backend:
restart: always
labels:
# Route HTTPS traffic for this domain
- traefik.http.routers.backend-https.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.backend-https.entrypoints=https
- traefik.http.routers.backend-https.tls=true
# Use the Let's Encrypt resolver
- traefik.http.routers.backend-https.tls.certresolver=le
volumes:
# Create a volume to store the certificates, even if the container is recreated
traefik-certificates:
-25
View File
@@ -6,8 +6,6 @@ services:
# DOMAIN=localhost.tiangolo.com
proxy:
image: traefik:3.6
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- "80:80"
- "8090:8080"
@@ -15,14 +13,10 @@ services:
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Add a constraint to only use services with the label for this stack
- --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80
# Create an entrypoint "https" listening on port 443
- --entrypoints.https.address=:443
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
@@ -33,29 +27,15 @@ services:
- --api
# Enable the Dashboard and API in insecure mode for local development
- --api.insecure=true
labels:
# Enable Traefik for this service, to make it available in the public network
- traefik.enable=true
- traefik.constraint-label=traefik-public
# Dummy https-redirect middleware that doesn't really redirect, only to
# allow running it locally
- traefik.http.middlewares.https-redirect.contenttype.autodetect=false
networks:
- traefik-public
- default
db:
restart: "no"
ports:
- "5432:5432"
adminer:
restart: "no"
ports:
- "8080:8080"
backend:
restart: "no"
ports:
- "8000:8000"
build:
@@ -123,8 +103,3 @@ services:
- ./frontend/test-results:/app/frontend/test-results
ports:
- 9323:9323
networks:
traefik-public:
# For local dev, don't expect an external Traefik network
external: false
-77
View File
@@ -1,77 +0,0 @@
services:
traefik:
image: traefik:3.6
ports:
# Listen on port 80, default for HTTP, necessary to redirect to HTTPS
- 80:80
# Listen on port 443, default for HTTPS
- 443:443
restart: always
labels:
# Enable Traefik for this service, to make it available in the public network
- traefik.enable=true
# Use the traefik-public network (declared below)
- traefik.docker.network=traefik-public
# Define the port inside of the Docker service to use
- traefik.http.services.traefik-dashboard.loadbalancer.server.port=8080
# Make Traefik use this domain (from an environment variable) in HTTP
- traefik.http.routers.traefik-dashboard-http.entrypoints=http
- traefik.http.routers.traefik-dashboard-http.rule=Host(`traefik.${DOMAIN?Variable not set}`)
# traefik-https the actual router using HTTPS
- traefik.http.routers.traefik-dashboard-https.entrypoints=https
- traefik.http.routers.traefik-dashboard-https.rule=Host(`traefik.${DOMAIN?Variable not set}`)
- traefik.http.routers.traefik-dashboard-https.tls=true
# Use the "le" (Let's Encrypt) resolver created below
- traefik.http.routers.traefik-dashboard-https.tls.certresolver=le
# Use the special Traefik service api@internal with the web UI/Dashboard
- traefik.http.routers.traefik-dashboard-https.service=api@internal
# https-redirect middleware to redirect HTTP to HTTPS
- traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
# traefik-http set up only to use the middleware to redirect to https
- traefik.http.routers.traefik-dashboard-http.middlewares=https-redirect
# admin-auth middleware with HTTP Basic auth
# Using the environment variables USERNAME and HASHED_PASSWORD
- traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
# Enable HTTP Basic auth, using the middleware created above
- traefik.http.routers.traefik-dashboard-https.middlewares=admin-auth
volumes:
# Add Docker as a mounted volume, so that Traefik can read the labels of other services
- /var/run/docker.sock:/var/run/docker.sock:ro
# Mount the volume to store the certificates
- traefik-public-certificates:/certificates
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80
# Create an entrypoint "https" listening on port 443
- --entrypoints.https.address=:443
# Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL
- --certificatesresolvers.le.acme.email=${EMAIL?Variable not set}
# Store the Let's Encrypt certificates in the mounted volume
- --certificatesresolvers.le.acme.storage=/certificates/acme.json
# Use the TLS Challenge for Let's Encrypt
- --certificatesresolvers.le.acme.tlschallenge=true
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
# Enable the Dashboard and API
- --api
networks:
# Use the public network created to be shared between Traefik and
# any other service that needs to be publicly available with HTTPS
- traefik-public
volumes:
# Create a volume to store the certificates, even if the container is recreated
traefik-public-certificates:
networks:
# Use the previously created public network "traefik-public", shared with other
# services that need to be publicly available via this Traefik
traefik-public:
external: true
+29 -39
View File
@@ -1,8 +1,24 @@
services:
proxy:
image: traefik:3.6
volumes:
# Add Docker as a mounted volume, so that Traefik can read the labels of other services
- /var/run/docker.sock:/var/run/docker.sock:ro
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
db:
image: postgres:18
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
@@ -20,35 +36,24 @@ services:
adminer:
image: adminer
restart: always
networks:
- traefik-public
- default
depends_on:
- db
environment:
- ADMINER_DESIGN=pepa-linha-dark
labels:
# Enable Traefik for this service
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.middlewares=https-redirect
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls.certresolver=le
- traefik.http.services.${STACK_NAME?Variable not set}-adminer.loadbalancer.server.port=8080
# Route HTTP traffic for the Adminer subdomain
- traefik.http.routers.adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`)
- traefik.http.routers.adminer-http.entrypoints=http
# Define the port inside of the Docker service to use
- traefik.http.services.adminer.loadbalancer.server.port=8080
prestart:
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
build:
context: .
dockerfile: backend/Dockerfile
networks:
- traefik-public
- default
depends_on:
db:
condition: service_healthy
@@ -77,10 +82,6 @@ services:
backend:
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
restart: always
networks:
- traefik-public
- default
depends_on:
db:
condition: service_healthy
@@ -118,26 +119,15 @@ services:
context: .
dockerfile: backend/Dockerfile
labels:
# Enable Traefik for this service
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000
# Define the port inside of the Docker service to use
- traefik.http.services.backend.loadbalancer.server.port=8000
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http
# Route HTTP traffic for this domain
- traefik.http.routers.backend-http.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.backend-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le
# Enable redirection for HTTP and HTTPS
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect
volumes:
app-db-data:
networks:
traefik-public:
# Allow setting it to false for testing
external: true
-5
View File
@@ -3,11 +3,6 @@ project_name:
help: The name of the project, shown to API users (in .env)
default: FastAPI Project
stack_name:
type: str
help: The name of the stack used for Docker Compose labels (no spaces) (in .env)
default: fastapi-project
secret_key:
type: str
help: |
+9 -107
View File
@@ -2,7 +2,7 @@
You can deploy the project using Docker Compose to a remote server.
This project expects you to have a Traefik proxy handling communication to the outside world and HTTPS certificates.
The deployment Docker Compose configuration includes Traefik to handle HTTPS and route incoming traffic to the application.
You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions.
@@ -10,101 +10,13 @@ But you have to configure a couple things first. 🤓
## Preparation
* Have a remote server ready and available.
* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com`, `traefik.fastapi-project.example.com`, and `adminer.fastapi-project.example.com`. Do the same for staging domains such as `staging.fastapi-project.example.com`.
* Have a remote server ready and available. Use a separate server for each environment, for example one for staging and one for production.
* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com` and `adminer.fastapi-project.example.com`.
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
## Public Traefik
We need a Traefik proxy to handle incoming connections and HTTPS certificates.
You need to do these next steps only once.
### Traefik Docker Compose
* Create a remote directory to store your Traefik Docker Compose file:
```bash
mkdir -p /root/code/traefik-public/
```
Copy the Traefik Docker Compose file to your server. You could do it by running the command `rsync` in your local terminal:
```bash
rsync -a compose.traefik.yml root@your-server.example.com:/root/code/traefik-public/
```
### Traefik Public Network
This Traefik will expect a Docker "public network" named `traefik-public` to communicate with your stack(s).
This way, there will be a single public Traefik proxy that handles the communication (HTTP and HTTPS) with the outside world, and then behind that, you could have one or more stacks with different domains, even if they are on the same single server.
To create a Docker "public network" named `traefik-public` run the following command in your remote server:
```bash
docker network create traefik-public
```
### Traefik Environment Variables
The Traefik Docker Compose file expects some environment variables to be set in your terminal before starting it. You can do it by running the following commands in your remote server.
* Create the username for HTTP Basic Auth, e.g.:
```bash
export USERNAME=admin
```
* Create an environment variable with the password for HTTP Basic Auth, e.g.:
```bash
export PASSWORD=changethis
```
* Use openssl to generate the "hashed" version of the password for HTTP Basic Auth and store it in an environment variable:
```bash
export HASHED_PASSWORD=$(openssl passwd -apr1 $PASSWORD)
```
To verify that the hashed password is correct, you can print it:
```bash
echo $HASHED_PASSWORD
```
* Create an environment variable with the domain name for your server, e.g.:
```bash
export DOMAIN=fastapi-project.example.com
```
* Create an environment variable with the email for Let's Encrypt, e.g.:
```bash
export EMAIL=admin@example.com
```
**Note**: you need to set a different email, an email `@example.com` won't work.
### Start the Traefik Docker Compose
Go to the directory where you copied the Traefik Docker Compose file in your remote server:
```bash
cd /root/code/traefik-public/
```
Now with the environment variables set and the `compose.traefik.yml` in place, you can start the Traefik Docker Compose running the following command:
```bash
docker compose -f compose.traefik.yml up -d
```
## Deploy the FastAPI Project
Now that you have Traefik in place you can deploy your FastAPI project with Docker Compose.
You can deploy your FastAPI project with Docker Compose.
**Note**: You might want to jump ahead to the section about Continuous Deployment with GitHub Actions.
@@ -175,7 +87,6 @@ export FRONTEND_HOST="https://${DOMAIN?Variable not set}"
You can set several other environment variables:
* `PROJECT_NAME`: The name of the project, used in the API for the docs and emails.
* `STACK_NAME`: The name of the stack used for Docker Compose labels and project name, this should be different for `staging`, `production`, etc. You could use the same domain replacing dots with dashes, e.g. `fastapi-project-example-com` and `staging-fastapi-project-example-com`.
* `BACKEND_CORS_ORIGINS`: A list of additional allowed CORS origins separated by commas. The frontend served by FastAPI uses the same origin and doesn't need to be added.
* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users.
* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc).
@@ -201,19 +112,17 @@ With the environment variables in place, you can deploy with Docker Compose:
```bash
cd /root/code/app/
docker compose -f compose.yml build
docker compose -f compose.yml up -d
docker compose -f compose.yml -f compose.deploy.yml build
docker compose -f compose.yml -f compose.deploy.yml up -d
```
For production you wouldn't want to have the overrides in `compose.override.yml`, that's why we explicitly specify `compose.yml` as the file to use.
The `compose.deploy.yml` file adds the deployment settings to the shared configuration in `compose.yml`, including HTTPS and automatic certificate handling. Explicitly listing these files also excludes the local development settings in `compose.override.yml`.
## Continuous Deployment (CD)
You can use GitHub Actions to deploy your project automatically. 😎
You can have multiple environment deployments.
There are already two environments configured, `staging` and `production`. 🚀
There are already two environment deployments configured, `staging` and `production`. Each environment should be deployed to a separate server. 🚀
### Install GitHub Actions Runner
@@ -299,10 +208,7 @@ For each GitHub Environment (`staging` and `production`), configure the required
The current Github Actions workflows expect these secrets:
* `DOMAIN_PRODUCTION`
* `DOMAIN_STAGING`
* `STACK_NAME_PRODUCTION`
* `STACK_NAME_STAGING`
* `DOMAIN`
* `EMAILS_FROM_EMAIL`
* `FIRST_SUPERUSER`
* `FIRST_SUPERUSER_PASSWORD`
@@ -326,10 +232,6 @@ If you need to add extra environments you could use those as a starting point.
Replace `fastapi-project.example.com` with your domain.
### Main Traefik Dashboard
Traefik UI: `https://traefik.fastapi-project.example.com`
### Production
Application (frontend and API): `https://fastapi-project.example.com`
+3 -1
View File
@@ -97,7 +97,7 @@ After you update it, run again:
docker compose watch
```
When deploying, for example in production, the main Traefik is configured outside of the Docker Compose files. For local development, there's an included Traefik in `compose.override.yml`, just to let you test that the domain works as expected, for example with `localhost.tiangolo.com`.
Traefik is included in the main Docker Compose file. The development overrides expose its local dashboard so you can inspect the routes used with `localhost.tiangolo.com`.
## Docker Compose files and env vars
@@ -105,6 +105,8 @@ There is a main `compose.yml` file with all the configurations that apply to the
And there's also a `compose.override.yml` with overrides for development, for example to mount the source code as a volume. It is used automatically by `docker compose` to apply overrides on top of `compose.yml`.
The `compose.deploy.yml` file contains the deployment-specific settings, including HTTPS and automatic certificate handling. It is explicitly combined with `compose.yml` when deploying the application.
These Docker Compose files use the `.env` file containing configurations to be injected as environment variables in the containers.
They also use some additional configurations taken from environment variables set in the scripts before calling the `docker compose` command.