Skip to main content
ClinicalPilot can be deployed using Docker for consistent, reproducible deployments across environments.
Docker files are not included in the current repository. This guide shows how to create them from scratch.

Prerequisites

  • Docker 24.0+
  • Docker Compose 2.0+
  • 16GB+ RAM (32GB+ for local LLM)

Creating the Dockerfile

Create Dockerfile in the project root:
# Dockerfile
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    poppler-utils \
    tesseract-ocr \
    libmagic1 \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements first (for layer caching)
COPY requirements.txt .

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Download spaCy model
RUN pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.7.1/en_core_web_lg-3.7.1-py3-none-any.whl

# Copy application code
COPY backend/ /app/backend/
COPY frontend/ /app/frontend/
COPY data/ /app/data/

# Create data directories
RUN mkdir -p /app/data/lancedb /app/data/drugbank

# Expose port
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8000/api/health || exit 1

# Run the application
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Creating docker-compose.yml

Create docker-compose.yml in the project root:
version: '3.9'

services:
  clinicalpilot:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: clinicalpilot
    ports:
      - "8000:8000"
    environment:
      # LLM Configuration
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - OPENAI_MODEL=gpt-4o
      - OPENAI_FAST_MODEL=gpt-4o-mini
      
      # Groq (AI Chat)
      - GROQ_API_KEY=${GROQ_API_KEY}
      - GROQ_MODEL=llama-3.3-70b-versatile
      
      # PubMed
      - NCBI_EMAIL=${NCBI_EMAIL}
      - NCBI_API_KEY=${NCBI_API_KEY}
      
      # Observability
      - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}
      - LANGSMITH_API_KEY=${LANGSMITH_API_KEY}
      - LANGCHAIN_PROJECT=clinicalpilot
      
      # Application Settings
      - LOG_LEVEL=INFO
      - CORS_ORIGINS=["*"]
      - EMERGENCY_TIMEOUT_SEC=5
      - MAX_DEBATE_ROUNDS=3
      
      # Data Paths
      - LANCEDB_PATH=/app/data/lancedb
      - DRUGBANK_CSV_PATH=/app/data/drugbank/drugbank_vocabulary.csv
    
    volumes:
      # Persist LanceDB vector store
      - lancedb_data:/app/data/lancedb
      # Persist DrugBank data
      - drugbank_data:/app/data/drugbank
      # Optional: mount custom documents for RAG
      - ./data/rag_documents:/app/data/rag_documents:ro
    
    restart: unless-stopped
    
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  # Optional: Redis for caching
  redis:
    image: redis:7-alpine
    container_name: clinicalpilot-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  # Optional: Nginx reverse proxy
  nginx:
    image: nginx:alpine
    container_name: clinicalpilot-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - clinicalpilot
    restart: unless-stopped

volumes:
  lancedb_data:
  drugbank_data:
  redis_data:

Environment Variables

Create .env.docker for Docker-specific configuration:
# .env.docker
OPENAI_API_KEY=sk-...
GROQ_API_KEY=gsk_...
NCBI_EMAIL=[email protected]
NCBI_API_KEY=...
LANGCHAIN_TRACING_V2=false
LANGSMITH_API_KEY=
Do not commit .env.docker to Git. Add it to .gitignore.

Building and Running

1

Build the Image

docker-compose build
This takes 5-10 minutes (downloads Python packages, spaCy model).
2

Start the Services

docker-compose --env-file .env.docker up -d
The -d flag runs in detached mode (background).
3

Check Logs

docker-compose logs -f clinicalpilot
Look for:
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000
4

Verify Health

curl http://localhost:8000/api/health
Expected:
{"status": "ok", "version": "1.0.0", "timestamp": "..."}
5

Access the Application

Open http://localhost:8000 in your browser.

Stopping and Removing

# Stop services
docker-compose down

# Stop and remove volumes (WARNING: deletes LanceDB data)
docker-compose down -v

Production Configuration

Multi-Stage Build (Smaller Image)

