Deployment Guide¶
Complete guide for setting up, developing, building, releasing, and deploying Juddges App using Docker.
Table of Contents¶
- Prerequisites
- Initial Setup
- Development (Local with Hot Reload)
- Local Build & Deploy
- Versioned Releases (SemVer)
- Production Deployment (Registry Images)
- Rollback
- Monitoring & Troubleshooting
- Script Reference
1. Prerequisites¶
| Tool | Version | Check |
|---|---|---|
| Docker | 24+ | docker --version |
| Docker Compose | v2+ | docker compose version |
| Git | 2.30+ | git --version |
| Docker Hub account | — | docker login |
Server requirements for production: - 8 GB RAM minimum (16 GB recommended — backend alone reserves 12 GB) - 4 CPU cores minimum (backend reserves 4) - 30 GB disk space
2. Initial Setup¶
Clone and configure¶
git clone <repository-url>
cd juddges-app
# Create environment file from template
cp .env.example .env
Edit .env with your credentials¶
At minimum, set:
# Supabase (database + auth + vector search)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# LLM
OPENAI_API_KEY=sk-...
# Backend auth
BACKEND_API_KEY=your-backend-api-key
# Docker Hub (for build & deploy scripts)
DOCKER_USERNAME=your-dockerhub-username
DOCKER_TOKEN=your-dockerhub-access-token
Verify Docker is working¶
3. Development (Local with Hot Reload)¶
Use the dev helper script with docker-compose.dev.yml.
Dev ports¶
| Service | URL |
|---|---|
| Frontend | http://localhost:3007 |
| Backend API | http://localhost:8004 |
| API Docs (Swagger) | http://localhost:8004/docs |
Common dev commands¶
./docker-dev.sh stop # Stop all services
./docker-dev.sh restart # Restart all services
./docker-dev.sh logs-f # Follow logs
./docker-dev.sh status # Show container status
./docker-dev.sh rebuild # Rebuild images from scratch
./docker-dev.sh shell-be # Shell into backend container
./docker-dev.sh shell-fe # Shell into frontend container
./docker-dev.sh test-be # Run backend tests (pytest)
./docker-dev.sh test-fe # Run frontend tests (jest)
./docker-dev.sh clean # Remove containers and volumes
Hot reload behavior¶
- Backend: Python source mounted at
/app, Uvicorn runs with auto-reload - Frontend:
frontend/mounted at/app, Next.js dev server handles HMR - Worker: Celery worker shares the backend volume mount
- Dependency or Dockerfile changes require
./docker-dev.sh rebuild
4. Local Build & Deploy¶
For a production-like local build without version bumps or registry pushes:
Local production ports¶
| Service | Port | Notes |
|---|---|---|
| Frontend | 3006 (exposed internally) | Accessed via nginx-proxy or docker compose exec |
| Backend API | 8002 (exposed internally) | Same |
| Backend Worker | — | No port (Celery process) |
Note: Production compose uses
exposeinstead ofportsbecause services sit behind nginx-proxy. For direct access during local testing, temporarily switchexposetoportsor usedocker compose exec.
5. Versioned Releases (SemVer)¶
Important: Releases are cut from
mainonly. Before running this script, the work to be released must already be merged fromdevelopintomainvia a release PR (release: vX.Y.Z). See Section 13 — CI/CD Pipeline for the full branching flow.
The release flow is handled by scripts/build_and_push_prod.sh. It performs version bumping, file sync, Docker build + push, and git tagging in a single interactive script.
Usage¶
git checkout main && git pull # always run from clean main
./scripts/build_and_push_prod.sh # Auto-increment patch (0.0.2 -> 0.0.3)
./scripts/build_and_push_prod.sh patch # Same as above
./scripts/build_and_push_prod.sh minor # Increment minor (0.0.3 -> 0.1.0)
./scripts/build_and_push_prod.sh major # Increment major (0.1.0 -> 1.0.0)
./scripts/build_and_push_prod.sh 2.1.0 # Use explicit version
What the script does¶
- Reads the latest
prod-v*git tag to determine current version (legacyv*fallback) - Calculates the next version based on bump type
- Confirms with the user before proceeding
- Syncs version across all project files (see table below)
- Loads
.envfor Docker Hub credentials and frontend build args - Logs into Docker Hub
- Builds and pushes two Docker images (tagged with version +
latest): <username>/juddges-frontend:<version><username>/juddges-backend:<version>- Commits the version-synced files with message
release: v<version> - Creates an annotated git tag
prod-v<version> - Optionally pushes the commit and tag to origin (the tag push triggers the
deploy-prodCI job)
Version files kept in sync¶
| File | Field | Example |
|---|---|---|
VERSION |
Plain text | 1.2.3 |
backend/pyproject.toml |
version = "..." |
version = "1.2.3" |
frontend/package.json |
"version": "..." |
"version": "1.2.3" |
.env.example |
JUDDGES_IMAGE_TAG comment |
# JUDDGES_IMAGE_TAG=1.2.3 |
Docker images¶
The backend image is shared by two services:
- backend — runs the FastAPI API server
- backend-worker — runs celery -A app.workers worker (same image, different command)
6. Production Deployment (Registry Images)¶
After a versioned release, deploy on any server by pulling images from Docker Hub.
First-time server setup¶
git clone <repository-url>
cd juddges-app
cp .env.example .env
# Edit .env with production credentials
Deploy with the deploy script¶
./scripts/deploy_prod.sh # Deploy :latest
./scripts/deploy_prod.sh 0.2.0 # Deploy specific version
./scripts/deploy_prod.sh --status # Show running containers
What the deploy script does¶
- Loads
.envforDOCKER_USERNAME - Pulls both images at the specified tag
- Stops existing containers gracefully (30s timeout)
- Starts services with
JUDDGES_IMAGE_TAGset to the target version - Waits up to 120s for health checks (3+ containers running, none unhealthy)
- Logs the deployment to
.deploy-history
Manual deployment (without the script)¶
Deploy with SSL (nginx-proxy)¶
For HTTPS with automatic Let's Encrypt certificates, set in .env:
VIRTUAL_HOST_FRONTEND=app.yourdomain.com
VIRTUAL_PORT_FRONTEND=3006
VIRTUAL_HOST_BACKEND=api.yourdomain.com
VIRTUAL_PORT_BACKEND=8002
LETSENCRYPT_EMAIL=you@example.com
The production docker-compose.yml already includes VIRTUAL_HOST, VIRTUAL_PORT, and LETSENCRYPT_* environment variables. Requires the nginx-router_proxy-tier external Docker network from nginx-proxy.
Verify deployment¶
# Service status
docker compose ps
# Health checks
curl http://localhost:8002/health/healthz
docker compose exec frontend node -e "require('http').get('http://127.0.0.1:3006/', (r) => { console.log(r.statusCode); process.exit(r.statusCode === 200 ? 0 : 1) })"
7. Rollback¶
Using the deploy script¶
This reads .deploy-history to find the previous version and redeploys it (with confirmation).
Manual rollback to a specific version¶
Roll back from a broken local build¶
Revert a git release¶
# Revert the release commit
git revert HEAD
# Delete the tag locally and remotely
git tag -d v1.2.3
git push origin :refs/tags/v1.2.3
8. Monitoring & Troubleshooting¶
View logs¶
# All services
docker compose logs -f
# Specific service
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f backend-worker
# Last 100 lines
docker compose logs --tail=100 backend
Restart services¶
Check resource usage¶
Common issues¶
| Problem | Diagnosis | Fix |
|---|---|---|
| Service won't start | docker compose logs <service> |
Check .env vars, rebuild |
| Health check failing | curl localhost:8002/health/healthz |
Wait for start_period (40-60s), check dependencies |
| Out of disk space | docker system df |
docker system prune -f |
| Image pull fails | docker login |
Re-authenticate with Docker Hub |
| Port conflict | lsof -i :8004 |
Change port in .env or stop conflicting process |
| Frontend can't reach backend | Check NEXT_PUBLIC_API_BASE_URL |
Ensure it points to the correct backend URL |
| Celery worker not processing | docker compose logs backend-worker |
Check Redis connection, restart worker |
Docker cleanup¶
# Remove stopped containers
docker container prune -f
# Remove unused images
docker image prune -f
# Remove unused volumes (careful — deletes data!)
docker volume prune -f
# Remove everything unused
docker system prune -f --volumes
9. Script Reference¶
| Script | Purpose | When to use |
|---|---|---|
./docker-dev.sh |
Development environment with hot reload | Day-to-day development |
./scripts/build_and_push_prod.sh |
SemVer release: version sync, Docker build + push, git tag | Releasing a new version |
./scripts/deploy_prod.sh |
Pull from Docker Hub and deploy on production host | Deploying to production |
Release decision tree¶
Need to release a new version?
|
+--> Yes --> ./scripts/build_and_push_prod.sh
| |
| +--> Syncs version files
| +--> Builds + pushes Docker images
| +--> Creates git tag
| +--> Then deploy: ./scripts/deploy_prod.sh <version>
|
+--> No, just test production build locally --> docker compose up -d --build
|
+--> No, just developing --> ./docker-dev.sh start
Environment overview¶
| Environment | Compose file | Images from | Frontend | Backend | Worker |
|---|---|---|---|---|---|
| Development | docker-compose.dev.yml |
Local build + hot reload | :3007 | :8004 | (no port) |
| Production (local build) | docker-compose.yml |
Local docker compose build |
:3006* | :8002* | (no port) |
| Production (registry) | docker-compose.yml |
Docker Hub | :3006* | :8002* | (no port) |
* Production ports are exposed internally (behind nginx-proxy), not mapped to the host by default.
10. Architecture Overview¶
┌──────────────────────────────────────────────────────────┐
│ Production Host │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │ │ Backend │ │ Meilisearch │ │
│ │ (Next.js) │ │ (FastAPI) │ │ (Search) │ │
│ │ Port: 3006 │ │ Port: 8002 │ │ Port: 7700 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
│ │ │ │
│ │ ┌───────┴───────┐ │
│ │ │ │ │
│ │ ┌──────▼──────┐ ┌─────▼──────┐ │
│ │ │ Worker │ │ Beat │ │
│ │ │ (Celery) │ │ (Scheduler)│ │
│ │ └─────────────┘ └────────────┘ │
│ │ │
│ ┌──────▼───────────────────────────────────────────┐ │
│ │ Docker Network (app-network) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ External Services: │
│ - Supabase (PostgreSQL + Auth + pgvector) │
│ - Redis (Celery broker) │
│ - OpenAI API (embeddings + LLM) │
│ - Cohere API (reranking, optional) │
│ - Langfuse (observability, optional) │
└──────────────────────────────────────────────────────────┘
Service Summary¶
| Service | Image | Port | Purpose |
|---|---|---|---|
frontend |
juddges-frontend |
3006 | Next.js standalone server |
backend |
juddges-backend |
8002 | FastAPI with 12 Gunicorn workers |
backend-worker |
juddges-backend (shared) |
— | Celery async task worker |
backend-beat |
juddges-backend (shared) |
— | Celery periodic task scheduler |
meilisearch |
getmeili/meilisearch:v1.13 |
7700 | Autocomplete search engine |
11. Health Checks¶
All production services include health checks:
| Service | Check | Interval | Timeout | Retries | Start Period |
|---|---|---|---|---|---|
| Frontend | HTTP GET 127.0.0.1:3006 |
30s | 10s | 3 | 40s |
| Backend | HTTP GET /health/healthz |
30s | 10s | 3 | 40s |
| Worker | celery inspect ping |
60s | 30s | 3 | 60s |
| Meilisearch | HTTP GET /health |
30s | 5s | 3 | — |
# All services
docker compose ps
# Specific service health
docker inspect --format='{{.State.Health.Status}}' juddges-app-backend-1
# Health check logs
docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' juddges-app-backend-1
Backend Health Endpoints¶
| Endpoint | Auth | Purpose |
|---|---|---|
GET /health/healthz |
None | Basic liveness (used by Docker) |
GET /health/status |
API Key | Detailed status with service checks |
12. Resource Limits¶
Production Defaults¶
| Service | CPU Limit | Memory Limit | CPU Reserved | Memory Reserved |
|---|---|---|---|---|
| Frontend | 1 | 2 GB | 0.5 | 1 GB |
| Backend | 8 | 16 GB | 4 | 12 GB |
| Worker | 2 | 4 GB | 1 | 2 GB |
| Beat | 0.5 | 512 MB | 0.25 | 256 MB |
Backend Worker Configuration¶
Workers: 12 uvicorn workers
Timeout: 120 seconds
Graceful timeout: 30 seconds
Keep-alive: 5 seconds
Max requests: 1000 per worker (with 50 jitter)
13. CI/CD Pipeline¶
GitHub Actions Workflow¶
File: .github/workflows/ci.yml
Push to develop/main or PR
│
├─── backend-lint (Ruff format + lint)
├─── backend-test (pytest -m unit)
├─── frontend-lint (ESLint)
├─── frontend-test (Jest)
└─── docker-build (validate production build)
│
│ All pass
▼
┌─────────────────┐
│ Push to develop │──→ deploy-dev (tag: dev-latest)
└─────────────────┘
┌─────────────────┐
│ Push prod-v* tag│──→ deploy-prod (tag: version + latest)
└─────────────────┘
Branching Model¶
Two long-lived branches:
main— production. Only release PRs (fromdevelop) andhotfix/*PRs land here. Production images are built only when aprod-v*tag is pushed (and tags should always be cut frommain).develop— integration. All in-progress feature work lands here. A push todeveloptriggersdeploy-dev, which publishesdev-latestimages to Docker Hub (no production impact).
Short-lived branches:
feature/<name>,fix/<name>— branch fromdevelop, PR back intodevelop.hotfix/<name>— branch frommain, PR back intomain. After release, back-mergemain→developso the fix isn't lost on the next release.
Never open a feature PR directly against
main. Branch protection onmainshould be configured to enforce this; see the project root README for the canonical flow.
Release Flow¶
# 1. Open a PR from develop → main titled "release: vX.Y.Z" and merge it via GitHub.
# All feature/fix work that should ship in this release must already be in develop.
# 2. Build, tag, and push from a clean main:
git checkout main && git pull
./scripts/build_and_push_prod.sh # creates prod-vX.Y.Z tag
# 3. Pushing the prod-v* tag (the script offers to do this) triggers
# the deploy-prod CI job, which builds versioned + :latest images.
git push origin prod-vX.Y.Z # if you didn't push from the script
# 4. On the production host:
./scripts/deploy_prod.sh # pulls :latest and restarts containers
Hotfix Flow¶
# 1. Branch from main:
git checkout main && git pull
git checkout -b hotfix/<short-name>
# 2. Fix, commit, open a PR into main, merge.
# 3. Cut a patch release exactly as in the Release Flow above
# (./scripts/build_and_push_prod.sh, deploy_prod.sh on the host).
# 4. Back-merge main → develop so the hotfix is included in the next release:
git checkout develop && git pull
git merge main && git push
14. Observability¶
Langfuse Integration (Optional)¶
LLM call observability via Langfuse:
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://cloud.langfuse.com
ENABLE_LANGFUSE=true
Tracks: LLM query analysis, embedding generation, chat chain invocations, query enhancement.
Watchtower Labels¶
Services are labeled for Watchtower auto-update support: