Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ APP_NAME=FastAPI Template
APP_HOST=127.0.0.1
PORT=80
RELOAD=False

## Docker Compose settings ##
# Optional Compose isolation. Give each simultaneous stack a unique project name.
# COMPOSE_PROJECT_NAME=fastapi-template-dev
# Host-facing URLs and ports used by Docker Compose.
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/actions-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: GitHub workflow checks

on:
push:
paths:
- ".github/workflows/**/*.yml"
pull_request:
paths:
- ".github/workflows/**/*.yml"

permissions:
contents: read

jobs:
gitleaks:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read

steps:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

- name: Scan workflow changes for secrets
uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

actionlint:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Lint GitHub Actions workflows
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2
30 changes: 30 additions & 0 deletions .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Documentation

on:
push:
paths:
- "docs/**/*.md"
- ".markdownlint-cli2.yaml"
- ".github/workflows/documentation.yml"
pull_request:
paths:
- "docs/**/*.md"
- ".markdownlint-cli2.yaml"
- ".github/workflows/documentation.yml"

permissions:
contents: read

jobs:
markdownlint:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Lint documentation
uses: DavidAnson/markdownlint-cli2-action@8de2aa07cae85fd17c0b35642db70cf5495f1d25 # v24.0.0
with:
config: .markdownlint-cli2.yaml
globs: "docs/**/*.md"
4 changes: 2 additions & 2 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
- "sonar-project.properties"
- ".sonarcloud.properties"
- ".github/dependabot.yml"
- ".github/workflows/**.yml"
- ".github/workflows/workflow.yml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep workflow edits under the Actions lint gate

When a PR changes only another workflow such as .github/workflows/documentation.yml, this narrowed path filter prevents the CI/CD workflow's existing actionlint job from starting; the new documentation workflow only runs markdownlint, so future edits to that workflow no longer get the repository's Actions lint check. Keep the broader workflow glob here, or add an actionlint job to the documentation workflow for workflow-file changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a dedicated GitHub Actions lint workflow that covers every workflow file change while keeping application CI scoped.

pull_request:
paths:
- "**.py"
Expand All @@ -34,7 +34,7 @@ on:
- "sonar-project.properties"
- ".sonarcloud.properties"
- ".github/dependabot.yml"
- ".github/workflows/**.yml"
- ".github/workflows/workflow.yml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep actionlint on workflow-file PRs

