Skip to content

Deployment Guide

github-actions[bot] edited this page Dec 2, 2025 · 1 revision

Deployment Guide

Complete guide for deploying tail-lookup in various environments, from simple Docker containers to production Kubernetes clusters.

Quick Start

Docker (Simplest)

Pull and run the latest image:

docker run -d \
  --name tail-lookup \
  -p 8080:8080 \
  --restart unless-stopped \
  ryakel/tail-lookup:latest

Access at: http://localhost:8080

Docker Compose (Recommended)

Create docker-compose.yml:

version: "3.8"

services:
  tail-lookup:
    image: ryakel/tail-lookup:latest
    container_name: tail-lookup
    ports:
      - "8080:8080"
    restart: unless-stopped

Deploy:

docker compose up -d

Deployment Options

1. Local Docker Deployment

Use case: Local development, testing, personal use

Steps:

  1. Pull latest image:

    docker pull ryakel/tail-lookup:latest
  2. Run container:

    docker run -d \
      --name tail-lookup \
      -p 8080:8080 \
      --restart unless-stopped \
      ryakel/tail-lookup:latest
  3. Verify:

    curl http://localhost:8080/api/v1/health
  4. View logs:

    docker logs -f tail-lookup
  5. Stop/Start:

    docker stop tail-lookup
    docker start tail-lookup
  6. Update:

    docker pull ryakel/tail-lookup:latest
    docker stop tail-lookup
    docker rm tail-lookup
    docker run -d --name tail-lookup -p 8080:8080 --restart unless-stopped ryakel/tail-lookup:latest

Pros:

  • Simple, single command
  • No configuration needed
  • Fast startup

Cons:

  • Manual updates
  • No automatic restart on reboot (unless using --restart)

2. Docker Compose Deployment

Use case: Production single-host deployments, easier management

Steps:

  1. Create docker-compose.yml:

    version: "3.8"
    
    services:
      tail-lookup:
        image: ryakel/tail-lookup:latest
        container_name: tail-lookup
        ports:
          - "8080:8080"
        restart: unless-stopped
        healthcheck:
          test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8080/api/v1/health').raise_for_status()"]
          interval: 30s
          timeout: 5s
          retries: 3
          start_period: 5s
  2. Deploy:

    docker compose up -d
  3. View logs:

    docker compose logs -f
  4. Update:

    docker compose pull
    docker compose up -d
  5. Stop:

    docker compose down

Pros:

  • Declarative configuration
  • Easy updates with docker compose pull
  • Health check built-in
  • Simple scaling (if needed)

Cons:

  • Requires docker-compose.yml file
  • Still manual updates

3. Portainer Deployment

Use case: Production with web UI, automatic updates

Prerequisites:

Steps:

  1. In Portainer UI:

    • Navigate to "Stacks" β†’ "Add stack"
    • Name: tail-lookup
    • Web editor, paste:
    version: "3.8"
    
    services:
      tail-lookup:
        image: ryakel/tail-lookup:latest
        ports:
          - "8080:8080"
        restart: unless-stopped
    • Click "Deploy the stack"
  2. Enable webhook for automatic updates:

    • Navigate to stack β†’ "Webhook"
    • Toggle "Service webhook"
    • Copy webhook URL
    • Add to GitHub repository secrets as PORTAINER_WEBHOOK_URL
  3. Manual update:

    • Stack β†’ "Update" β†’ "Pull and redeploy"

Pros:

  • Web UI for management
  • Automatic updates via webhook
  • Easy rollback
  • Multi-host support (Portainer Business)

Cons:

  • Requires Portainer installation
  • Additional complexity

4. Watchtower Auto-Update

Use case: Automatic updates without webhooks

Steps:

  1. Create docker-compose.yml with Watchtower:

    version: "3.8"
    
    services:
      tail-lookup:
        image: ryakel/tail-lookup:latest
        container_name: tail-lookup
        ports:
          - "8080:8080"
        restart: unless-stopped
    
      watchtower:
        image: containrrr/watchtower
        container_name: watchtower
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
        command: --interval 3600 tail-lookup
        restart: unless-stopped
  2. Deploy:

    docker compose up -d

How it works:

  • Watchtower checks for image updates every hour
  • Automatically pulls new image if available
  • Gracefully restarts tail-lookup container
  • Zero manual intervention

Pros:

  • Fully automatic updates
  • No webhook configuration needed
  • Works with any Docker deployment

Cons:

  • Additional container running
  • Polls Docker Hub (might hit rate limits)
  • Updates happen automatically (no approval)

5. Kubernetes Deployment

Use case: Large-scale, multi-instance deployments

Prerequisites:

  • Kubernetes cluster (minikube, EKS, GKE, AKS, etc.)
  • kubectl configured

Deployment Manifest (tail-lookup-deployment.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tail-lookup
  labels:
    app: tail-lookup
spec:
  replicas: 3
  selector:
    matchLabels:
      app: tail-lookup
  template:
    metadata:
      labels:
        app: tail-lookup
    spec:
      containers:
      - name: tail-lookup
        image: ryakel/tail-lookup:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
          name: http
        livenessProbe:
          httpGet:
            path: /api/v1/health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /api/v1/health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: tail-lookup
spec:
  type: LoadBalancer
  selector:
    app: tail-lookup
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http

Deploy:

kubectl apply -f tail-lookup-deployment.yaml

Get service URL:

kubectl get svc tail-lookup

Update:

kubectl rollout restart deployment/tail-lookup

Scale:

kubectl scale deployment/tail-lookup --replicas=5

Pros:

  • High availability (multiple replicas)
  • Automatic load balancing
  • Rolling updates
  • Self-healing
  • Horizontal scaling

Cons:

  • Complex setup
  • Requires Kubernetes knowledge
  • Overkill for small deployments

6. Cloud Platform Deployments

AWS ECS (Elastic Container Service)

Task Definition:

{
  "family": "tail-lookup",
  "containerDefinitions": [
    {
      "name": "tail-lookup",
      "image": "ryakel/tail-lookup:latest",
      "memory": 256,
      "cpu": 256,
      "essential": true,
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp"
        }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 10
      }
    }
  ]
}

Deploy via:

  • AWS Console β†’ ECS β†’ Create Service
  • AWS CLI: aws ecs create-service ...
  • Terraform/CloudFormation for IaC

Google Cloud Run

gcloud run deploy tail-lookup \
  --image ryakel/tail-lookup:latest \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --port 8080

Pros: Fully managed, auto-scaling, pay-per-use

Azure Container Instances

az container create \
  --resource-group myResourceGroup \
  --name tail-lookup \
  --image ryakel/tail-lookup:latest \
  --ports 8080 \
  --dns-name-label tail-lookup-aci \
  --restart-policy Always

Reverse Proxy Configuration

Nginx

Use case: TLS termination, custom domain, multiple services

nginx.conf:

server {
    listen 80;
    server_name tail-lookup.example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name tail-lookup.example.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    location / {
        proxy_pass http://localhost:8080;
        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;
    }

    # Optional: serve static files directly
    location /static/ {
        proxy_pass http://localhost:8080/static/;
        expires 1d;
        add_header Cache-Control "public, immutable";
    }
}

Traefik

docker-compose.yml with Traefik:

version: "3.8"

services:
  tail-lookup:
    image: ryakel/tail-lookup:latest
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.tail-lookup.rule=Host(`tail-lookup.example.com`)"
      - "traefik.http.routers.tail-lookup.entrypoints=websecure"
      - "traefik.http.routers.tail-lookup.tls.certresolver=letsencrypt"
      - "traefik.http.services.tail-lookup.loadbalancer.server.port=8080"
    networks:
      - traefik

networks:
  traefik:
    external: true

Caddy

Caddyfile:

tail-lookup.example.com {
    reverse_proxy localhost:8080
}

Environment Configuration

Port Mapping

Default internal port: 8080

Map to different external port:

docker run -p 3000:8080 ryakel/tail-lookup:latest  # Access on port 3000

Database Path (Custom Build)

If building custom image with different database:

FROM ryakel/tail-lookup:latest
COPY my-custom.db /app/data/aircraft.db

No environment variable needed; path is hardcoded in Dockerfile.


Health Checks

Docker Health Check

Built into Dockerfile:

HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
    CMD python -c "import httpx; httpx.get('http://localhost:8080/api/v1/health').raise_for_status()"

Check container health:

docker inspect --format='{{.State.Health.Status}}' tail-lookup

Kubernetes Probes

Liveness probe:

livenessProbe:
  httpGet:
    path: /api/v1/health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 30

Readiness probe:

readinessProbe:
  httpGet:
    path: /api/v1/health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

External Monitoring

UptimeRobot:

  • Monitor: https://tail-lookup.example.com/api/v1/health
  • Interval: 5 minutes
  • Alert on: Status != 200

Prometheus:

scrape_configs:
  - job_name: 'tail-lookup'
    metrics_path: '/api/v1/health'
    static_configs:
      - targets: ['localhost:8080']

Backup and Recovery

Backup Strategy

Database snapshots:

  • Automatically stored in GitHub Releases (daily)
  • Download: curl -L -o aircraft.db https://github.com/ryakel/tail-lookup/releases/latest/download/aircraft.db

Container backup (not typically needed):

  • Database is immutable in container
  • Just pull latest image to restore

Disaster Recovery

Scenario: Container lost, need to restore

  1. Pull latest image:

    docker pull ryakel/tail-lookup:latest
  2. Redeploy:

    docker run -d -p 8080:8080 --restart unless-stopped ryakel/tail-lookup:latest

Scenario: Need specific historical database

  1. Download from release:

    curl -L -o aircraft.db https://github.com/ryakel/tail-lookup/releases/download/data-2025-11-20/aircraft.db
  2. Build custom image:

    FROM ryakel/tail-lookup:latest
    COPY aircraft.db /app/data/aircraft.db
  3. Build and run:

    docker build -t tail-lookup:custom .
    docker run -d -p 8080:8080 tail-lookup:custom

Scaling Considerations

Vertical Scaling (Bigger Container)

Adjust resource limits:

Docker:

docker run -d \
  --name tail-lookup \
  -p 8080:8080 \
  --memory=512m \
  --cpus=1.0 \
  ryakel/tail-lookup:latest

Kubernetes:

resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "1000m"

Horizontal Scaling (More Containers)

Load Balancer Required:

  • Nginx, HAProxy, Traefik, or cloud load balancer
  • Round-robin or least-connections algorithm

Docker Compose (simple):

version: "3.8"

services:
  tail-lookup:
    image: ryakel/tail-lookup:latest
    deploy:
      replicas: 3
    ports:
      - "8080-8082:8080"

Kubernetes (production):

spec:
  replicas: 5  # Run 5 instances

Database considerations:

  • Each container has full database copy
  • Read-only workload, no synchronization needed
  • No shared state between containers
  • Perfect for horizontal scaling

Performance Tuning

Database optimization (already done):

  • SQLite with JOIN index
  • Batch inserts during build
  • Read-only mode (no write locks)

Application optimization (already done):

  • Async FastAPI with Uvicorn
  • Pydantic validation
  • No external API calls

Container optimization:

  • Use date-tagged images (not latest) for consistency
  • Enable Docker BuildKit for faster builds
  • Use multi-stage builds (not applicable here)

Security Best Practices

Network Security

Firewall rules:

  • Allow inbound on port 8080 (or your mapped port)
  • Deny all other inbound traffic
  • Allow outbound (for Docker Hub pulls)

TLS/SSL:

  • Use reverse proxy (Nginx, Traefik, Caddy) for TLS termination
  • Let's Encrypt for free certificates
  • Redirect HTTP to HTTPS

CORS:

  • Currently allows all origins ( Allow-Origin: *)
  • For production, consider restricting to specific domains

Container Security

Run as non-root (future improvement):

USER nobody

Read-only root filesystem (future improvement):

docker run --read-only --tmpfs /tmp ryakel/tail-lookup:latest