# Dockerfile.prod
FROM python:3.11-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

FROM python:3.11-slim

RUN apt-get update && apt-get install -y \
    poppler-utils tesseract-ocr libmagic1 curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY backend/ /app/backend/
COPY frontend/ /app/frontend/

ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

HTTPS with Nginx

Create nginx.conf:
events {
    worker_connections 1024;
}

http {
    upstream clinicalpilot {
        server clinicalpilot:8000;
    }

    server {
        listen 80;
        server_name clinicalpilot.example.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name clinicalpilot.example.com;

        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        location / {
            proxy_pass http://clinicalpilot;
            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;
        }

        location /ws/ {
            proxy_pass http://clinicalpilot;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    }
}
Add SSL certificates to ssl/ directory:
mkdir ssl
# Copy your fullchain.pem and privkey.pem to ssl/

Docker Compose with Ollama (Local LLM)

Add Ollama service:
services:
  ollama:
    image: ollama/ollama:latest
    container_name: clinicalpilot-ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    restart: unless-stopped

  clinicalpilot:
    # ... existing config ...
    environment:
      - USE_LOCAL_LLM=true
      - OLLAMA_BASE_URL=http://ollama:11434
      - OLLAMA_MODEL=medgemma2:9b
    depends_on:
      - ollama

volumes:
  ollama_data:
Pull MedGemma model:
docker exec -it clinicalpilot-ollama ollama pull medgemma2:9b

Persistent Data

Docker volumes persist data across container restarts:
VolumePurposeBackup?
lancedb_dataVector storeYes (daily)
drugbank_dataDrug interaction dataYes (weekly)
redis_dataCache (optional)No
ollama_dataLLM modelsNo (re-download if lost)

Backing Up Volumes

# Backup LanceDB
docker run --rm -v lancedb_data:/data -v $(pwd):/backup \
  alpine tar czf /backup/lancedb_backup.tar.gz -C /data .

# Restore LanceDB
docker run --rm -v lancedb_data:/data -v $(pwd):/backup \
  alpine tar xzf /backup/lancedb_backup.tar.gz -C /data

Monitoring with Prometheus

Add Prometheus and Grafana:
services:
  prometheus:
    image: prom/prometheus:latest
    container_name: clinicalpilot-prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: clinicalpilot-grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    restart: unless-stopped

volumes:
  prometheus_data:
  grafana_data:
Create prometheus.yml:
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'clinicalpilot'
    static_configs:
      - targets: ['clinicalpilot:8000']

Troubleshooting

Container Fails to Start

# Check logs
docker-compose logs clinicalpilot

# Check container status
docker ps -a

# Inspect container
docker inspect clinicalpilot

Health Check Failing

# Check health status
docker inspect clinicalpilot | grep -A 10 Health

# Test health endpoint manually
docker exec clinicalpilot curl -f http://localhost:8000/api/health

Out of Memory

Increase Docker memory limit:
# Docker Desktop: Settings → Resources → Memory → 16GB+

# Or in docker-compose.yml:
services:
  clinicalpilot:
    mem_limit: 16g
    mem_reservation: 8g

Volume Permissions

If you see permission errors:
# Fix ownership (use container user ID)
docker run --rm -v lancedb_data:/data alpine chown -R 1000:1000 /data

CI/CD with Docker

GitHub Actions example:
# .github/workflows/docker.yml
name: Docker Build and Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build Docker image
        run: docker build -t clinicalpilot:latest .
      
      - name: Run smoke tests
        run: |
          docker run -d -p 8000:8000 --name test clinicalpilot:latest
          sleep 30
          curl -f http://localhost:8000/api/health
      
      - name: Push to registry
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker tag clinicalpilot:latest myregistry/clinicalpilot:${{ github.sha }}
          docker push myregistry/clinicalpilot:${{ github.sha }}

Next Steps

Production Deployment

Deploy Docker containers to production

HIPAA Compliance

Security and compliance for Docker deployments

Build docs developers (and LLMs) love