Compare commits

..

3 Commits

Author SHA1 Message Date
pr-submit[bot] ddb3c34f19 🔖 Release version 0.11.1 (#2437)
Co-authored-by: pr-submit[bot] <pr-submit[bot]@users.noreply.github.com>
2026-08-11 21:57:14 +02:00
latest-changes[bot] eae2d6701d 📝 Update release notes
[skip ci]
2026-08-11 19:35:48 +00:00
Sebastián Ramírez 6b9fa4113c ♻️ Simplify Docker Compose deployment (#2435) 2026-08-11 21:35:45 +02:00
13 changed files with 109 additions and 173 deletions
-37
View File
@@ -1,37 +0,0 @@
name: Deploy to Production
on:
release:
types:
- published
permissions:
contents: read
jobs:
deploy:
environment: production
# Do not deploy in the main repository, only in user projects
if: github.repository_owner != 'fastapi'
runs-on:
- self-hosted
- production
env:
DOMAIN: ${{ secrets.DOMAIN }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: docker compose -f compose.yml -f compose.deploy.yml build
- run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh
- run: docker compose -f compose.yml -f compose.deploy.yml up -d
@@ -1,21 +1,20 @@
name: Deploy to Staging name: Deploy
on: on:
push: push:
branches: branches:
- master - master
paths-ignore:
- release-notes.md
permissions: permissions:
contents: read contents: read
jobs: jobs:
deploy: deploy:
environment: staging
# Do not deploy in the main repository, only in user projects # Do not deploy in the main repository, only in user projects
if: github.repository_owner != 'fastapi' if: github.repository_owner != 'fastapi'
runs-on: runs-on: self-hosted
- self-hosted
- staging
env: env:
DOMAIN: ${{ secrets.DOMAIN }} DOMAIN: ${{ secrets.DOMAIN }}
SECRET_KEY: ${{ secrets.SECRET_KEY }} SECRET_KEY: ${{ secrets.SECRET_KEY }}
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
version: "0.11.18" version: "0.11.18"
- run: docker compose down -v --remove-orphans - run: docker compose down -v --remove-orphans
- run: docker compose up -d db mailpit - run: docker compose up -d db mailcatcher
- name: Migrate DB - name: Migrate DB
run: uv run bash scripts/prestart.sh run: uv run bash scripts/prestart.sh
working-directory: backend working-directory: backend
+1 -1
View File
@@ -21,7 +21,7 @@
- 🔒 Secure password hashing by default. - 🔒 Secure password hashing by default.
- 🔑 JWT (JSON Web Token) authentication. - 🔑 JWT (JSON Web Token) authentication.
- 📫 Email based password recovery. - 📫 Email based password recovery.
- 📬 [Mailpit](https://mailpit.axllent.org) for local email testing during development. - 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development.
- ✅ Tests with [Pytest](https://pytest.org). - ✅ Tests with [Pytest](https://pytest.org).
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer. - 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
- 🚢 Deployment instructions using Docker Compose with automatic HTTPS provided by Traefik. - 🚢 Deployment instructions using Docker Compose with automatic HTTPS provided by Traefik.
+2 -2
View File
@@ -9,10 +9,10 @@
Run the backend locally and connect it to PostgreSQL in Docker Compose. Run the backend locally and connect it to PostgreSQL in Docker Compose.
From the project root, start PostgreSQL and Mailpit: From the project root, start PostgreSQL and Mailcatcher:
```console ```console
$ docker compose up -d db mailpit $ docker compose up -d db mailcatcher
``` ```
Then, from `./backend/`, install the dependencies, prepare the database, and start the development server: Then, from `./backend/`, install the dependencies, prepare the database, and start the development server:
+6 -6
View File
@@ -64,14 +64,14 @@ services:
- ./backend/htmlcov:/app/backend/htmlcov - ./backend/htmlcov:/app/backend/htmlcov
environment: environment:
FASTAPI_ENV: "development" FASTAPI_ENV: "development"
SMTP_HOST: "mailpit" SMTP_HOST: "mailcatcher"
SMTP_PORT: "1025" SMTP_PORT: "1025"
SMTP_TLS: "false" SMTP_TLS: "false"
mailpit: mailcatcher:
image: axllent/mailpit image: schickling/mailcatcher
ports: ports:
- "8025:8025" - "1080:1080"
- "1025:1025" - "1025:1025"
playwright: playwright:
@@ -83,13 +83,13 @@ services:
ipc: host ipc: host
depends_on: depends_on:
- backend - backend
- mailpit - mailcatcher
environment: environment:
- FIRST_SUPERUSER=${FIRST_SUPERUSER:?Variable not set} - FIRST_SUPERUSER=${FIRST_SUPERUSER:?Variable not set}
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD:?Variable not set} - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD:?Variable not set}
- PLAYWRIGHT_BASE_URL=http://backend:8000 - PLAYWRIGHT_BASE_URL=http://backend:8000
- VITE_API_URL=http://backend:8000 - VITE_API_URL=http://backend:8000
- MAILPIT_HOST=http://mailpit:8025 - MAILCATCHER_HOST=http://mailcatcher:1080
# For the reports when run locally # For the reports when run locally
- PLAYWRIGHT_HTML_HOST=0.0.0.0 - PLAYWRIGHT_HTML_HOST=0.0.0.0
- CI=${CI:-} - CI=${CI:-}
+12 -48
View File
@@ -4,13 +4,13 @@ You can deploy the project using Docker Compose to a remote server.
The deployment Docker Compose configuration includes Traefik to handle HTTPS and route incoming traffic to the application. 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. The included GitHub Actions workflow can deploy the application automatically.
But you have to configure a couple things first. 🤓 But you have to configure a couple things first. 🤓
## Preparation ## Preparation
* Have a remote server ready and available. Use a separate server for each environment, for example one for staging and one for production. * 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` and `adminer.fastapi-project.example.com`. * 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). * Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
@@ -82,12 +82,15 @@ You can set several other environment variables:
* `EMAILS_FROM_EMAIL`: The email account to send emails from. * `EMAILS_FROM_EMAIL`: The email account to send emails from.
* `SENTRY_DSN`: The DSN for Sentry, if you are using it. * `SENTRY_DSN`: The DSN for Sentry, if you are using it.
## GitHub Actions Environment Variables ## GitHub Repository Automation
There are some environment variables only used by GitHub Actions that you can configure: Install the following GitHub Apps to enable the included repository automation:
* `LATEST_CHANGES`: Used by the GitHub Action [latest-changes](https://github.com/tiangolo/latest-changes) to automatically add release notes based on the PRs merged. It's a personal access token, read the docs for details. * [Latest Changes](https://github.com/apps/latest-changes) updates `release-notes.md` when a pull request is merged.
* `SMOKESHOW_AUTH_KEY`: Used to handle and publish the code coverage using [Smokeshow](https://github.com/samuelcolvin/smokeshow), follow their instructions to create a (free) Smokeshow key. * [PR Push](https://github.com/apps/pr-push) lets the pre-commit workflow push automated fixes to pull request branches.
* [PR Submit](https://github.com/apps/pr-submit) lets the **Bump pre-commit hooks** and **Prepare Release** workflows create pull requests.
To publish code coverage with [Smokeshow](https://github.com/samuelcolvin/smokeshow), add `SMOKESHOW_AUTH_KEY` as a repository secret.
### Deploy with Docker Compose ### Deploy with Docker Compose
@@ -106,7 +109,7 @@ The `compose.deploy.yml` file adds the deployment settings to the shared configu
You can use GitHub Actions to deploy your project automatically. 😎 You can use GitHub Actions to deploy your project automatically. 😎
There are already two environment deployments configured, `staging` and `production`. Each environment should be deployed to a separate server. 🚀 The included `deploy.yml` workflow deploys the application whenever changes are pushed to `master`, including when a pull request is merged. 🚀
### Install GitHub Actions Runner ### Install GitHub Actions Runner
@@ -136,8 +139,6 @@ cd
* [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository). * [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository).
* When asked about labels, add a label for the environment, e.g. `production`. You can also add labels later.
After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost. After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost.
To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user: To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user:
@@ -180,17 +181,9 @@ cd /home/github/actions-runner
You can read more about it in the official guide: [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service). You can read more about it in the official guide: [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service).
### Configure GitHub Environments ### Set Repository Secrets
The deployment workflows use [GitHub Environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments) for `staging` and `production`. This enables environment-specific secrets, deployment protection rules (e.g. required reviewers, wait timers), and deployment status tracking. In your repository, go to **Settings** > **Secrets and variables** > **Actions** and add the following [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository):
To configure them, go to your repository's **Settings** > **Environments** and create the `staging` and `production` environments.
### Set Secrets
For each GitHub Environment (`staging` and `production`), configure the required secrets as [environment secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-an-environment). Environment secrets are preferred over [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository) because they are scoped to the specific environment, reducing exposure and aligning with any protection rules you configure.
The deployment workflows require these secrets:
* `DOMAIN` * `DOMAIN`
* `FIRST_SUPERUSER` * `FIRST_SUPERUSER`
@@ -207,41 +200,12 @@ To enable emails, configure these additional secrets with the values from your e
To enable Sentry, configure the `SENTRY_DSN` secret. To enable Sentry, configure the `SENTRY_DSN` secret.
## GitHub Action Deployment Workflows
There are GitHub Action workflows in the `.github/workflows` directory already configured for deploying to the environments (GitHub Actions runners with the labels):
* `staging`: after pushing (or merging) to the branch `master`.
* `production`: after publishing a release.
Both workflows are associated with their respective GitHub Environments, so deployments will be visible in the repository's **Environments** section and will respect any protection rules you configure.
### Prepare a Release
Install the [PR Submit GitHub App](https://github.com/apps/pr-submit) in your repository to enable the **Prepare Release** workflow.
Run the workflow manually from the **Actions** tab and select the version bump. It creates a pull request that updates the release notes. When you merge that pull request, the **Create Draft Release** workflow creates a draft GitHub release with the corresponding release notes.
Review and publish the draft release to trigger the production deployment.
If you need to add extra environments you could use those as a starting point.
## URLs ## URLs
Replace `fastapi-project.example.com` with your domain. Replace `fastapi-project.example.com` with your domain.
### Production
Application (frontend and API): `https://fastapi-project.example.com` Application (frontend and API): `https://fastapi-project.example.com`
Interactive API docs: `https://fastapi-project.example.com/docs` Interactive API docs: `https://fastapi-project.example.com/docs`
Adminer: `https://adminer.fastapi-project.example.com` Adminer: `https://adminer.fastapi-project.example.com`
### Staging
Application (frontend and API): `https://staging.fastapi-project.example.com`
Interactive API docs: `https://staging.fastapi-project.example.com/docs`
Adminer: `https://adminer.staging.fastapi-project.example.com`
+8 -8
View File
@@ -2,12 +2,12 @@
## Local Development ## Local Development
For local development, run PostgreSQL and Mailpit with Docker Compose, and run the FastAPI and Vite development servers locally. For local development, run PostgreSQL and Mailcatcher with Docker Compose, and run the FastAPI and Vite development servers locally.
Start the supporting services: Start the supporting services:
```bash ```bash
docker compose up -d db mailpit docker compose up -d db mailcatcher
``` ```
Then, from the `backend` directory, install the dependencies and prepare the database: Then, from the `backend` directory, install the dependencies and prepare the database:
@@ -38,7 +38,7 @@ Backend API: <http://localhost:8000>
Automatic interactive API documentation with Swagger UI: <http://localhost:8000/docs> Automatic interactive API documentation with Swagger UI: <http://localhost:8000/docs>
Mailpit: <http://localhost:8025> Mailcatcher: <http://localhost:1080>
The frontend development server uses the backend at `http://localhost:8000`, as configured in `frontend/.env`. The frontend development server uses the backend at `http://localhost:8000`, as configured in `frontend/.env`.
@@ -71,15 +71,15 @@ Adminer, database web administration: <http://localhost:8080>
Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090> Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090>
Mailpit: <http://localhost:8025> Mailcatcher: <http://localhost:1080>
Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`. Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`.
**Note**: The first time you start the stack, it might take a minute for all the services to be ready. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service. **Note**: The first time you start the stack, it might take a minute for all the services to be ready. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service.
## Mailpit ## Mailcatcher
[Mailpit](https://mailpit.axllent.org) captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailpit` service. Captured emails are available at <http://localhost:8025>. Mailcatcher captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailcatcher` service. Captured emails are available at <http://localhost:1080>.
## Docker Compose files and env vars ## Docker Compose files and env vars
@@ -156,7 +156,7 @@ biome check..............................................................Passed
## URLs ## URLs
The production or staging URLs would use these same paths, but with your own domain. The deployed URLs use these same paths, but with your own domain.
### Development URLs ### Development URLs
@@ -172,4 +172,4 @@ Adminer: <http://localhost:8080>
Traefik UI: <http://localhost:8090> Traefik UI: <http://localhost:8090>
Mailpit: <http://localhost:8025> MailCatcher: <http://localhost:1080>
+1 -1
View File
@@ -1,2 +1,2 @@
VITE_API_URL=http://localhost:8000 VITE_API_URL=http://localhost:8000
MAILPIT_HOST=http://localhost:8025 MAILCATCHER_HOST=http://localhost:1080
+9 -5
View File
@@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test" import { expect, test } from "@playwright/test"
import { emailUrl, findLastEmail } from "./utils/mailpit" import { findLastEmail } from "./utils/mailcatcher"
import { randomEmail, randomPassword } from "./utils/random" import { randomEmail, randomPassword } from "./utils/random"
import { logInUser, signUpNewUser } from "./utils/user" import { logInUser, signUpNewUser } from "./utils/user"
@@ -46,11 +46,13 @@ test("User can reset password successfully using the link", async ({
const emailData = await findLastEmail({ const emailData = await findLastEmail({
request, request,
query: `to:${email}`, filter: (e) => e.recipients.includes(`<${email}>`),
timeout: 5000, timeout: 5000,
}) })
await page.goto(emailUrl(emailData)) await page.goto(
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`,
)
const selector = 'a[href*="/reset-password?token="]' const selector = 'a[href*="/reset-password?token="]'
@@ -97,11 +99,13 @@ test("Weak new password validation", async ({ page, request }) => {
const emailData = await findLastEmail({ const emailData = await findLastEmail({
request, request,
query: `to:${email}`, filter: (e) => e.recipients.includes(`<${email}>`),
timeout: 5000, timeout: 5000,
}) })
await page.goto(emailUrl(emailData)) await page.goto(
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`,
)
const selector = 'a[href*="/reset-password?token="]' const selector = 'a[href*="/reset-password?token="]'
const url = await page.getAttribute(selector, "href") const url = await page.getAttribute(selector, "href")
+62
View File
@@ -0,0 +1,62 @@
import type { APIRequestContext } from "@playwright/test"
type Email = {
id: number
recipients: string[]
subject: string
}
async function findEmail({
request,
filter,
}: {
request: APIRequestContext
filter?: (email: Email) => boolean
}) {
const response = await request.get(`${process.env.MAILCATCHER_HOST}/messages`)
let emails = await response.json()
if (filter) {
emails = emails.filter(filter)
}
const email = emails[emails.length - 1]
if (email) {
return email as Email
}
return null
}
export function findLastEmail({
request,
filter,
timeout = 5000,
}: {
request: APIRequestContext
filter?: (email: Email) => boolean
timeout?: number
}) {
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error("Timeout while trying to get latest email")),
timeout,
),
)
const checkEmails = async () => {
while (true) {
const emailData = await findEmail({ request, filter })
if (emailData) {
return emailData
}
// Wait for 100ms before checking again
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
return Promise.race([timeoutPromise, checkEmails()])
}
-59
View File
@@ -1,59 +0,0 @@
import type { APIRequestContext } from "@playwright/test"
type Address = {
Name: string
Address: string
}
type Email = {
ID: string
To: Address[]
Subject: string
}
async function findEmail({
request,
query,
}: {
request: APIRequestContext
query: string
}) {
const response = await request.get(
`${process.env.MAILPIT_HOST}/api/v1/search`,
{
params: { query, limit: 1 },
},
)
const { messages }: { messages: Email[] } = await response.json()
return messages[0] ?? null
}
export async function findLastEmail({
request,
query,
timeout = 5000,
}: {
request: APIRequestContext
query: string
timeout?: number
}) {
const deadline = Date.now() + timeout
while (Date.now() < deadline) {
const email = await findEmail({ request, query })
if (email) {
return email
}
await new Promise((resolve) => setTimeout(resolve, 100))
}
throw new Error(`Timeout while trying to get the latest email for "${query}"`)
}
export function emailUrl(email: Email) {
return `${process.env.MAILPIT_HOST}/view/${email.ID}.html`
}
+3
View File
@@ -2,8 +2,11 @@
## Latest Changes ## Latest Changes
## 0.11.1 (2026-08-11)
### Refactors ### Refactors
* ♻️ Simplify Docker Compose deployment. PR [#2435](https://github.com/fastapi/full-stack-fastapi-template/pull/2435) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91). * ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91).
### Internal ### Internal