Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Production env vars (Cloud Run). Reference only — actual values live in
# Secret Manager (DATABASE_URL, SECRET_KEY) or are baked into scripts/deploy.sh
# (the non-secret ones). Do NOT commit a populated copy of this file.

# Database — full asyncpg URL with a Cloud SQL Unix-socket host. Stored in
# Secret Manager as DATABASE_URL and bound to the Cloud Run service via
# --set-secrets in scripts/deploy.sh.
DATABASE_URL=postgresql+asyncpg://app:<password>@/treepolitics?host=/cloudsql/treepolitics-prod:us-east1:treepolitics-db

# JWT signing key — strong random, stored in Secret Manager as SECRET_KEY.
# Generate with: openssl rand -hex 32
SECRET_KEY=<32+ chars from openssl rand -hex 32>

ENVIRONMENT=production

# CORS — origins allowed to call the API. Frontend lives at treepolitics.net.
CORS_ORIGINS=https://treepolitics.net,https://www.treepolitics.net

FRONTEND_URL=https://treepolitics.net

# Shared across api.treepolitics.net + treepolitics.net so auth cookies work.
COOKIE_DOMAIN=.treepolitics.net

LOG_LEVEL=INFO
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,46 @@ uv run alembic history
uv run alembic revision --autogenerate -m "description"
```

## Production Deployment (Cloud Run)

The API runs on Cloud Run in GCP project `treepolitics-prod` (region `us-east1`), backed by a Cloud SQL Postgres instance and Secret Manager.

### Deploy a new revision

From a clean working tree on `main`:

```bash
./scripts/deploy.sh
```

The script builds via Cloud Build, pushes to Artifact Registry tagged with the current git short-SHA, and deploys a new Cloud Run revision pinned to that image.

### Run migrations against prod

Migrations are **not** auto-applied on container start. Run them separately before deploying a revision that introduces schema changes:

```bash
# Terminal 1 — start the Cloud SQL Auth Proxy
cloud-sql-proxy --address 127.0.0.1 --port 5432 \
treepolitics-prod:us-east1:treepolitics-db

# Terminal 2 — run migrations against the proxy
DATABASE_URL="postgresql+asyncpg://app:<password>@127.0.0.1:5432/treepolitics" \
uv run alembic upgrade head
```

The DB user password is in Secret Manager (not committed).

### Infrastructure (one-time setup, already provisioned)

- **Cloud SQL Postgres 16** (`treepolitics-db`, `db-f1-micro`, zonal, us-east1) — public IP, no authorized networks; only reachable via the Cloud SQL connector.
- **Artifact Registry** docker repo `api` in us-east1.
- **Runtime SA** `treepolitics-api-runtime@treepolitics-prod.iam.gserviceaccount.com` with `roles/cloudsql.client` and `roles/secretmanager.secretAccessor` (scoped to `DATABASE_URL` and `SECRET_KEY`).
- **Secrets** in Secret Manager: `DATABASE_URL`, `SECRET_KEY`.
- **Custom domain** `api.treepolitics.net` via Cloud Run domain mapping.

CI/CD (auto-deploy on merge to `main`) is tracked in [issue #5](https://github.com/ag-tech-group/treepolitics-api/issues/5).

## Testing

Tests use SQLite in-memory for speed and isolation.
Expand Down
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
services:
migrate:
build: .
command: ["uv", "run", "alembic", "upgrade", "head"]
environment:
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/api_template
depends_on:
db:
condition: service_healthy

api:
build: .
ports:
Expand All @@ -10,6 +19,8 @@ services:
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
volumes:
- ./app:/app/app # Hot reload in development

Expand Down
45 changes: 45 additions & 0 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Manual deploy of the Treepolitics API to Cloud Run.
#
# Until Phase D (#5) wires up GitHub Actions auto-deploy, run this from a clean
# working tree to ship a new revision. Migrations are NOT applied here — run
# them separately via cloud-sql-proxy (see README "Production Deployment").

set -euo pipefail

PROJECT="treepolitics-prod"
ACCOUNT="amr@agtechgroup.solutions"
REGION="us-east1"
SERVICE="treepolitics-api"
REPO="api"
RUNTIME_SA="treepolitics-api-runtime@${PROJECT}.iam.gserviceaccount.com"
SQL_INSTANCE="${PROJECT}:${REGION}:treepolitics-db"
TAG="$(git rev-parse --short HEAD)"
IMAGE="${REGION}-docker.pkg.dev/${PROJECT}/${REPO}/api:${TAG}"

echo "Building ${IMAGE} via Cloud Build..."
gcloud builds submit \
--tag "${IMAGE}" \
--account="${ACCOUNT}" \
--project="${PROJECT}"

echo "Deploying ${SERVICE} (revision pinned to ${TAG})..."
# `^|^` sets `|` as the env-var delimiter so commas inside CORS_ORIGINS survive.
gcloud run deploy "${SERVICE}" \
--image="${IMAGE}" \
--region="${REGION}" \
--service-account="${RUNTIME_SA}" \
--add-cloudsql-instances="${SQL_INSTANCE}" \
--set-env-vars="^|^ENVIRONMENT=production|COOKIE_DOMAIN=.treepolitics.net|FRONTEND_URL=https://treepolitics.net|CORS_ORIGINS=https://treepolitics.net,https://www.treepolitics.net" \
--set-secrets="DATABASE_URL=DATABASE_URL:latest,SECRET_KEY=SECRET_KEY:latest" \
--allow-unauthenticated \
--account="${ACCOUNT}" \
--project="${PROJECT}"

echo
echo "Deployed. Service URL:"
gcloud run services describe "${SERVICE}" \
--region="${REGION}" \
--format="value(status.url)" \
--account="${ACCOUNT}" \
--project="${PROJECT}"
6 changes: 2 additions & 4 deletions start.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#!/bin/sh
set -e

# Apply any pending database migrations
uv run alembic upgrade head

# Start the application
# Migrations run separately as a pre-deploy step (see scripts/deploy.sh) — avoids
# slow Cloud Run cold starts and migration races across concurrent revisions.
exec uv run uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}"
Loading