diff --git a/.env.dev.example b/.env.dev.example new file mode 100644 index 0000000..0df52d9 --- /dev/null +++ b/.env.dev.example @@ -0,0 +1,24 @@ +# Environment identity +SQUIRREL_ENV=dev +SQUIRREL_DEBUG=true + +# Database (dev-local) +SQUIRREL_DATABASE_URL=postgresql+asyncpg://squirrel:squirrel@127.0.0.1:5432/squirrel_dev + +# Redis (dev-local) +SQUIRREL_REDIS_URL=redis://:squirrel@127.0.0.1:6379/0 + +# EPICS (DEV gateway only, non-critical IOCs) +SQUIRREL_EPICS_CA_AUTO_ADDR_LIST=NO +SQUIRREL_EPICS_CA_ADDR_LIST=dev-gateway.example.org:5068 +SQUIRREL_EPICS_CA_CONN_TIMEOUT=2.0 +SQUIRREL_EPICS_CA_TIMEOUT=5.0 + +SQUIRREL_EPICS_PVA_AUTO_ADDR_LIST=NO +SQUIRREL_EPICS_PVA_ADDR_LIST=dev-gateway.example.org:5075 +SQUIRREL_EPICS_PVA_SERVER_PORT=5075 +SQUIRREL_EPICS_PVA_BROADCAST_PORT=5076 +SQUIRREL_EPICS_PVA_TIMEOUT=5.0 + +# Optional safety controls +SQUIRREL_EPICS_UNPREFIXED_PVA_FALLBACK=false diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..6f3763b --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,24 @@ +# Environment identity +SQUIRREL_ENV=prod +SQUIRREL_DEBUG=false + +# Database (managed/prod) +SQUIRREL_DATABASE_URL=postgresql+asyncpg://squirrel:@prod-db.example.org:5432/squirrel + +# Redis (managed/prod) +SQUIRREL_REDIS_URL=redis://:@prod-redis.example.org:6379/0 + +# EPICS (PROD gateway only) +SQUIRREL_EPICS_CA_AUTO_ADDR_LIST=NO +SQUIRREL_EPICS_CA_ADDR_LIST=prod-gateway.example.org:5068 +SQUIRREL_EPICS_CA_CONN_TIMEOUT=2.0 +SQUIRREL_EPICS_CA_TIMEOUT=10.0 + +SQUIRREL_EPICS_PVA_AUTO_ADDR_LIST=NO +SQUIRREL_EPICS_PVA_ADDR_LIST=prod-gateway.example.org:5075 +SQUIRREL_EPICS_PVA_SERVER_PORT=5075 +SQUIRREL_EPICS_PVA_BROADCAST_PORT=5076 +SQUIRREL_EPICS_PVA_TIMEOUT=10.0 + +# Optional safety controls +SQUIRREL_EPICS_UNPREFIXED_PVA_FALLBACK=false diff --git a/.github/workflows/deploy-dev-prod.yml b/.github/workflows/deploy-dev-prod.yml new file mode 100644 index 0000000..a877e0b --- /dev/null +++ b/.github/workflows/deploy-dev-prod.yml @@ -0,0 +1,102 @@ +name: Deploy Dev and Prod + +on: + # Auto-deploy dev from main. + push: + branches: + - main + # Deploy prod from version tags. + tags: + - "v*" + # Allow manual execution from Actions UI. + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # Build once, publish once, deploy same artifact to each environment. + build-image: + runs-on: ubuntu-latest + outputs: + image_ref: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Immutable tag for exact commit deploys. + type=sha + # Convenience tag for default branch consumers. + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.dev + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy-dev: + runs-on: ubuntu-latest + needs: build-image + if: github.ref == 'refs/heads/main' + environment: dev + steps: + - name: Deploy to DEV via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEV_SSH_HOST }} + username: ${{ secrets.DEV_SSH_USER }} + key: ${{ secrets.DEV_SSH_KEY }} + script: | + set -euo pipefail + cd /opt/react-squirrel-backend + # Pin deployment to the exact image built from this commit. + export SQUIRREL_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} + docker compose -f docker/docker-compose.prod.yml --env-file .env.dev pull + docker compose -f docker/docker-compose.prod.yml --env-file .env.dev up -d + + # Promote to prod only from release tags (v*). + deploy-prod: + runs-on: ubuntu-latest + needs: + - build-image + if: startsWith(github.ref, 'refs/tags/v') + environment: prod + steps: + - name: Deploy to PROD via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.PROD_SSH_HOST }} + username: ${{ secrets.PROD_SSH_USER }} + key: ${{ secrets.PROD_SSH_KEY }} + script: | + set -euo pipefail + cd /opt/react-squirrel-backend + # Use the same immutable commit image in prod. + export SQUIRREL_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} + docker compose -f docker/docker-compose.prod.yml --env-file .env.prod pull + docker compose -f docker/docker-compose.prod.yml --env-file .env.prod up -d diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..85bc9de --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,151 @@ +# Deployment Runbook (Dev and Prod) + +This project uses one codebase and two deployment environments: +- `dev` for testing and non-critical data +- `prod` for production workloads + +## 1) Files Added for Environment Separation + +- `docker/docker-compose.dev.yml` +- `docker/docker-compose.prod.yml` +- `.env.dev.example` +- `.env.prod.example` +- `.github/workflows/deploy-dev-prod.yml` + +## 2) Environment Variables + +Create real env files from templates: + +```bash +cp .env.dev.example .env.dev +cp .env.prod.example .env.prod +``` + +Edit both files with real values. + +Important: +- `dev` must point to non-critical EPICS gateways. +- `prod` must point to production gateways. +- Keep secrets out of git. + +## 3) Local Dev Startup + +Use dev compose: + +```bash +docker compose -f docker/docker-compose.dev.yml --env-file .env.dev up -d --build +``` + +Service check: + +```bash +docker compose -f docker/docker-compose.dev.yml --env-file .env.dev ps +``` + +Stop: + +```bash +docker compose -f docker/docker-compose.dev.yml --env-file .env.dev down +``` + +## 4) Production Runtime Model + +`docker/docker-compose.prod.yml` expects: +- External Postgres (from `.env.prod`) +- External Redis (from `.env.prod`) +- Prebuilt app image via `SQUIRREL_IMAGE` + +Manual prod launch example: + +```bash +export SQUIRREL_IMAGE=ghcr.io//:sha- +docker compose -f docker/docker-compose.prod.yml --env-file .env.prod pull +docker compose -f docker/docker-compose.prod.yml --env-file .env.prod up -d +``` + +## 5) CI/CD Workflow + +Workflow: `.github/workflows/deploy-dev-prod.yml` + +Behavior: +- Push to `main` -> build image -> deploy to `dev` +- Push tag `v*` -> build image -> deploy to `prod` + +Required GitHub secrets: + +Dev: +- `DEV_SSH_HOST` +- `DEV_SSH_USER` +- `DEV_SSH_KEY` + +Prod: +- `PROD_SSH_HOST` +- `PROD_SSH_USER` +- `PROD_SSH_KEY` + +Also configure GitHub Environments: +- `dev` +- `prod` (recommended: require manual approval) + +## 6) First-Time Server Bootstrap + +On target host(s): + +1. Install Docker and Docker Compose plugin. +2. Clone repo to deployment path (workflow expects `/opt/react-squirrel-backend`). +3. Create env file: + - `/opt/react-squirrel-backend/.env.dev` for dev host + - `/opt/react-squirrel-backend/.env.prod` for prod host +4. Verify registry pull access (for GHCR private images if needed). +5. Run the compose commands from section 4. + +## 7) Database Migrations + +Run Alembic before or during deploy (policy decision): + +```bash +source venv/bin/activate +alembic upgrade head +``` + +Recommended: +- auto-migrate in `dev` +- gated/approved migrate in `prod` + +## 8) Health and Smoke Checks + +After deployment: + +1. API health (if endpoint exists) +```bash +curl -f http://:8000/docs >/dev/null +``` + +2. Worker startup logs +```bash +docker logs --tail=100 +``` + +3. Monitor startup logs +```bash +docker logs --tail=100 +``` + +## 9) Rollback + +Rollback by redeploying a previous image tag: + +```bash +export SQUIRREL_IMAGE=ghcr.io//:sha- +docker compose -f docker/docker-compose.prod.yml --env-file .env.prod up -d +``` + +If a DB migration is incompatible, restore from backup before rollback. + +## 10) Security Checklist + +- Do not commit `.env.dev` or `.env.prod`. +- Use unique credentials for dev/prod. +- Use Redis auth in both environments. +- Restrict prod SSH keys to deployment-only accounts. +- Use GitHub Environment approval for `prod` deploys. diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 0000000..7e885c7 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,78 @@ +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: squirrel + POSTGRES_PASSWORD: squirrel + POSTGRES_DB: squirrel_dev + ports: + - "5432:5432" + volumes: + - squirrel_dev_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U squirrel"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + command: redis-server --requirepass squirrel + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "squirrel", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + api: + build: + context: .. + dockerfile: docker/Dockerfile.dev + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + env_file: + - ../.env.dev + ports: + - "8080:8000" + volumes: + - ../app:/app/app + - ../tests:/app/tests + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + + monitor: + build: + context: .. + dockerfile: docker/Dockerfile.dev + command: python -m app.monitor_main + env_file: + - ../.env.dev + volumes: + - ../app:/app/app + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + + worker: + build: + context: .. + dockerfile: docker/Dockerfile.dev + command: arq app.worker.WorkerSettings + env_file: + - ../.env.dev + volumes: + - ../app:/app/app + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + +volumes: + squirrel_dev_db_data: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml new file mode 100644 index 0000000..ba5a45e --- /dev/null +++ b/docker/docker-compose.prod.yml @@ -0,0 +1,27 @@ +services: + api: + image: ${SQUIRREL_IMAGE:?set SQUIRREL_IMAGE} + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + env_file: + - ../.env.prod + ports: + - "8000:8000" + restart: unless-stopped + + monitor: + image: ${SQUIRREL_IMAGE:?set SQUIRREL_IMAGE} + command: python -m app.monitor_main + env_file: + - ../.env.prod + restart: unless-stopped + + worker: + image: ${SQUIRREL_IMAGE:?set SQUIRREL_IMAGE} + command: arq app.worker.WorkerSettings + env_file: + - ../.env.prod + restart: unless-stopped + +# Note: +# - This production compose assumes external managed Postgres/Redis from .env.prod. +# - If you host DB/Redis on the same server, add explicit db/redis services here.