Skip to content
Open
56 changes: 55 additions & 1 deletion .github/workflows/backend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ on:
- "backend/**"
- ".github/workflows/backend.yaml"

env:
# GCP/Deploy settings (used by deploy job)
GCP_PROJECT_ID: atomify-ee7b5
REGION: europe-west1
SERVICE_NAME: atomify-api
IMAGE_NAME: atomify-api

jobs:
test:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -52,5 +59,52 @@ jobs:
- uses: actions/checkout@v4

- name: Build Docker image
run: docker build -t atomify-api:test ./backend
run: docker build -t ${{ env.IMAGE_NAME }}:test ./backend

deploy:
needs: [test, docker]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker for GCR
run: gcloud auth configure-docker

- name: Build and push Docker image
run: |
IMAGE_TAG="gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
docker build -t $IMAGE_TAG ./backend
docker push $IMAGE_TAG
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV

- name: Deploy to Cloud Run
run: |
gcloud run deploy ${{ env.SERVICE_NAME }} \
--image=${{ env.IMAGE_TAG }} \
--region=${{ env.REGION }} \
--platform=managed \
--execution-environment=gen2 \
--allow-unauthenticated \
--service-account=${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} \
--set-env-vars="DATABASE_URL=sqlite+aiosqlite:////data/atomify.db,FIREBASE_PROJECT_ID=atomify-ee7b5,GCS_BUCKET_NAME=atomify-user-files,CORS_ORIGINS=http://localhost:3000,https://andeplane.github.io" \
--add-volume=name=db-volume,type=cloud-storage,bucket=atomify-db \
--add-volume-mount=volume=db-volume,mount-path=/data \
--memory=512Mi \
--cpu=1 \
--min-instances=0 \
--max-instances=10 \
--timeout=300 \
--port=8000
21 changes: 21 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# API Settings
API_HOST=0.0.0.0
API_PORT=8000
DEBUG=true

# Database (SQLite)
DATABASE_URL=sqlite+aiosqlite:///./atomify.db

# Firebase
FIREBASE_PROJECT_ID=atomify-ee7b5

# Google Cloud credentials (for GCS and Firebase Admin)
# Option 1: Use gcloud auth application-default login (no file needed)
# Option 2: Point to a service account key file
# GOOGLE_APPLICATION_CREDENTIALS=./service-account.json

# Google Cloud Storage
GCS_BUCKET_NAME=atomify-user-files

# CORS (comma-separated origins)
CORS_ORIGINS=http://localhost:3000,http://localhost:5173,https://andeplane.github.io
6 changes: 5 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ venv/
*.sqlite

# Environment
.env
# .env.local # Use for local overrides if needed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Committing .env files is a security risk and a configuration management anti-pattern. These files often contain sensitive data or environment-specific values that should not be in version control. The standard practice is to ignore .env files and commit a template like .env.example instead. Both .env and any local overrides like .env.local should be added to .gitignore.

.env
.env.local


# Service account keys (NEVER commit these)
*-key.json
service-account.json

# IDE
.idea/
Expand Down
176 changes: 176 additions & 0 deletions backend/DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Backend Deployment Guide

## Overview

The backend is deployed to Google Cloud Run automatically via GitHub Actions when changes are pushed to `main`.

## Hardcoded Values (in workflow)

- **GCP Project ID**: `atomify-ee7b5`
- **Firebase Project ID**: `atomify-ee7b5` (same as GCP)
- **Region**: `europe-west1`
- **Service Name**: `atomify-api`
- **Database URL**: `sqlite+aiosqlite:////data/atomify.db` (SQLite file path)
- **GCS Bucket Name**: `atomify-user-files`
- **CORS Origins**: `http://localhost:3000,https://andeplane.github.io`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The CORS origins listed here (and also in the manual deployment command on line 141) are inconsistent with the backend/.env file, which also includes http://localhost:5173. To avoid potential CORS issues during development and ensure the documentation is accurate, it's best to keep these consistent.

Suggested change
- **CORS Origins**: `http://localhost:3000,https://andeplane.github.io`
- **CORS Origins**: `http://localhost:3000,http://localhost:5173,https://andeplane.github.io`


## Required GitHub Secrets

Set these in your GitHub repository: **Settings → Secrets and variables → Actions**

### 1. `GCP_SA_KEY`
- **Description**: Service account JSON key for CI/CD deployment
- **Service account**: `[email protected]`
- **Roles**:
- `Cloud Run Admin` — deploy services
- `Service Account User` — impersonate runtime SA
- `Storage Object Admin` — push Docker images to GCR

### 2. `GCP_SERVICE_ACCOUNT_EMAIL`
- **Description**: Email of the service account that Cloud Run uses at runtime
- **Value**: `[email protected]`
- **Roles**:
- `Storage Object Admin` — access GCS buckets
- **Note**: No Firebase role needed — token verification uses public keys

## Cloud Run Setup

### 1. Enable APIs

```bash
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
containerregistry.googleapis.com \
storage-api.googleapis.com
```

### 2. Create Service Account for Cloud Run

```bash
gcloud iam service-accounts create atomify-api \
--display-name="Atomify API Service Account"

gcloud projects add-iam-policy-binding atomify-ee7b5 \
--member="serviceAccount:[email protected]" \
--role="roles/storage.objectAdmin"
```

### 3. Create GCS Bucket

**Note**: Billing must be enabled for the project first.

