Development¶
This section covers project-specific workflows and the Makefile interface.
Configuration¶
Initialize the application environment file:
cp .env.docker.template .env.docker
Hot-Reload & Volumes¶
In development mode (using docker-compose.override.yaml), the following local paths are mounted:
src/→/workspace/app/src/(enables hot-reload).env.docker→/workspace/app/src/app/config/.env(container-specific config)
Startup Sequence¶
The initial bootstrap requires a manual role synchronization for PgBouncer:
# 1. Start PostgreSQL
docker compose up -d postgres
# 2. Sync roles and passwords
make pgbouncer-sync
# 3. Start the rest of the stack
docker compose up -d
Database Migrations¶
To apply migrations manually:
docker compose run --rm migrator
Networking & IPC¶
The stack uses Unix Domain Sockets (UDS) for inter-container communication between Angie and Granian.
Socket Path: Shared via a Docker volume at
/run/app/granian.sock.Permissions: Granian is configured with
--uds-permissions 438(octal666) to allow Angie to read/write to the socket.Healthchecks: The application health is verified by a Python script performing a raw HTTP/1.1 request directly over the Unix socket.
Note
The API does not listen on a TCP port (e.g., 8000). All requests must be routed through the Angie proxy.
Production Tuning¶
Host System Configuration¶
For optimal HTTP/3 (QUIC) performance, you must increase the UDP receive and send buffer sizes on the host machine.
- 1. Create a dedicated configuration file
To maintain a clean system, avoid modifying the main
sysctl.conf. Create a separate file for Angie:sudo nano /etc/sysctl.d/99-angie-quic.conf- 2. Add network buffer parameters
Paste the following lines into the file:
net.core.rmem_max=2500000 net.core.wmem_max=2500000
- 3. Apply changes
Load the new configuration immediately without a reboot:
sudo sysctl --system
SSL & Certificates¶
Development (Local HTTPS)¶
Use mkcert to create a locally-trusted development certificate.
1. Install local CA (once per machine)
mkcert -install
2. Generate certificates
mkdir -p deploy/certs
mkcert -cert-file deploy/certs/local-cert.pem \
-key-file deploy/certs/local-key.pem \
app.localhost localhost 127.0.0.1 ::1
Production¶
Use Certbot on the host. Since Angie mounts deploy/certs as read-only (:ro), reload the service after certificate renewal:
docker exec angie_iron_track angie -s reload
Makefile Reference¶
Command |
Description |
|---|---|
|
Resets the environment and installs fresh dependencies. |
|
Syncs Postgres roles to PgBouncer |
|
Updates |
|
Standard quality assurance commands. |
View Full Makefile
SHELL := /bin/bash
# =============================================================================
# Variables
# =============================================================================
.DEFAULT_GOAL:=help
.ONESHELL:
.EXPORT_ALL_VARIABLES:
MAKEFLAGS += --no-print-directory
# Define colors and formatting
BLUE := $(shell printf "\033[1;34m")
GREEN := $(shell printf "\033[1;32m")
RED := $(shell printf "\033[1;31m")
YELLOW := $(shell printf "\033[1;33m")
NC := $(shell printf "\033[0m")
INFO := $(shell printf "$(BLUE)ℹ$(NC)")
OK := $(shell printf "$(GREEN)✓$(NC)")
WARN := $(shell printf "$(YELLOW)⚠$(NC)")
ERROR := $(shell printf "$(RED)✖$(NC)")
# Define configuration
COMPOSE_INFRA_FILE := deploy/docker-compose.infra.yaml
COMPOSE_INFRA := docker compose -f $(COMPOSE_INFRA_FILE)
POSTGRES_CONTAINER := postgres_V18_iron_track
PGBOUNCER_USERLIST := deploy/pgbouncer/conf/userlist.txt
SYNC_USERS ?= 'alfacat', 'admin', 'monitor'
##@ General
.PHONY: help
help: ## Display this help text
@echo ""
@echo " Usage: make $(BLUE)<target>$(NC)"
@echo ""
@awk 'BEGIN {FS = ":.*##"} \
/^[a-zA-Z0-9_-]+:.*?##/ { printf " $(BLUE)%-20s$(NC) %s\n", $$1, $$2 } \
/^##@/ { printf "\n$(NC)\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
@echo ""
# =============================================================================
# Development
# =============================================================================
##@ Development
.PHONY: install-uv
install-uv: ## Install latest version of uv
@if command -v uv >/dev/null 2>&1; then \
echo "${OK} uv is already installed"; \
else \
echo "${INFO} Installing uv..."; \
curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1; \
echo "${OK} uv installed successfully"; \
fi
.PHONY: install
install: destroy clean ## Install project dependencies and dev packages
@echo "${INFO} Starting fresh installation (Python 3.12)..."
@uv python pin 3.12 >/dev/null 2>&1
@uv venv >/dev/null 2>&1
@uv sync --all-extras --dev
@echo "${OK} Installation complete! 🎉"
.PHONY: lock
lock: ## Rebuild lockfiles from scratch
@echo "${INFO} Rebuilding lockfiles... 🔄"
@uv lock --upgrade >/dev/null 2>&1
@echo "${OK} Lockfiles updated"
.PHONY: upgrade
upgrade: ## Upgrade all dependencies to the latest stable versions
@echo "${INFO} Updating all dependencies... 🔄"
@uv lock --upgrade
@echo "${OK} Dependencies updated 🔄"
@echo "${INFO} Updating pre-commit hooks..."
@uv run pre-commit autoupdate
@echo "${OK} Updated Pre-commit hooks 🔄"
.PHONY: clean
clean: ## Cleanup temporary build artifacts and caches
@echo "${INFO} Cleaning working directory..."
@rm -rf build/ dist/ .eggs/ .pytest_cache .ruff_cache .mypy_cache .coverage coverage.xml htmlcov/ .hypothesis >/dev/null 2>&1
@find . -name '*.egg-info' -exec rm -rf {} + >/dev/null 2>&1
@find . -type f -name '*.py[co]' -delete >/dev/null 2>&1
@find . -name '__pycache__' -exec rm -rf {} + >/dev/null 2>&1
@find . -name '*~' -exec rm -f {} + >/dev/null 2>&1
@echo "${OK} Working directory cleaned"
@$(MAKE) docs-clean
.PHONY: destroy
destroy: ## Destroy the virtual environment
@echo "${INFO} Destroying virtual environment... 🗑️"
@rm -rf .venv
@echo "${OK} Virtual environment destroyed 🗑️"
.PHONY: release
release: ## Bump version and create tag (usage: make release bump=[major|minor|patch])
@if [ -z "$(bump)" ]; then \
echo "${ERROR} Argument 'bump' is missing! Use: make release bump=patch"; \
exit 1; \
fi
@echo "${INFO} Starting release process ($(bump))... 📦"
@uv run bump-my-version bump $(bump)
@echo "${OK} Version bumped and tag created 🎉"
# =============================================================================
# Quality Control
# =============================================================================
##@ Quality Control
.PHONY: mypy
mypy: ## Run static type checking with mypy
@echo "${INFO} Running mypy... 🔍"
@uv run dmypy run src/app
@echo "${OK} Mypy checks passed ✨"
.PHONY: pre-commit
pre-commit: ## Run all pre-commit hooks (ruff, codespell, etc.)
@echo "${INFO} Running pre-commit checks... 🔎"
@uv run pre-commit run --color=always --all-files
@echo "${OK} Pre-commit checks passed ✨"
.PHONY: fix
fix: ## Auto-fix linting issues and format code
@echo "${INFO} Running code formatters... 🔧"
@uv run ruff check --fix --unsafe-fixes
@uv run ruff format
@echo "${OK} Code formatting complete ✨"
.PHONY: lint
lint: pre-commit mypy ## Run all linting and type checking
@echo "${OK} All linting checks passed ✨"
.PHONY: test
test: ## Run tests in parallel (2 workers)
@echo "${INFO} Running test cases... 🧪"
@uv run pytest tests -n 2 --quiet
@echo "${OK} Tests passed ✨"
.PHONY: coverage
coverage: ## Run tests and generate coverage reports (HTML/XML)
@echo "${INFO} Running tests with coverage... 📊"
@uv run pytest tests --cov -n auto --quiet
@uv run coverage html >/dev/null 2>&1
@uv run coverage xml >/dev/null 2>&1
@echo "${OK} Coverage report generated (htmlcov/index.html) ✨"
.PHONY: check-all
check-all: lint test coverage ## Run everything: linting, tests, and coverage
# =============================================================================
# Documentation
# =============================================================================
##@ Documentation
.PHONY: docs-clean
docs-clean: ## Dump the existing built docs
@echo "${INFO} Cleaning documentation build assets... 🧹"
@rm -rf docs/_build >/dev/null 2>&1
@echo "${OK} Documentation assets cleaned"
.PHONY: docs-serve
docs-serve: docs-clean ## Serve the docs locally with live-reload
@echo "${INFO} Starting documentation server... 📚"
@uv run sphinx-autobuild docs docs/_build/ -j auto --host 0.0.0.0 --port 8002 \
--watch src/app --watch docs --watch tests --watch CONTRIBUTING.rst
.PHONY: docs
docs: docs-clean ## Build the HTML documentation
@echo "${INFO} Building documentation... 📝"
@uv run sphinx-build -M html docs docs/_build/ -E -a -j auto -W --keep-going
@echo "${OK} Documentation built successfully"
.PHONY: docs-linkcheck
docs-linkcheck: ## Run internal link check on the docs
@echo "${INFO} Checking documentation links... 🔗"
@uv run sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_ignore='http://.*','https://.*' >/dev/null 2>&1
@echo "${OK} Link check complete"
.PHONY: docs-linkcheck-full
docs-linkcheck-full: ## Run full link check (including external URLs)
@echo "${INFO} Running full link check... 🔗"
@uv run sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_anchors=0 >/dev/null 2>&1
@echo "${OK} Full link check complete"
# =============================================================================
# Local Infrastructure
# =============================================================================
##@ Infrastructure
.PHONY: infra-up
infra-up: ## Start local infrastructure containers
@echo "${INFO} Starting local infrastructure... 🚀"
@$(COMPOSE_INFRA) up -d --force-recreate
@echo "${OK} Infrastructure is ready"
.PHONY: infra-down
infra-down: ## Stop local infrastructure containers
@echo "${INFO} Stopping infrastructure... 🛑"
@$(COMPOSE_INFRA) down
@echo "${OK} Infrastructure stopped"
.PHONY: infra-wipe
infra-wipe: ## Remove local containers, volumes and orphans
@echo "${INFO} Wiping infrastructure... 🧹"
@$(COMPOSE_INFRA) down -v --remove-orphans
@echo "${OK} Infrastructure wiped clean"
.PHONY: infra-logs
infra-logs: ## Tail infrastructure logs
@echo "${INFO} Tailing infrastructure logs... 📋"
@$(COMPOSE_INFRA) logs -f
# =============================================================================
# Maintenance
# =============================================================================
##@ Maintenance
.PHONY: seed
seed: ## Populate database with initial data
@echo "${INFO} Seeding database... 🌱"
@uv run python -m src.app.scripts.seeder
@echo "${OK} Seeding complete"
.PHONY: pgbouncer-sync
pgbouncer-sync: ## Sync Postgres roles to PgBouncer userlist (usage: make pgbouncer-sync SYNC_USERS="'user1','user2'")
@echo "${INFO} Checking if PostgreSQL container is healthy... 🔍"
@STATUS=$$(docker inspect --format='{{.State.Health.Status}}' $(POSTGRES_CONTAINER) 2>/dev/null); \
if [ "$$STATUS" != "healthy" ]; then \
echo "${ERROR} Container $(POSTGRES_CONTAINER) is not ready (Status: $$STATUS)"; \
exit 1; \
fi
@echo "${INFO} Syncing users: $(SYNC_USERS) -> $(PGBOUNCER_USERLIST)"
@mkdir -p $$(dirname $(PGBOUNCER_USERLIST))
@rm -f $(PGBOUNCER_USERLIST)
@docker exec -i $(POSTGRES_CONTAINER) psql -U postgres -d postgres -t -q -A -c \
"SELECT '\"' || rolname || '\" \"' || rolpassword || '\"' FROM pg_authid WHERE rolname IN ($(SYNC_USERS));" \
>> $(PGBOUNCER_USERLIST)
@chmod 644 $(PGBOUNCER_USERLIST)
@echo "${OK} Sync complete! ✨"
.PHONY: gen-key
gen-key: ## Generate a new Ed25519 JWK for JWT_PRIVATE_KEY
@echo "${INFO} Generating new Ed25519 key pair... 🔑"
@uv run python -c "import json; from joserfc.jwk import OKPKey; print(json.dumps(OKPKey.generate_key('Ed25519').as_dict(private=True)))"
@echo "${OK} Generation complete!"