Skip to content
Closed
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.dev.example
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions .env.prod.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Environment identity
SQUIRREL_ENV=prod
SQUIRREL_DEBUG=false

# Database (managed/prod)
SQUIRREL_DATABASE_URL=postgresql+asyncpg://squirrel:<DB_PASSWORD>@prod-db.example.org:5432/squirrel

# Redis (managed/prod)
SQUIRREL_REDIS_URL=redis://:<REDIS_PASSWORD>@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
102 changes: 102 additions & 0 deletions .github/workflows/deploy-dev-prod.yml
Original file line number Diff line number Diff line change
@@ -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
151 changes: 151 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -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/<org>/<repo>:sha-<commit>
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://<host>:8000/docs >/dev/null
```

2. Worker startup logs
```bash
docker logs --tail=100 <worker-container-name>
```

3. Monitor startup logs
```bash
docker logs --tail=100 <monitor-container-name>
```

## 9) Rollback

Rollback by redeploying a previous image tag:

```bash
export SQUIRREL_IMAGE=ghcr.io/<org>/<repo>:sha-<previous-commit>
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.
78 changes: 78 additions & 0 deletions docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -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:
Loading