Skip to content

Deployment Guide

This guide covers deploying Metricis to production environments.

Overview

Metricis consists of four main components that need to be deployed:

  1. Server (FastAPI backend) - Python application
  2. Portal (Researcher admin) - Static React app
  3. Patient Portal (Patient/caregiver) - Static React app
  4. Client (Assessment interface) - Static jsPsych app

Additionally, you'll need: - PostgreSQL database - Redis for session storage and Celery - Celery workers for background tasks

Prerequisites

  • Domain name with SSL certificate
  • PostgreSQL 15+ database
  • Redis 5.0+ instance
  • SMTP server for email notifications (optional)
  • Twilio account for SMS notifications (optional)
  • Firebase project for push notifications (optional)

Environment Configuration

Server Environment Variables

Create a .env file in the server/ directory:

# Application
ENVIRONMENT=production
DEBUG=false
LOG_LEVEL=info

# Database
DATABASE_URL=postgresql+asyncpg://user:password@db-host:5432/metricis

# Security (REQUIRED - generate with: python -c "import secrets; print(secrets.token_urlsafe(32))")
JWT_SECRET_KEY=your-secret-key-here
SESSION_SECRET_KEY=your-session-secret-here

# Redis
SESSION_STORAGE_BACKEND=redis
REDIS_URL=redis://redis-host:6379/0

# CORS - List all frontend domains
ALLOWED_ORIGINS=https://portal.metricis.app,https://app.metricis.app,https://assess.metricis.app

# Rate Limiting
RATE_LIMIT_PER_MINUTE=60
AUTH_RATE_LIMIT_PER_MINUTE=10

# REDCap (if using)
REDCAP_TOKEN_SITE1=your-redcap-api-token
REDCAP_TOKEN_SITE2=another-redcap-token

# Notifications (optional)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@metricis.app
SMTP_PASSWORD=your-smtp-password

TWILIO_ACCOUNT_SID=your-twilio-sid
TWILIO_AUTH_TOKEN=your-twilio-token
TWILIO_FROM_PHONE=+15551234567

FIREBASE_CREDENTIALS_PATH=/path/to/firebase-credentials.json

Frontend Environment Variables

Portal (.env.production)

VITE_API_URL=https://api.metricis.app/api
VITE_ENVIRONMENT=production

Patient Portal (.env.production)

VITE_API_URL=https://api.metricis.app/api
VITE_ENVIRONMENT=production

Client (.env.production)

VITE_API_URL=https://api.metricis.app/api
VITE_ENVIRONMENT=production

Deployment Options

1. Build and Run with Docker Compose

# docker-compose.prod.yml
version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: metricis
      POSTGRES_USER: metricis
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: always

  redis:
    image: redis:7-alpine
    restart: always
    volumes:
      - redis_data:/data

  server:
    build:
      context: ./server
      dockerfile: Dockerfile.prod
    environment:
      DATABASE_URL: postgresql+asyncpg://metricis:${DB_PASSWORD}@db:5432/metricis
      REDIS_URL: redis://redis:6379/0
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      SESSION_SECRET_KEY: ${SESSION_SECRET_KEY}
    depends_on:
      - db
      - redis
    restart: always

  celery_worker:
    build:
      context: ./server
      dockerfile: Dockerfile.prod
    command: celery -A app.celery_app worker -Q reminders -l info
    environment:
      DATABASE_URL: postgresql+asyncpg://metricis:${DB_PASSWORD}@db:5432/metricis
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - db
      - redis
    restart: always

  celery_beat:
    build:
      context: ./server
      dockerfile: Dockerfile.prod
    command: celery -A app.celery_app beat -l info
    environment:
      DATABASE_URL: postgresql+asyncpg://metricis:${DB_PASSWORD}@db:5432/metricis
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - db
      - redis
    restart: always

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
      - ./portal/dist:/usr/share/nginx/html/portal
      - ./patient-portal/dist:/usr/share/nginx/html/app
      - ./client/dist:/usr/share/nginx/html/assess
    depends_on:
      - server
    restart: always

volumes:
  postgres_data:
  redis_data:

2. Create Nginx Configuration

# nginx.conf
upstream api {
    server server:8000;
}

server {
    listen 80;
    server_name portal.metricis.app app.metricis.app assess.metricis.app api.metricis.app;
    return 301 https://$server_name$request_uri;
}

