From 96c1147ee7eb33b84c0942585011f57b5a0ba7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 8 Aug 2026 18:07:39 +0200 Subject: [PATCH 01/12] =?UTF-8?q?=F0=9F=94=92=20Replace=20pre-commit=20PAT?= =?UTF-8?q?=20with=20PR=20Push=20(#2407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pr-push.yml | 2 ++ .github/workflows/pre-commit.yml | 36 ++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .github/pr-push.yml diff --git a/.github/pr-push.yml b/.github/pr-push.yml new file mode 100644 index 00000000..15913650 --- /dev/null +++ b/.github/pr-push.yml @@ -0,0 +1,2 @@ +workflows: + - .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 98c50d66..2dc07362 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -2,16 +2,17 @@ name: pre-commit on: pull_request: -permissions: - contents: read +permissions: {} env: - # Forks and Dependabot don't have access to secrets - HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }} + CAN_PUSH: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} jobs: pre-commit: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write timeout-minutes: 5 steps: - name: Dump GitHub context @@ -20,20 +21,18 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout PR for own repo - if: env.HAS_SECRETS == 'true' + if: env.CAN_PUSH == 'true' with: # To be able to commit it needs to fetch the head of the branch, not the # merge commit ref: ${{ github.head_ref }} # And it needs the full history to be able to compute diffs fetch-depth: 0 - # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI - token: ${{ secrets.PRE_COMMIT }} # zizmor: ignore[secrets-outside-env] - persist-credentials: true # Required for `git push` command + persist-credentials: false # pre-commit lite ci needs the default checkout configs to work - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout PR for fork - if: env.HAS_SECRETS == 'false' + if: env.CAN_PUSH == 'false' with: # To be able to commit it needs the head branch of the PR, the remote one ref: ${{ github.event.pull_request.head.sha }} @@ -64,11 +63,26 @@ jobs: id: precommit run: uv run prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure continue-on-error: true + - name: Check for changes + id: changes + run: | + if [[ -n "$(git status --porcelain)" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + - name: Get PR Push token + id: pr-push + if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' + uses: tiangolo/pr-push@ff4e51a433de4c22bbf90597e069e8247b9203d2 # 0.0.1 - name: Commit and push changes - if: env.HAS_SECRETS == 'true' + if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' + env: + PR_PUSH_TOKEN: ${{ steps.pr-push.outputs.token }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${PR_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" git add -A if git diff --staged --quiet; then echo "No changes to commit" @@ -77,7 +91,7 @@ jobs: git push fi - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - if: env.HAS_SECRETS == 'false' + if: env.CAN_PUSH == 'false' with: msg: 🎨 Auto format and update with pre-commit - name: Error out on pre-commit errors From e5cdc73405042be59751b0adba0c26a181309a7b Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:07:42 +0000 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 18ca3311..445799bb 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.1.13 to >=3.3.4,<4.0.0. PR [#2399](https://github.com/fastapi/full-stack-fastapi-template/pull/2399) by [@dependabot[bot]](https://github.com/apps/dependabot). From 34378100a8f6020120ecf81cac8214232e7d5af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 8 Aug 2026 23:23:30 +0200 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=91=B7=20Use=20PR=20Push=20commit?= =?UTF-8?q?=20identity=20(#2409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 2dc07362..9d53bef4 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -80,8 +80,8 @@ jobs: env: PR_PUSH_TOKEN: ${{ steps.pr-push.outputs.token }} run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "pr-push[bot]" + git config user.email "pr-push[bot]@users.noreply.github.com" git remote set-url origin "https://x-access-token:${PR_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" git add -A if git diff --staged --quiet; then From 220bcb74aabd294bc388134360c7758d032c9fbb Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:23:32 +0000 Subject: [PATCH 04/12] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 445799bb..b433f221 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo). * 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). * 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo). From 6a1c084f6ec9ddf52fc0608af7b1d79dde1bebab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 00:28:17 +0200 Subject: [PATCH 05/12] =?UTF-8?q?=F0=9F=91=B7=20Use=20GitHub=20CLI=20for?= =?UTF-8?q?=20Git=20authentication=20(#2410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9d53bef4..a1ea6d2c 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -78,11 +78,11 @@ jobs: - name: Commit and push changes if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' env: - PR_PUSH_TOKEN: ${{ steps.pr-push.outputs.token }} + GH_TOKEN: ${{ steps.pr-push.outputs.token }} run: | git config user.name "pr-push[bot]" git config user.email "pr-push[bot]@users.noreply.github.com" - git remote set-url origin "https://x-access-token:${PR_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + gh auth setup-git git add -A if git diff --staged --quiet; then echo "No changes to commit" From 9097b329d696c071e58647f409d081bcced75a1b Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:28:20 +0000 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index b433f221..2b257e7d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 👷 Use GitHub CLI for Git authentication. PR [#2410](https://github.com/fastapi/full-stack-fastapi-template/pull/2410) by [@tiangolo](https://github.com/tiangolo). * 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo). * 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). From d5b20172e401ed63ea7a4dae6a09a2ed4936a464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 00:47:39 +0200 Subject: [PATCH 07/12] =?UTF-8?q?=F0=9F=94=90=20Use=20PR=20Submit=20for=20?= =?UTF-8?q?pull=20requests=20(#2411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pr-submit.yml | 2 ++ .github/workflows/bump-pre-commit-hooks.yml | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .github/pr-submit.yml diff --git a/.github/pr-submit.yml b/.github/pr-submit.yml new file mode 100644 index 00000000..1c5d0aa4 --- /dev/null +++ b/.github/pr-submit.yml @@ -0,0 +1,2 @@ +workflows: + - .github/workflows/bump-pre-commit-hooks.yml diff --git a/.github/workflows/bump-pre-commit-hooks.yml b/.github/workflows/bump-pre-commit-hooks.yml index 9442b353..c549b207 100644 --- a/.github/workflows/bump-pre-commit-hooks.yml +++ b/.github/workflows/bump-pre-commit-hooks.yml @@ -12,6 +12,9 @@ jobs: if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + id-token: write steps: - name: Dump GitHub context env: @@ -19,8 +22,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - token: ${{ secrets.LATEST_CHANGES }} - persist-credentials: true + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: @@ -36,9 +38,12 @@ jobs: uv.lock - name: Bump pre-commit hooks run: uv run prek auto-update --freeze --cooldown-days 7 + - name: Get PR Submit token + id: pr-submit + uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1 - name: Create pull request env: - GH_TOKEN: ${{ secrets.LATEST_CHANGES }} + GH_TOKEN: ${{ steps.pr-submit.outputs.token }} BASE_BRANCH: ${{ github.event.repository.default_branch }} run: | set -euo pipefail @@ -46,12 +51,13 @@ jobs: echo "No pre-commit hook updates available" exit 0 fi - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "pr-submit[bot]" + git config user.email "pr-submit[bot]@users.noreply.github.com" branch="bump-pre-commit-hooks" git switch -C "$branch" git add .pre-commit-config.yaml git commit -m "⬆ Bump pre-commit hooks" + gh auth setup-git git push --force origin "$branch" if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then gh pr create \ From 75c388ba1cd60f39b43dd63a73978bae8c69cb87 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:47:42 +0000 Subject: [PATCH 08/12] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 2b257e7d..f9902d91 100644 --- a/release-notes.md +++ b/release-notes.md @@ -30,6 +30,7 @@ ### Internal +* 🔐 Use PR Submit for pull requests. PR [#2411](https://github.com/fastapi/full-stack-fastapi-template/pull/2411) by [@tiangolo](https://github.com/tiangolo). * 👷 Use GitHub CLI for Git authentication. PR [#2410](https://github.com/fastapi/full-stack-fastapi-template/pull/2410) by [@tiangolo](https://github.com/tiangolo). * 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo). * 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo). From 0137c3f2c34a15fe825bc0f980d034ed946e2d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 20:21:42 +0200 Subject: [PATCH 09/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20Traefik?= =?UTF-8?q?=20deployment,=20make=20it=20part=20of=20the=20stack=20(#2412)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 - .github/workflows/deploy-production.yml | 9 +- .github/workflows/deploy-staging.yml | 9 +- README.md | 3 +- compose.deploy.yml | 59 ++++++++++++ compose.override.yml | 25 ----- compose.traefik.yml | 77 ---------------- compose.yml | 68 ++++++-------- copier.yml | 5 - deployment.md | 116 ++---------------------- development.md | 4 +- 11 files changed, 109 insertions(+), 267 deletions(-) create mode 100644 compose.deploy.yml delete mode 100644 compose.traefik.yml diff --git a/.env b/.env index f0887ce4..2ab3e7e9 100644 --- a/.env +++ b/.env @@ -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" diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index f86c79e6..9c2b3965 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -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 diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 21adf429..61c3dd96 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -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 diff --git a/README.md b/README.md index 4cfc3446..57b170b3 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/compose.deploy.yml b/compose.deploy.yml new file mode 100644 index 00000000..5fa51263 --- /dev/null +++ b/compose.deploy.yml @@ -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: diff --git a/compose.override.yml b/compose.override.yml index 0c9e77dc..2378d468 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -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 diff --git a/compose.traefik.yml b/compose.traefik.yml deleted file mode 100644 index bcd7d142..00000000 --- a/compose.traefik.yml +++ /dev/null @@ -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 diff --git a/compose.yml b/compose.yml index 6debf53c..ac7625d9 100644 --- a/compose.yml +++ b/compose.yml @@ -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 diff --git a/copier.yml b/copier.yml index f98e3fc8..f28369e8 100644 --- a/copier.yml +++ b/copier.yml @@ -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: | diff --git a/deployment.md b/deployment.md index cc470e58..32bd71e4 100644 --- a/deployment.md +++ b/deployment.md @@ -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` diff --git a/development.md b/development.md index 250c1db8..576d3930 100644 --- a/development.md +++ b/development.md @@ -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. From 2d04828d6ab7c32699d0825bc5af00370d9a9f48 Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:21:44 +0000 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index f9902d91..aa103ce3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify Traefik deployment, make it part of the stack. PR [#2412](https://github.com/fastapi/full-stack-fastapi-template/pull/2412) by [@tiangolo](https://github.com/tiangolo). * ♻️ Serve frontend from FastAPI. PR [#2393](https://github.com/fastapi/full-stack-fastapi-template/pull/2393) by [@tiangolo](https://github.com/tiangolo). * 🔧 Refactor PostgreSQL env var config. PR [#2392](https://github.com/fastapi/full-stack-fastapi-template/pull/2392) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor models to improve types. PR [#2356](https://github.com/fastapi/full-stack-fastapi-template/pull/2356) by [@tiangolo](https://github.com/tiangolo). From 9c8983e2a5d4160b74e7b51f3015fbf19712443e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Aug 2026 22:26:19 +0200 Subject: [PATCH 11/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20environme?= =?UTF-8?q?nt=20configuration=20(#2413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 10 +-- .github/workflows/deploy-production.yml | 1 - .github/workflows/deploy-staging.yml | 1 - compose.deploy.yml | 6 ++ compose.yml | 87 ++++++++++--------------- deployment.md | 10 +-- 6 files changed, 47 insertions(+), 68 deletions(-) diff --git a/.env b/.env index 2ab3e7e9..efd4aa28 100644 --- a/.env +++ b/.env @@ -6,10 +6,8 @@ DOMAIN=localhost # DOMAIN=localhost.tiangolo.com # Public frontend URL, used by the backend to generate links in emails. -# Keep the Vite URL for local development; deployments use the application URL. +# Deployments derive this from DOMAIN in compose.deploy.yml. FRONTEND_HOST=http://localhost:5173 -# In staging and production, set this env var to the frontend host, e.g. -# FRONTEND_HOST=https://example.com # Environment: local, staging, production ENVIRONMENT=local @@ -17,7 +15,8 @@ ENVIRONMENT=local PROJECT_NAME="Full Stack FastAPI Project" # Backend -BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://localhost.tiangolo.com" +# Additional allowed CORS origins, separated by commas +BACKEND_CORS_ORIGINS= SECRET_KEY=changethis FIRST_SUPERUSER=admin@example.com FIRST_SUPERUSER_PASSWORD=changethis @@ -39,6 +38,3 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=changethis SENTRY_DSN= - -# Configure these with your own Docker registry images -DOCKER_IMAGE_BACKEND=backend diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 9c2b3965..cc4b34db 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -19,7 +19,6 @@ jobs: env: ENVIRONMENT: 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 }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 61c3dd96..256bd0d2 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -19,7 +19,6 @@ jobs: env: ENVIRONMENT: 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 }} diff --git a/compose.deploy.yml b/compose.deploy.yml index 5fa51263..a54c2d44 100644 --- a/compose.deploy.yml +++ b/compose.deploy.yml @@ -44,8 +44,14 @@ services: # Use the Let's Encrypt resolver - traefik.http.routers.adminer-https.tls.certresolver=le + prestart: + environment: + FRONTEND_HOST: https://${DOMAIN?Variable not set} + backend: restart: always + environment: + FRONTEND_HOST: https://${DOMAIN?Variable not set} labels: # Route HTTPS traffic for this domain - traefik.http.routers.backend-https.rule=Host(`${DOMAIN?Variable not set}`) diff --git a/compose.yml b/compose.yml index ac7625d9..bbc7aa65 100644 --- a/compose.yml +++ b/compose.yml @@ -49,65 +49,36 @@ services: # 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 - depends_on: - db: - condition: service_healthy - restart: true - command: bash scripts/prestart.sh - env_file: - - .env - environment: - - DOMAIN=${DOMAIN} - - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} - - ENVIRONMENT=${ENVIRONMENT} - - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} - - SECRET_KEY=${SECRET_KEY?Variable not set} - - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} - - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} - - SMTP_HOST=${SMTP_HOST} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} - - POSTGRES_SERVER=db - - POSTGRES_PORT=${POSTGRES_PORT} - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER?Variable not set} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - - SENTRY_DSN=${SENTRY_DSN} - backend: - image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' + image: backend:latest depends_on: db: condition: service_healthy restart: true prestart: condition: service_completed_successfully - env_file: - - .env - environment: - - DOMAIN=${DOMAIN} - - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} - - ENVIRONMENT=${ENVIRONMENT} - - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} - - SECRET_KEY=${SECRET_KEY?Variable not set} - - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} - - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} - - SMTP_HOST=${SMTP_HOST} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} - - POSTGRES_SERVER=db - - POSTGRES_PORT=${POSTGRES_PORT} - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER?Variable not set} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - - SENTRY_DSN=${SENTRY_DSN} + # The YAML anchor lets the prestart service reuse this environment mapping. + environment: &backend-environment + FRONTEND_HOST: ${FRONTEND_HOST?Variable not set} + ENVIRONMENT: ${ENVIRONMENT} + PROJECT_NAME: ${PROJECT_NAME?Variable not set} + BACKEND_CORS_ORIGINS: ${BACKEND_CORS_ORIGINS} + SECRET_KEY: ${SECRET_KEY?Variable not set} + FIRST_SUPERUSER: ${FIRST_SUPERUSER?Variable not set} + FIRST_SUPERUSER_PASSWORD: ${FIRST_SUPERUSER_PASSWORD?Variable not set} + SMTP_HOST: ${SMTP_HOST} + SMTP_USER: ${SMTP_USER} + SMTP_PASSWORD: ${SMTP_PASSWORD} + EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL} + SMTP_TLS: ${SMTP_TLS} + SMTP_SSL: ${SMTP_SSL} + SMTP_PORT: ${SMTP_PORT} + POSTGRES_SERVER: db + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER?Variable not set} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?Variable not set} + SENTRY_DSN: ${SENTRY_DSN} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"] @@ -129,5 +100,17 @@ services: - traefik.http.routers.backend-http.rule=Host(`${DOMAIN?Variable not set}`) - traefik.http.routers.backend-http.entrypoints=http + prestart: + image: backend:latest + build: + context: . + dockerfile: backend/Dockerfile + depends_on: + db: + condition: service_healthy + restart: true + command: bash scripts/prestart.sh + environment: *backend-environment + volumes: app-db-data: diff --git a/deployment.md b/deployment.md index 32bd71e4..1485c6ba 100644 --- a/deployment.md +++ b/deployment.md @@ -58,6 +58,8 @@ Set the `DOMAIN`, by default `localhost` (for development), but when deploying y export DOMAIN=fastapi-project.example.com ``` +The deployment Docker Compose configuration also uses `DOMAIN` to set the public frontend URL used in links generated by the backend. + Set the `POSTGRES_PASSWORD` to something different than `changethis`: ```bash @@ -72,18 +74,12 @@ export SECRET_KEY="changethis" Note: you can use the Python command above to generate a secure secret key. -Set the `FIRST_SUPER_USER_PASSWORD` to something different than `changethis`: +Set the `FIRST_SUPERUSER_PASSWORD` to something different than `changethis`: ```bash export FIRST_SUPERUSER_PASSWORD="changethis" ``` -Set the `FRONTEND_HOST` to the application URL. It is used to generate links in emails: - -```bash -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. From 66f444a63a11ce7b4b6df6c4fbe9e15b2fa7aa3a Mon Sep 17 00:00:00 2001 From: "latest-changes[bot]" <313495250+latest-changes[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:26:21 +0000 Subject: [PATCH 12/12] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index aa103ce3..55714781 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ ### Refactors +* ♻️ Simplify environment configuration. PR [#2413](https://github.com/fastapi/full-stack-fastapi-template/pull/2413) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify Traefik deployment, make it part of the stack. PR [#2412](https://github.com/fastapi/full-stack-fastapi-template/pull/2412) by [@tiangolo](https://github.com/tiangolo). * ♻️ Serve frontend from FastAPI. PR [#2393](https://github.com/fastapi/full-stack-fastapi-template/pull/2393) by [@tiangolo](https://github.com/tiangolo). * 🔧 Refactor PostgreSQL env var config. PR [#2392](https://github.com/fastapi/full-stack-fastapi-template/pull/2392) by [@tiangolo](https://github.com/tiangolo).