Security scanning:

docker scan ryakel/tail-lookup:latest

Access Control

No built-in authentication (by design):

  • Public FAA data, open access
  • Add authentication with reverse proxy if needed

Example with Nginx Basic Auth:

location / {
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
    proxy_pass http://localhost:8080;
}

Troubleshooting

Container Won't Start

Check logs:

docker logs tail-lookup

Common causes:

  • Port 8080 already in use: Change mapping -p 8081:8080
  • Database file missing: Use official image, not custom build
  • Python errors: Check Python version in Dockerfile

Health Check Failing

Manual check:

curl http://localhost:8080/api/v1/health

Expected response:

{
  "status": "healthy",
  "database_exists": true,
  "record_count": 297431,
  "last_updated": "2025-11-28T06:15:23Z"
}

If failing:

  • Verify container is running: docker ps
  • Check database exists: docker exec tail-lookup ls -lh /app/data/aircraft.db
  • Restart container: docker restart tail-lookup

Stale Database

Symptom: last_updated is old (more than 24 hours)

Solutions:

  1. Pull latest image: docker pull ryakel/tail-lookup:latest
  2. Recreate container
  3. Check nightly build workflow for failures

High Memory Usage

Normal: ~100-150MB RAM usage

High (>500MB): Possible memory leak

Solutions:

  1. Restart container: docker restart tail-lookup
  2. Check for long-running requests
  3. Monitor with docker stats tail-lookup

Monitoring and Logging

Container Logs

View logs:

docker logs -f tail-lookup

Log to file:

docker logs tail-lookup > tail-lookup.log 2>&1

Docker Compose logging:

services:
  tail-lookup:
    image: ryakel/tail-lookup:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Application Metrics (Future)

Prometheus metrics (not yet implemented):

# Add to main.py
from prometheus_fastapi_instrumentator import Instrumentator

Instrumentator().instrument(app).expose(app)

Access at: http://localhost:8080/metrics

Uptime Monitoring

Self-hosted (Uptime Kuma):

docker run -d \
  --name uptime-kuma \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  louislam/uptime-kuma:latest

Add monitor for http://tail-lookup:8080/api/v1/health


Cost Considerations

Hosting Costs

Self-hosted (VPS):

  • Small VPS (1 vCPU, 1GB RAM): $5-10/month
  • Can run many containers
  • Example: DigitalOcean, Linode, Vultr

Cloud Platforms:

  • AWS ECS: ~$15-30/month (Fargate)
  • Google Cloud Run: ~$5-15/month (pay-per-use)
  • Azure Container Instances: ~$10-20/month

Kubernetes Cluster:

  • Managed: $50-150/month (EKS, GKE, AKS)
  • Self-hosted: VPS costs only

Bandwidth Costs

Docker Hub pulls:

  • Free tier: 100 pulls per 6 hours (unauthenticated)
  • Paid tier: Unlimited pulls
  • Our usage: ~30 pulls/day (nightly builds + updates)

API traffic:

  • Minimal bandwidth (JSON responses ~1-5KB)
  • 100K requests/month β‰ˆ 100-500MB
  • Usually included in hosting

Recommended Production Setup

For production deployment, we recommend:

  1. Infrastructure:

    • Docker Compose on VPS (simple) or Kubernetes (complex)
    • Nginx reverse proxy for TLS
    • Automated updates (Watchtower or Portainer webhook)
  2. Monitoring:

    • Health check endpoint monitoring (UptimeRobot, Uptime Kuma)
    • Container logs to external service (optional)
    • Alerting on failures (email, Slack)
  3. Security:

    • TLS certificate (Let's Encrypt)
    • Firewall rules (only ports 80/443 open)
    • Regular image updates (automated)
  4. Backup:

    • No backup needed (database in GitHub Releases)
    • Just redeploy latest image if container lost

Example Production Stack:

version: "3.8"

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - tail-lookup

  tail-lookup:
    image: ryakel/tail-lookup:latest
    restart: unless-stopped
    expose:
      - "8080"

  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 3600 tail-lookup

References

Clone this wiki locally