diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..3f26d12 --- /dev/null +++ b/.env.production.example @@ -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:@/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 diff --git a/README.md b/README.md index 1db310a..4c4302c 100644 --- a/README.md +++ b/README.md @@ -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:@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. diff --git a/docker-compose.yml b/docker-compose.yml index b8b60dc..83ebcd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: @@ -10,6 +19,8 @@ services: depends_on: db: condition: service_healthy + migrate: + condition: service_completed_successfully volumes: - ./app:/app/app # Hot reload in development diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..89aaa4b --- /dev/null +++ b/scripts/deploy.sh @@ -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}" diff --git a/start.sh b/start.sh index 9fd160c..9e1b577 100755 --- a/start.sh +++ b/start.sh @@ -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}"