```bash
gcloud storage buckets create gs://atomify-user-files \
--project=atomify-ee7b5 \
--location=europe-west1

gcloud storage buckets add-iam-policy-binding gs://atomify-user-files \
--member="serviceAccount:[email protected]" \
--role="roles/storage.objectAdmin"
```

### 4. Create Database Bucket (for SQLite persistence)

SQLite is persisted via GCS FUSE mount:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using SQLite with GCS FUSE on Cloud Run is a risky approach, especially if the service might scale to more than one instance. SQLite is not designed for concurrent write access from multiple processes, which is what would happen with multiple Cloud Run instances. This can lead to database corruption. It is crucial to either configure Cloud Run to have a maximum of 1 instance or to prominently document this limitation and its risks. For a scalable service, consider using a managed database service like Cloud SQL.


```bash
# Create bucket for SQLite database
gcloud storage buckets create gs://atomify-db \
--project=atomify-ee7b5 \
--location=europe-west1

# Grant runtime service account access
gcloud storage buckets add-iam-policy-binding gs://atomify-db \
--member="serviceAccount:[email protected]" \
--role="roles/storage.objectAdmin"
```

The deploy command mounts this bucket at `/data` using Cloud Storage FUSE.

## Database Migrations

Migrations are **NOT** run automatically in the container startup to avoid race conditions.

### Option 1: Manual Migration (Recommended)

Before deploying, run migrations manually:

```bash
# Build and run migration container
docker build -t atomify-api:migrate ./backend
docker run --rm \
-e DATABASE_URL="$DATABASE_URL" \
atomify-api:migrate \
alembic upgrade head
Comment on lines +102 to +105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This command relies on the DATABASE_URL environment variable being exported in the shell. To make this more convenient and less error-prone for developers, you could use Docker's --env-file flag to load variables directly from a local .env file (which should be git-ignored).

Suggested change
docker run --rm \
-e DATABASE_URL="$DATABASE_URL" \
atomify-api:migrate \
alembic upgrade head
docker run --rm \
--env-file ./backend/.env \
atomify-api:migrate \
alembic upgrade head

```

### Option 2: Cloud Run Job

Create a Cloud Run Job for migrations:

```bash
gcloud run jobs create atomify-api-migrate \
--image=gcr.io/atomify-ee7b5/atomify-api:latest \
--region=europe-west1 \
--set-env-vars="DATABASE_URL=$DATABASE_URL" \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This command requires the DATABASE_URL variable to be set in the shell. It would be helpful to add a note for developers on how to provide this value, for instance by sourcing it from their local .env file, to make the command easier to use.

--command="alembic" \
--args="upgrade,head"

# Run before each deployment
gcloud run jobs execute atomify-api-migrate --region=europe-west1 --wait
```

## Manual Deployment

If you need to deploy manually:

```bash
# Build and push
docker build -t gcr.io/atomify-ee7b5/atomify-api:latest ./backend
docker push gcr.io/atomify-ee7b5/atomify-api:latest

# Deploy
gcloud run deploy atomify-api \
--image=gcr.io/atomify-ee7b5/atomify-api:latest \
--region=europe-west1 \
--platform=managed \
--execution-environment=gen2 \
--allow-unauthenticated \
--service-account=atomify-api@atomify-ee7b5.iam.gserviceaccount.com \
--set-env-vars="DATABASE_URL=sqlite+aiosqlite:////data/atomify.db,FIREBASE_PROJECT_ID=atomify-ee7b5,GCS_BUCKET_NAME=atomify-user-files,CORS_ORIGINS=http://localhost:3000,https://andeplane.github.io" \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The CORS_ORIGINS value here is inconsistent with backend/.env as it's missing http://localhost:5173. To ensure the manual deployment command works as expected for all documented local development setups, this should be updated.

Suggested change
--set-env-vars="DATABASE_URL=sqlite+aiosqlite:////data/atomify.db,FIREBASE_PROJECT_ID=atomify-ee7b5,GCS_BUCKET_NAME=atomify-user-files,CORS_ORIGINS=http://localhost:3000,https://andeplane.github.io" \
--set-env-vars="DATABASE_URL=sqlite+aiosqlite:////data/atomify.db,FIREBASE_PROJECT_ID=atomify-ee7b5,GCS_BUCKET_NAME=atomify-user-files,CORS_ORIGINS=http://localhost:3000,http://localhost:5173,https://andeplane.github.io" \

--add-volume=name=db-volume,type=cloud-storage,bucket=atomify-db \
--add-volume-mount=volume=db-volume,mount-path=/data \
--memory=512Mi \
--cpu=1 \
--port=8000
```

## Monitoring

- **Logs**: `gcloud run services logs read atomify-api --region=europe-west1`
- **Metrics**: Google Cloud Console → Cloud Run → atomify-api
- **Health Check**: `https://atomify-api-xxx.run.app/health`

## Troubleshooting

### Service won't start
- Check logs: `gcloud run services logs read atomify-api --region=europe-west1`
- Verify environment variables are set correctly
- Check service account permissions

### Database connection fails
- Verify `DATABASE_URL` is correct
- For Cloud SQL: Ensure Cloud SQL Admin API is enabled
- For SQLite volume: Ensure volume is mounted at `/data`

### Firebase Auth fails
- Verify `FIREBASE_PROJECT_ID` matches your Firebase project
- Token verification uses public keys (no special permissions needed)
- Ensure Firebase Admin SDK is initialized correctly

### GCS access fails
- Verify `GCS_BUCKET_NAME` exists
- Check service account has `Storage Object Admin` role
- Verify bucket IAM allows the service account

Loading