> ## Documentation Index
> Fetch the complete documentation index at: https://docs.causeloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker & Docker Compose

> Run the full Causeloop stack — API, Postgres, and Caddy (automatic HTTPS) — on a single machine with docker compose up.

The repository ships a production-ready `docker-compose.yml` that runs three services on one box:

| Service | Image                   | Role                                            |
| ------- | ----------------------- | ----------------------------------------------- |
| `api`   | Built from `Dockerfile` | FastAPI application (Python 3.11)               |
| `db`    | `postgres:16-alpine`    | PostgreSQL 16 database with persistent volume   |
| `caddy` | `caddy:2-alpine`        | Reverse proxy — automatic TLS via Let's Encrypt |

<Note>
  This single-box setup is suitable for most early-stage and growth-stage deployments. For high-availability requirements, move Postgres to a managed provider (Neon, RDS, Cloud SQL) and deploy the API container to a managed compute platform.
</Note>

## Dockerfile

The image uses a two-stage build to minimize the final image size. The runtime stage runs as a non-root user (`appuser`) for security.

```dockerfile theme={null}
# ── build stage ──────────────────────────────────────────────────────────────
FROM python:3.11-slim AS build
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip \
 && pip install --no-cache-dir -r requirements.txt

# ── runtime stage ─────────────────────────────────────────────────────────────
FROM python:3.11-slim AS runtime
WORKDIR /app
COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=build /usr/local/bin /usr/local/bin
COPY app/ ./app/

RUN adduser --disabled-password --gecos "" appuser
USER appuser

ENV PORT=4000
EXPOSE 4000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:${PORT}/health')"

CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT} --workers 2"]
```

**Build and run the image standalone:**

```bash theme={null}
# Build
docker build -t causeloop-backend:latest .

# Run with .env file
docker run --rm -p 4000:4000 --env-file .env causeloop-backend:latest

# Run in mock LLM mode (no .env needed)
make docker-run-mock
```

## docker-compose.yml

```yaml theme={null}
services:
  api:
    build: .
    restart: unless-stopped
    env_file: .env
    environment:
      PORT: 4000
      DATABASE_URL: postgresql://causeloop:${POSTGRES_PASSWORD}@db:5432/causeloop
    depends_on:
      db:
        condition: service_healthy
    expose:
      - "4000"
    healthcheck:
      test: ["CMD", "python", "-c",
             "import urllib.request; urllib.request.urlopen('http://localhost:4000/health')"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: causeloop
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: causeloop
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U causeloop"]
      interval: 10s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - api

volumes:
  pgdata:
  caddy_data:
  caddy_config:
```

## Caddyfile

Edit `Caddyfile` and replace `api.yourdomain.com` with your real subdomain before starting:

```caddyfile theme={null}
# Caddy automatically provisions and renews a free Let's Encrypt TLS cert.
api.yourdomain.com {
    reverse_proxy api:4000

    header {
        Strict-Transport-Security "max-age=31536000;"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        -Server
    }

    encode gzip
}
```

Caddy handles certificate provisioning and renewal automatically — no certbot, no cron jobs.

## Deploying the stack

<Steps>
  <Step title="Provision your server and point DNS">
    Create an Ubuntu 22.04 / Debian 12 VM (Hetzner CX22 is \~€5/month). Add an A record pointing `api.yourdomain.com` to the server's public IP. Let the record propagate before starting Caddy.
  </Step>

  <Step title="Install Docker">
    ```bash theme={null}
    ssh root@<server-ip>
    curl -fsSL https://get.docker.com | sh
    ```
  </Step>

  <Step title="Clone the repository">
    ```bash theme={null}
    git clone https://github.com/your-org/causeloop-backend.git
    cd causeloop-backend
    ```
  </Step>

  <Step title="Configure secrets">
    ```bash theme={null}
    cp .env.example .env
    ```

    Edit `.env` — at minimum set:

    ```env theme={null}
    JWT_SECRET=<strong-random-string>
    CORS_ORIGINS=https://app.yourdomain.com
    POSTGRES_PASSWORD=<strong-random-password>
    CAUSELOOP_MASTER_KEY=<base64-32-bytes>
    # Optional — add LLM keys for real AI responses:
    # ANTHROPIC_API_KEY=sk-ant-...
    ```

    Generate secrets:

    ```bash theme={null}
    python3 -c "import secrets; print(secrets.token_urlsafe(48))"  # JWT_SECRET
    python3 -c "import os,base64; print(base64.b64encode(os.urandom(32)).decode())"  # CAUSELOOP_MASTER_KEY
    ```
  </Step>

  <Step title="Edit the Caddyfile">
    Replace `api.yourdomain.com` in `Caddyfile` with your actual subdomain.
  </Step>

  <Step title="Start the stack">
    ```bash theme={null}
    docker compose up -d
    ```

    On first start, `db` initialises, then `api` starts once the database health check passes, then `caddy` begins serving HTTPS.
  </Step>

  <Step title="Load the database schema">
    After the stack is running, load the schema into the Postgres container:

    ```bash theme={null}
    docker compose exec db psql -U causeloop -d causeloop -f /dev/stdin < db/schema.sql
    docker compose exec db psql -U causeloop -d causeloop -f /dev/stdin < db/seed_reference.sql
    docker compose exec db psql -U causeloop -d causeloop -f /dev/stdin < db/onboard_client.sql
    ```

    Then apply migrations:

    ```bash theme={null}
    DATABASE_URL=postgresql://causeloop:${POSTGRES_PASSWORD}@localhost:5432/causeloop make migrate
    ```

    See [Database setup](/deploy-security/database) for the full guide, including the required `causeloop_app` role.
  </Step>

  <Step title="Verify">
    ```bash theme={null}
    curl https://api.yourdomain.com/health
    # → {"status":"ok"}
    ```
  </Step>
</Steps>

<Warning>
  The `db` service in `docker-compose.yml` runs as `POSTGRES_USER=causeloop`, which is the Postgres superuser for that database. **Row-Level Security is bypassed by superusers.** Before using the database with real tenant data, create a dedicated `causeloop_app` role with `NOSUPERUSER` / `NOBYPASSRLS` and set `DATABASE_URL` to connect as that role. See [Database setup — RLS two-role model](/deploy-security/database#rls-two-role-model).
</Warning>

## Kubernetes

The Docker image works on any Kubernetes cluster. Use the `/health` and `/ready` endpoints as probes:

```yaml theme={null}
livenessProbe:
  httpGet:
    path: /health
    port: 4000
  initialDelaySeconds: 10
readinessProbe:
  httpGet:
    path: /ready
    port: 4000
  initialDelaySeconds: 5
```

Push the image to your registry:

```bash theme={null}
docker build -t causeloop-backend:latest .
docker tag causeloop-backend:latest your-registry/causeloop-backend:latest
docker push your-registry/causeloop-backend:latest
```

Supply environment variables via a `Secret` (for `JWT_SECRET`, `CAUSELOOP_MASTER_KEY`, `DATABASE_URL`) and a `ConfigMap` (for `PORT`, `CORS_ORIGINS`, `LLM_PROVIDER_ORDER`).

## Updating

```bash theme={null}
git pull
docker compose build api
docker compose up -d api
```

Caddy and Postgres do not need to be restarted for application updates.