# Portal (Researcher Admin)
server {
    listen 443 ssl http2;
    server_name portal.metricis.app;

    ssl_certificate /etc/nginx/ssl/portal.crt;
    ssl_certificate_key /etc/nginx/ssl/portal.key;

    root /usr/share/nginx/html/portal;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# Patient Portal
server {
    listen 443 ssl http2;
    server_name app.metricis.app;

    ssl_certificate /etc/nginx/ssl/app.crt;
    ssl_certificate_key /etc/nginx/ssl/app.key;

    root /usr/share/nginx/html/app;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

# Assessment Client
server {
    listen 443 ssl http2;
    server_name assess.metricis.app;

    ssl_certificate /etc/nginx/ssl/assess.crt;
    ssl_certificate_key /etc/nginx/ssl/assess.key;

    root /usr/share/nginx/html/assess;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

# API Server
server {
    listen 443 ssl http2;
    server_name api.metricis.app;

    ssl_certificate /etc/nginx/ssl/api.crt;
    ssl_certificate_key /etc/nginx/ssl/api.key;

    location / {
        proxy_pass http://api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

3. Deploy

# Build frontend assets
npm run build
npm run build:portal
npm run build:patient-portal

# Start services
docker-compose -f docker-compose.prod.yml up -d

# Run migrations
docker-compose exec server alembic upgrade head

# Create admin user
docker-compose exec server python -m app.cli create-admin \
  --email admin@metricis.app \
  --password secure-password

Option 2: Separate Cloud Services

Server Deployment (Railway, Fly.io, or AWS)

  1. Build the server Docker image:
# server/Dockerfile.prod
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
  1. Deploy to Railway:
railway login
railway init
railway add
railway up
  1. Set environment variables in Railway dashboard

  2. Run migrations:

railway run alembic upgrade head

Frontend Deployment (Vercel, Netlify, or Cloudflare Pages)

  1. Build frontend apps:
npm run build
npm run build:portal
npm run build:patient-portal
  1. Deploy to Vercel:
# Install Vercel CLI
npm i -g vercel

# Deploy portal
cd portal
vercel --prod

# Deploy patient portal
cd patient-portal
vercel --prod

# Deploy client
cd client
vercel --prod
  1. Configure environment variables in Vercel dashboard

  2. Set up custom domains for each app

Option 3: Kubernetes

See k8s/ directory for Kubernetes manifests (not included in this guide).

Database Migrations

Rolling out a REDCap-managed sponsor study? This guide covers the generic platform deployment. The study-specific cutover (REDCap project config, token rotation, DET smoke tests, per-study admin assignment, post-cutover monitoring, failure-mode runbook) is in Sponsor-Study Cutover Checklist. Run this guide first, then walk that checklist for each sponsor study.

Production Migration Strategy

  1. Backup database before migration:
pg_dump -U metricis -h db-host metricis > backup_$(date +%Y%m%d).sql
  1. Test migration on staging first:
# Staging
alembic upgrade head

# If successful, run on production
  1. Run migration with zero downtime:
# Option 1: Blue-green deployment
# Deploy new version alongside old, migrate, then switch traffic

# Option 2: Rolling deployment
# Ensure migrations are backward compatible
alembic upgrade head
  1. Rollback if needed:
alembic downgrade -1

SSL/TLS Certificates

# Install certbot
sudo apt-get install certbot python3-certbot-nginx

# Obtain certificates
sudo certbot --nginx -d portal.metricis.app
sudo certbot --nginx -d app.metricis.app
sudo certbot --nginx -d assess.metricis.app
sudo certbot --nginx -d api.metricis.app

# Auto-renewal (certbot sets up a cron job automatically)
sudo certbot renew --dry-run

Monitoring and Logging

Application Logging

Server logs are structured JSON using structlog:

# Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
logger.info("user_login", user_id=user.id, ip_address=request.client.host)

Log Aggregation

Use a log aggregation service:

  • Datadog - Full observability platform
  • Sentry - Error tracking and performance monitoring
  • CloudWatch (AWS) - AWS-native logging
  • Google Cloud Logging (GCP) - GCP-native logging

Health Checks

# Server health
curl https://api.metricis.app/api/health

# Database health
curl https://api.metricis.app/api/health/db

# Redis health
curl https://api.metricis.app/api/health/redis

Uptime Monitoring

Use an uptime monitoring service: - UptimeRobot - Pingdom - StatusCake

Backup Strategy

Database Backups

# Daily backup script
#!/bin/bash
BACKUP_DIR=/backups
DATE=$(date +%Y%m%d_%H%M%S)
pg_dump -U metricis -h db-host metricis | gzip > $BACKUP_DIR/metricis_$DATE.sql.gz

# Retain last 30 days
find $BACKUP_DIR -name "metricis_*.sql.gz" -mtime +30 -delete

File Backups

If storing uploaded files (consent PDFs, etc.), back up the file storage:

# For S3
aws s3 sync s3://metricis-files /backups/files/

# For local filesystem
rsync -av /var/metricis/files/ /backups/files/

Security Checklist

  • [ ] All secrets in environment variables (not in code)
  • [ ] SSL/TLS certificates configured and auto-renewing
  • [ ] Database connections encrypted
  • [ ] Rate limiting enabled
  • [ ] CORS properly configured (only allow your domains)
  • [ ] Security headers set (CSP, HSTS, X-Frame-Options)
  • [ ] Regular security updates applied
  • [ ] Backups tested and verified
  • [ ] Monitoring and alerting configured
  • [ ] Firewall rules configured
  • [ ] SSH key authentication (no password auth)

Scaling Considerations

Horizontal Scaling

  • Server: Run multiple uvicorn workers or multiple server instances behind a load balancer
  • Celery Workers: Add more workers to handle background tasks
  • Database: Use read replicas for read-heavy workloads
  • Redis: Use Redis Cluster for high availability

Vertical Scaling

  • Increase CPU/RAM for database
  • Increase uvicorn workers: uvicorn app.main:app --workers 4

Troubleshooting

Server won't start

  1. Check environment variables are set
  2. Check database connection: psql $DATABASE_URL
  3. Check Redis connection: redis-cli -u $REDIS_URL ping
  4. Check logs: docker-compose logs server

Frontend not loading

  1. Check VITE_API_URL is set correctly
  2. Check CORS settings on server
  3. Check SSL certificates are valid
  4. Check browser console for errors

Database migration failed

  1. Restore from backup
  2. Check migration file for syntax errors
  3. Manually fix database schema
  4. Re-run migration

Next Steps