Skip to content

Add Docker / cloud-friendly deploy path #4

Description

@madjin

Add Docker / cloud-friendly deploy path

Context

Today, x402chat is deployed on VPS-style infrastructure via setup.sh (installs to venv, configures systemd service, and optionally nginx). This works well for traditional servers, but many hosting platforms (Fly.io, Railway, Render, etc.) expect container images. This issue adds first-class Docker support without replacing the existing VPS path—both should coexist.

A cloud platform deployment should be as simple as:

docker build . && docker run -e HELIUS_API_KEY=... -e ENCRYPTION_KEY=... -v data:/app/data myapp

And local testing should require no nginx or systemd knowledge:

docker compose up
curl http://localhost:8765/health

Goals

  • Single Dockerfile that produces a runnable production image of the FastAPI app.
  • docker-compose.yml for local development parity (contributors can test without installing nginx/systemd).
  • Platform compatibility with managed PaaS (at least one sample config for Fly.io, Railway, or Render).
  • Persistent SQLite via volume mount (e.g., ./data:/app/data in compose, or /app/data volume in production).
  • No TLS in the image (TLS is terminated by the host platform). No nginx needed in the container.
  • Healthcheck wired to the existing /health endpoint (confirmed at app.py:1326).
  • Non-root user for security.
  • Minimal image size (target <300 MB).

Proposed Dockerfile shape

# Multi-stage build
FROM python:3.10-slim as builder
# Use uv or pip-tools to lock + install dependencies into a temp venv

FROM python:3.10-slim as runtime
# Copy app files: app.py, static/, privacy_scorer.py, requirements.txt
# Create non-root user (e.g., appuser:appuser)
# Set working directory to /app
# EXPOSE 8765
# CMD: run uvicorn directly for proper signal handling (not via app.py CLI)

Key points:

  • Lock dependencies once at build time (reproducible).
  • Runtime image copies only the installed dependencies + app code.
  • Non-root user prevents container escape scenarios.
  • uvicorn app:app --host 0.0.0.0 --port 8765 for proper SIGTERM handling (unlike app.py server which may delay shutdown).

docker-compose.yml shape

version: '3.9'
services:
  app:
    build: .
    ports:
      - "8765:8765"
    env_file: .env
    environment:
      DATABASE_URL: sqlite:////app/data/donations.db
    volumes:
      - ./data:/app/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8765/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Notes:

  • env_file: .env loads secrets (HELIUS_API_KEY, ENCRYPTION_KEY, etc.) from the local .env.
  • DATABASE_URL is a 4-slash path because it's inside the container (sqlite:////app/data/donations.db = absolute path /app/data/donations.db).
  • Volume ./data:/app/data persists SQLite across docker compose down && docker compose up.
  • Healthcheck is optional but recommended for orchestration platforms.

App-side changes (minimal)

1. Confirm DATABASE_URL override works

Already implemented at app.py:891:

DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///donations.db")

Document in .env.example or docs/deploy-docker.md:

# For Docker deployments, use an absolute path inside the container:
# DATABASE_URL=sqlite:////app/data/donations.db

2. Cookie security behind TLS-terminating proxies

The app already respects ENVIRONMENT=production (line 733) and validates origin headers. The cookie secure=True will work if:

  • The platform sets X-Forwarded-Proto: https (nginx does this; so do Fly.io and Railway).
  • The app trusts that header (confirm in FastAPI middleware).

Document: "When running behind a reverse proxy, ensure it sets X-Forwarded-Proto: https. Fly.io, Railway, and Render all do this automatically."

3. Brief deployment docs

Add a short docs/deploy-docker.md or a "Docker" section in the README covering:

  • Env var checklist for production: HELIUS_API_KEY, ENCRYPTION_KEY, ENVIRONMENT=production, PRODUCTION_ORIGIN.
  • Volume setup: Production systems must mount /app/data to a persistent storage (e.g., Fly.io volumes, Railway persistent disks).
  • Example Fly.io fly.toml (stub):
    [build]
    dockerfile = "Dockerfile"
    
    [[services]]
    internal_port = 8765
    protocol = "tcp"
    
    [[services.ports]]
    port = 443
    handlers = ["http", "tls"]
    
    [[mounts]]
    source = "data"
    destination = "/app/data"

Out of scope

  • Postgres migration: SQLite is fine for single-replica deployments (which is the current model). If multi-replica is needed later, that's a separate DB decision. Document the SQLite limitation.
  • Removing VPS path: setup.sh, systemd service, and nginx config stay. Existing users are unaffected.
  • Multi-replica deployments: SQLite doesn't support concurrent writers. Docker doesn't change this limit.

Acceptance criteria

  • docker build -t x402chat:latest . succeeds and produces an image under 300 MB.
  • docker compose up boots and curl http://localhost:8765/health returns 200 (JSON with "status": "ok").
  • Volume persistence: after docker compose down && docker compose up, donation history is intact (SQL query shows records).
  • Dockerfile runs the app as a non-root user (confirm with docker run ... whoami).
  • Image can start with minimal env vars (HELIUS_API_KEY + ENCRYPTION_KEY); other vars have sane defaults.
  • Brief docs added (README section or docs/deploy-docker.md) with env var checklist and sample Fly.io config.
  • PR passes existing tests (if any) and doesn't break setup.sh flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions