A full-stack local Python web application template with FastAPI, Jinja2, htmx, Tailwind CSS, SQLite, and Google OAuth.
| Layer | Technology |
|---|---|
| Framework | FastAPI (async) |
| Templates | Jinja2 + htmx |
| Styling | Tailwind CSS |
| Database | SQLite (via SQLModel + SQLAlchemy) |
| Migrations | Alembic |
| Auth | Google OAuth (authlib) |
| Validation | Pydantic v2 |
| Storage | Local filesystem |
| Logging | Pydantic Logfire |
| Package manager | uv |
| Deployment | Docker (optional) |
# 1. Install dependencies
uv sync
# 2. Configure environment
uv run python scripts/setup.py # interactive setup
# or: cp .env.example .env && edit .env
# 3. Run database migrations
uv run alembic upgrade head
# 4. Start the development server
make dev
# -> http://localhost:8000
# -> http://localhost:8000/docs (API docs)app/
├── main.py # App entry point, middleware, router mounts
├── config.py # Settings (reads .env)
├── db/
│ ├── database.py # DB engine and session dependency
│ └── schema.py # SQLModel table definitions
├── lib/
│ ├── auth.py # Session management, OAuth user creation
│ ├── errors.py # Standardised API response helpers
│ ├── oauth.py # authlib Google OAuth client
│ └── storage.py # Local file storage
├── middleware/
│ ├── auth.py # Auth FastAPI dependencies
│ ├── logging.py # Request logging
│ └── security.py # CSP + security headers
├── pages/ # One file per page (HTML + API routes)
├── functions/ # Shared DB query functions
└── templates/ # Jinja2 HTML templates
alembic/ # Database migrations
Dockerfile # Production container image
scripts/
└── setup.py # Interactive setup
make dev # Start dev server with hot reload
make setup # Interactive first-time configuration
make db-generate # Generate a new migration (msg="description")
make db-migrate # Apply pending migrations
make db-downgrade # Roll back one migration
make typecheck # Run pyright type checker
make lint # Run ruff linter
make format # Run ruff formatter- Create
app/pages/my_page.py - Create
app/templates/my_page.html - Add
from app.pages import my_pageandapp.include_router(my_page.router)inapp/main.py
See AGENTS.md for detailed patterns and examples.
Copy .env.example to .env and fill in:
| Variable | Required | Default |
|---|---|---|
SECRET_KEY |
Yes | — |
APP_URL |
Yes | http://localhost:8000 |
DATABASE_URL |
No | sqlite:///./data.db |
GOOGLE_CLIENT_ID |
OAuth only | — |
GOOGLE_CLIENT_SECRET |
OAuth only | — |
DEBUG |
No | false |
- Go to Google Cloud Console
- Create an OAuth 2.0 Client ID (Web application)
- Add
http://localhost:8000/auth/callbackas an authorised redirect URI - Copy the client ID and secret into
.env
.github/workflows/deploy.yml builds the image on every push to master/main
and pushes it to the homelab registry as
192.168.0.165:5000/<repo-name>:latest (plus a :<git-sha> tag for
rollbacks). Override the registry with the repo variable REGISTRY_HOST.
This requires a self-hosted GitHub Actions runner on the homelab LAN — GitHub-hosted runners cannot reach the private registry. The runner host's docker daemon must trust the registry (all swarm nodes already do).
Deploys are automatic: any swarm service using this image with the
shepherd.enable=true label rolls to the new :latest within ~5 minutes of
the push. The first deploy of a project is done once in the m720q-swarm
repo (stack file + ./run --tags stacks); after that, pushes deploy
themselves.
The included image runs Alembic migrations before starting Uvicorn. Build it locally or reference this repository as the build context from your deployment configuration:
docker build -t python-app .The deployment environment must provide SECRET_KEY and the public APP_URL.
It should mount persistent storage at /data and configure:
DATABASE_URL=sqlite:////data/app.db
STORAGE_PATH=/data/uploadsSet GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET when using OAuth, and
configure ${APP_URL}/auth/callback as the Google OAuth redirect URI. Keep
DEBUG=false, terminate TLS outside the container, and back up /data.
SQLite deployments should use a single app container; use an external database
before scaling to multiple replicas.
uv sync --locked --no-dev
uv run alembic upgrade head
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000