Prometheus: Expense Tracker API
I wanted one small API with the full production path around it: tests on every push, a versioned Docker image on every tag, metrics, and error tracking.
How it works
every tag is a deployable image
The code
FROM python:3.12-slim AS base
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
# Set environment variables for uv
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_CACHE_DIR=/opt/uv-cache
# Copy dependency files first for better layer caching
COPY pyproject.toml uv.lock ./
# Install dependencies (without project for faster rebuilds)
RUN uv sync --frozen --no-install-project
# Copy application code
COPY . .
# Install the project itself
RUN uv sync --frozen
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Expose port
EXPOSE 8000
# Run the application
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]show all 38 linesshow less
name: Release
on:
push:
tags: ['v*'] # triggers on v0.1.0 etc.
workflow_dispatch: # allow manual test runs
permissions:
contents: write
packages: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build-and-push:
# simple and template-friendly: run on any tag
if: ${{ startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-latest
env:
# optional; empty when not set, safe for 'if:' checks
SENTRY_DSN: ${{ secrets.SENTRY_DSN || '' }}
steps:
- uses: actions/checkout@v4
- name: Compute image name (lowercase)
id: img
run: |
REPO_LC=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
echo "image=ghcr.io/${REPO_LC}" >> "$GITHUB_OUTPUT"
- name: Log in to GHCR
run: echo "${{ github.token }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build
run: |
docker build \
-t ${{ steps.img.outputs.image }}:${{ github.ref_name }} \
-t ${{ steps.img.outputs.image }}:latest .
- name: Smoke test container health
run: |
cid=$(docker run -d -p 8000:8000 ${{ steps.img.outputs.image }}:latest)
for i in {1..30}; do curl -fsS http://localhost:8000/health && break || sleep 1; done
docker logs "$cid" --tail 50
docker rm -f "$cid"
- name: Push
run: |
docker push ${{ steps.img.outputs.image }}:${{ github.ref_name }}
docker push ${{ steps.img.outputs.image }}:latest
- name: Sentry release (optional)
if: ${{ env.SENTRY_DSN != '' }}
run: echo "Would run sentry-cli here for ${{ github.ref_name }}"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
body: |
See CHANGELOG.md for details.
Image:
- ${{ steps.img.outputs.image }}:latest
- ${{ steps.img.outputs.image }}:${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ github.token }}show all 69 linesshow less
import logging
from contextlib import asynccontextmanager
import sentry_sdk
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from app.api.routes import analytics, auth, categories, health, transactions
from app.config import settings
from app.core.database import init_database
from app.core.metrics import setup_metrics
# … cut: logging setup
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan events."""
# Startup
logger.info(f"Starting {settings.app_name} v{settings.app_version}")
# Initialize Sentry if DSN is provided
if settings.sentry_dsn:
sentry_sdk.init(
dsn=settings.sentry_dsn,
integrations=[
FastApiIntegration(auto_enabling_instrumentations=False),
LoggingIntegration(level=logging.INFO),
],
traces_sample_rate=0.1,
environment=settings.environment,
)
logger.info("Sentry initialized")
# Initialize database
await init_database()
logger.info("Application startup complete")
yield
# Shutdown
logger.info("Shutting down application")
# … cut: app creation, CORS and request logging
# Setup metrics
setup_metrics(app)
logger.info("Prometheus metrics enabled")
# … cut: routersshow all 54 linesshow less
Results
- CI, CodeQL and a release workflow on GitHub Actions
- Versioned images on GHCR
- Deploy blueprint for Render included