Development¶
This section covers project-specific workflows and the Makefile interface.
Configuration¶
Initialize the project environment in the project root:
make setup-env
This generates the necessary configuration files from templates:
.env.docker: Base infrastructure settings.env.docker.dev: Local development overrides
Note
These files are excluded from version control for security reasons. Please update the created files with your local credentials or custom settings before running docker compose.
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.dev→/workspace/app/src/app/config/.env(container-specific config)
Note
Changes to mounted configurations (.env.docker.dev) require a container restart, while code changes in src/ are reflected automatically.
Startup & Migrations¶
Warning
The following commands automatically use your local docker-compose.override.yaml.
Ensure your configuration files are correctly initialized via make setup-env
before starting the stack.
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: setup-env
setup-env: ## Initialize environment files from templates
@echo "${INFO} Initializing environment files... ⚙️"
@if [ ! -f .env.docker ]; then cp .env.docker.template .env.docker && echo "${OK} .env.docker created"; else echo "${WARN} .env.docker already exists, skipping"; fi
@if [ ! -f .env.docker.dev ]; then cp .env.docker.dev.template .env.docker.dev && echo "${OK} .env.docker.dev created"; else echo "${WARN} .env.docker.dev already exists, skipping"; fi
@echo "${OK} Done! Update your .env files with your specific values"
.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!"