When a PR changes .github/workflows/documentation.yml, this narrowed path filter prevents the CI/CD workflow's actionlint job from running; I checked the new documentation workflow and it only runs markdownlint, so syntax or GitHub Actions expression errors in that workflow can be merged without the repository's workflow lint gate that previously covered .github/workflows/**.yml.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a dedicated GitHub Actions lint workflow that runs whenever any workflow file changes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep CI gates on workflow-only changes

When a PR changes only one of the new workflow files, such as .github/workflows/documentation.yml or .github/workflows/actions-lint.yml, this path filter no longer starts the CI/CD workflow unless workflow.yml is also touched. The replacement actions-lint workflow only runs actionlint, so the existing Gitleaks and other validation gates in this workflow are skipped for workflow-only PRs; include all workflow files here or move those gates to a workflow that still watches them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added secret scanning to the dedicated workflow checks. Workflow-only changes now run both Gitleaks and actionlint without triggering unrelated application tests.


env:
REGISTRY: ghcr.io
Expand Down
6 changes: 6 additions & 0 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
config:
default: true
MD013: false
MD033:
allowed_elements:
- br
112 changes: 112 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Environment Variables

`.env.example` is the reference for the root `.env` file. Replace any applicable `<placeholder>` value before use.

**Required by** describes when a value must be supplied. Compose injects several application values itself, so host-run and Compose requirements differ.

## Application

| Key | Required by | Meaning |
| ---------- | ----------- | -------------------------------------------------------------- |
| `APP_NAME` | Optional | Name shown in FastAPI metadata. |
| `APP_HOST` | Optional | Host interface for a host-run API. Default: `127.0.0.1`. |
| `PORT` | Optional | API listening port. Default: `80`. |
| `RELOAD` | Optional | Enables Uvicorn reload for host development. Default: `False`. |

## Compose

| Key | Required by | Meaning |
| ------------------------- | ----------- | -------------------------------------------------------------------------------------- |
| `COMPOSE_PROJECT_NAME` | Optional | Isolates Compose containers, networks, volumes, and default image names. |
| `APP_PORT` | Optional | Host port mapped to the API. Default: `80`. |
| `APP_PUBLIC_URL` | Optional | Browser-visible API origin used for the Swagger callback. Default: `http://localhost`. |
| `KEYCLOAK_PORT` | Optional | Host port mapped to Keycloak. Default: `8080`. |
| `OIDC_PUBLIC_URL` | Optional | Browser-visible Keycloak origin and token issuer. Default: `http://localhost:8080`. |
| `APP_IMAGE` | Optional | Local API image name. |
| `MIGRATION_IMAGE` | Optional | Local migration image name. |
| `POSTGRES_PASSWORD` | **Compose** | Password for the local PostgreSQL service. |
| `KEYCLOAK_ADMIN_PASSWORD` | **Compose** | Password for the local `admin` account and configuration service. |

A minimal Compose `.env` needs only:

```dotenv
POSTGRES_PASSWORD=local-postgres
KEYCLOAK_ADMIN_PASSWORD=local-keycloak
```

## Database

| Key | Required by | Meaning |
| ------------------------ | ------------ | ----------------------------------------------------------------------- |
| `DATABASE_URL` | **Host run** | PostgreSQL connection URL. Compose creates its own internal URL. |
| `DATABASE_POOL_SIZE` | Optional | Number of persistent connections in the host-run pool. Default: `5`. |
| `DATABASE_MAX_OVERFLOW` | Optional | Extra temporary connections allowed above the pool size. Default: `10`. |
| `DATABASE_ECHO` | Optional | Logs generated SQL when enabled. Default: `False`. |
| `DATABASE_POOL_PRE_PING` | Optional | Checks pooled connections before reuse. Default: `True`. |

## OIDC

| Key | Required by | Meaning |
| ----------------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| `OIDC_ISSUER_URL` | **Host run** | Public realm URL used for discovery and issuer validation. |
| `OIDC_INTERNAL_URL` | Optional | Internal URL used to fetch discovery metadata and signing keys while retaining the public issuer. |
| `OIDC_CLIENT_ID` | **Host run** | Audience expected in API access tokens. |
| `OIDC_DOCS_CLIENT_ID` | **Host run** | Public PKCE client used by Swagger UI. |
| `OIDC_CLIENT_SECRET` | Scenario tests only | Confidential client credential; the main API and Swagger UI do not use it. |
| `OIDC_JWKS_CACHE_TTL_SECONDS` | Optional | Signing-key cache duration in seconds. Default: `300`. |

Compose supplies the issuer and client IDs for its local realm.

## OIDC discovery overrides

These keys are optional as a group. If one is set, all four must be set; partial overrides are rejected at startup.

| Key | Meaning |
| ----------------------------- | -------------------------------------------------- |
| `OIDC_JWKS_URI` | Signing-key endpoint used by the API. |
| `OIDC_ISSUER` | Exact issuer expected in tokens. |
| `OIDC_AUTHORIZATION_ENDPOINT` | Browser authorization endpoint used by Swagger UI. |
| `OIDC_TOKEN_ENDPOINT` | Token endpoint used by Swagger UI. |

The overrides bypass network discovery. They are mainly useful for tests, fixed infrastructure, or air-gapped environments.

## Copy-ready local `.env`

This example works with the default Docker Compose stack. The values are local-development credentials and must not be reused in production.

```dotenv
## Application settings ##
APP_NAME=FastAPI Template
APP_HOST=127.0.0.1
PORT=80
RELOAD=False

## Docker Compose settings ##
COMPOSE_PROJECT_NAME=fastapi-template-dev
APP_PORT=80
APP_PUBLIC_URL=http://localhost
KEYCLOAK_PORT=8080
OIDC_PUBLIC_URL=http://localhost:8080
APP_IMAGE=fastapi-template:local
MIGRATION_IMAGE=fastapi-template-migration:local

POSTGRES_PASSWORD=local-postgres
KEYCLOAK_ADMIN_PASSWORD=local-keycloak-admin

## Database settings ##
# Used by a host-run API; Compose injects its own internal database URL.
DATABASE_URL=postgresql+psycopg://postgres:local-postgres@localhost:5432/fastapi_db
DATABASE_POOL_SIZE=5
DATABASE_MAX_OVERFLOW=10
DATABASE_ECHO=False
DATABASE_POOL_PRE_PING=True

## OIDC / Authentication settings ##
OIDC_ISSUER_URL=http://localhost:8080/realms/fastapi-realm
OIDC_CLIENT_ID=fastapi-client
OIDC_DOCS_CLIENT_ID=fastapi-docs
OIDC_CLIENT_SECRET=change-me
OIDC_JWKS_CACHE_TTL_SECONDS=300
```

Save the copied block as `.env` in the repository root. The four discovery override keys can remain unset for the local stack.
60 changes: 60 additions & 0 deletions docs/local-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Local Development

The project supports a host-run application and a complete Docker Compose stack. Keycloak details are documented separately in [Local Keycloak](local-keycloak.md). Environment keys are listed in [Environment Variables](environment-variables.md).

## Host-run application

`make run` applies Alembic migrations and then starts FastAPI using `.env`:

```text
Alembic migration → FastAPI process
```

PostgreSQL and the configured OIDC provider must already be reachable. Reloading is controlled by `RELOAD`; it is disabled in `.env.example`.

## Compose stack

The local stack has five services. Solid arrows are startup gates; dotted arrows are runtime connections:

```mermaid
flowchart LR
db["db<br/>PostgreSQL"] -->|healthy| migrate["migrate<br/>apply schema, then exit"]
migrate -->|completed| app["app<br/>FastAPI API"]

keycloak["keycloak<br/>local identity"] -->|healthy| config["keycloak-config<br/>set Swagger callback, then exit"]
config -->|completed| app

db -.->|SQL| app
keycloak -.->|OIDC and keys| app
```

- `db` is PostgreSQL and stores data in a named volume.
- `migrate` applies Alembic migrations and exits.
- `keycloak-config` adjusts the local browser callback and exits.
- `app` starts only after the database, migration, and identity services are ready.

`POSTGRES_PASSWORD` and `KEYCLOAK_ADMIN_PASSWORD` are required Compose values. The application database and OIDC addresses are supplied by Compose, so their container-facing hostnames differ from host-run values.

The application source is copied into its image rather than mounted. Code changes therefore require an image rebuild. This makes the local container match the image tested by CI.

## Images and state

The application and migrations use separate images. The application image contains runtime code; the migration image contains Alembic and its migration-only dependencies. This keeps schema changes explicit instead of running them during application startup.

PostgreSQL data survives a normal `docker compose down`. Removing volumes resets it:

```bash
docker compose down -v
```

## Isolated stacks

`COMPOSE_PROJECT_NAME` separates container names, networks, volumes, and default image tags. Parallel stacks must also use distinct host ports and public URLs:

```text
APP_PORT, APP_PUBLIC_URL, KEYCLOAK_PORT, OIDC_PUBLIC_URL
```

`APP_PUBLIC_URL` and `OIDC_PUBLIC_URL` are externally visible addresses. Compose service names such as `db` and `keycloak` are internal addresses and do not change with host ports.

The Compose stack is development infrastructure. Production deployments use separately managed databases, identity services, networking, secrets, and migration jobs.
33 changes: 33 additions & 0 deletions docs/local-keycloak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Local Keycloak

The Compose stack runs Keycloak 26.7 in development mode. It is local test infrastructure, not a production Keycloak configuration.

## Realm

`docker-compose.yml` starts Keycloak with `start-dev --import-realm` and imports `.docker/realm-export.json` as `fastapi-realm`.

Keycloak has no persistent data volume. Recreating the container resets it to the committed realm export. This keeps fresh local stacks reproducible.

## Clients

| Client | Purpose | Configuration |
| ---------------- | ------------------ | ------------------------------------------------------------- |
| `fastapi-docs` | Swagger UI login | Public authorization-code client with PKCE; no browser secret |
| `fastapi-client` | API token audience | Confidential local client; the API validates its audience |

The `keycloak-config` one-shot service replaces the Swagger redirect URI and web origin with the current `APP_PUBLIC_URL`. Exact URLs are used because wildcard browser callbacks are unsafe and break isolated stacks on alternate ports.

The browser uses the host-facing issuer, such as `http://localhost:8080`. The API fetches Keycloak metadata and keys through the internal Compose address, `http://keycloak:8080`. Tokens still retain the public issuer.

## Local identities

- Keycloak administrator: `admin`; its password comes from `KEYCLOAK_ADMIN_PASSWORD`.
- Local user: `testuser` / `testuser@example.com`.

`testuser` has no initial password. The local browser flow accepts the known username, skips the absent password check, and immediately requires the user to create a password. Recreating Keycloak removes that password.

This password-claiming behavior is intentional local convenience. It must not be copied into a production realm.

## Scenario-test realm

`docker-compose.e2e.yml` replaces the normal import with `.docker/e2e-realm-export.json`. That realm is `fastapi-e2e-realm` and contains the fixed `e2e-user` identity used by automated scenario tests. Its credentials are test fixtures only.
Loading
Loading