Skip to content

Congressional Districts API - Systemd Deployment Guide

This guide covers deploying the Congressional Districts API as a systemd service.

Overview

Service Port Description
cbdistricts-api 32406 FastAPI data API
cbdistricts-web 32405 FastHTML web frontend

Prerequisites

  • Python 3.12+ via pyenv (~/.pyenv/versions/nominates)
  • DuckDB database at data/output/cbdistricts.duckdb
  • Project installed at /home/bisenbek/projects/nominate/cbdistricts

Service Files

API Service (/etc/systemd/system/cbdistricts-api.service)

[Unit]
Description=Congressional Districts API Server
After=network.target

[Service]
Type=simple
User=bisenbek
Group=bisenbek
WorkingDirectory=/home/bisenbek/projects/nominate/cbdistricts
Environment="PATH=/home/bisenbek/.pyenv/versions/nominates/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/bisenbek/.pyenv/versions/nominates/bin/python -m api.main
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/bisenbek/projects/nominate/cbdistricts/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Web Service (/etc/systemd/system/cbdistricts-web.service)

[Unit]
Description=Congressional Districts Web Server
After=network.target

[Service]
Type=simple
User=bisenbek
Group=bisenbek
WorkingDirectory=/home/bisenbek/projects/nominate/cbdistricts
Environment="PATH=/home/bisenbek/.pyenv/versions/nominates/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/bisenbek/.pyenv/versions/nominates/bin/python web/server.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/bisenbek/projects/nominate/cbdistricts/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Installation

# Copy service files
sudo cp docs/cbdistricts-api.service /etc/systemd/system/
sudo cp docs/cbdistricts-web.service /etc/systemd/system/

# Reload systemd
sudo systemctl daemon-reload

# Enable services to start on boot
sudo systemctl enable cbdistricts-api
sudo systemctl enable cbdistricts-web

# Start services
sudo systemctl start cbdistricts-api
sudo systemctl start cbdistricts-web

Management Commands

# Check status
sudo systemctl status cbdistricts-api
sudo systemctl status cbdistricts-web

# Start/Stop/Restart
sudo systemctl start cbdistricts-api
sudo systemctl stop cbdistricts-api
sudo systemctl restart cbdistricts-api

# View logs (live)
sudo journalctl -u cbdistricts-api -f

# View logs (last 100 lines)
sudo journalctl -u cbdistricts-api -n 100

# View logs since boot
sudo journalctl -u cbdistricts-api -b

Health Checks

# API health
curl http://localhost:32406/api/v1/health

# Web server
curl -I http://localhost:32405/

Nginx Reverse Proxy (Optional)

If fronting with nginx at districts.nominate.ai:

# /etc/nginx/sites-available/districts.nominate.ai

upstream cbdistricts_web {
    server 127.0.0.1:32405;
}

upstream cbdistricts_api {
    server 127.0.0.1:32406;
}

server {
    listen 443 ssl http2;
    server_name districts.nominate.ai;

    ssl_certificate /etc/letsencrypt/live/districts.nominate.ai/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/districts.nominate.ai/privkey.pem;

    # Web frontend
    location / {
        proxy_pass http://cbdistricts_web;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # API endpoints
    location /api/ {
        proxy_pass http://cbdistricts_api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # API docs
    location /docs {
        proxy_pass http://cbdistricts_api;
        proxy_set_header Host $host;
    }

    location /redoc {
        proxy_pass http://cbdistricts_api;
        proxy_set_header Host $host;
    }

    location /openapi.json {
        proxy_pass http://cbdistricts_api;
        proxy_set_header Host $host;
    }
}

server {
    listen 80;
    server_name districts.nominate.ai;
    return 301 https://$server_name$request_uri;
}

Troubleshooting

Service fails to start

# Check detailed error
sudo journalctl -u cbdistricts-api -n 50 --no-pager

# Verify Python environment
/home/bisenbek/.pyenv/versions/nominates/bin/python -c "import api.main; print('OK')"

# Verify database exists
ls -la /home/bisenbek/projects/nominate/cbdistricts/data/output/cbdistricts.duckdb

Port already in use

# Find process using port
sudo lsof -i :32406
sudo lsof -i :32405

# Kill if needed
sudo kill -9 <PID>

Permission issues

# Ensure correct ownership
sudo chown -R bisenbek:bisenbek /home/bisenbek/projects/nominate/cbdistricts

# Verify pyenv is accessible
sudo -u bisenbek /home/bisenbek/.pyenv/versions/nominates/bin/python --version

Environment Variables

Optional environment variables (set in service file or /etc/environment):

Variable Default Description
API_HOST 0.0.0.0 API bind address
API_PORT 32406 API port
API_DEBUG false Debug mode
API_DB_MAX_CONNECTIONS 10 Max DB pool connections

Monitoring

Recommended monitoring endpoints:

  • Liveness: GET /api/v1/health - Returns 200 if service is running
  • Readiness: GET /api/v1/info - Returns 200 with DB stats if fully operational

Example Prometheus scrape config:

- job_name: 'cbdistricts-api'
  static_configs:
    - targets: ['localhost:32406']
  metrics_path: /api/v1/health