Skip to content

Deployment Guide

github-actions[bot] edited this page Apr 11, 2026 · 7 revisions

MediKeep Deployment Guide

Complete guide for deploying MediKeep in production environments.

Table of Contents

  1. Deployment Overview
  2. Prerequisites
  3. Docker Deployment (Recommended)
  4. Environment Variables Reference
  5. SSL/HTTPS Setup
  6. Reverse Proxy Configuration
  7. Database Setup
  8. Production Deployment Checklist
  9. Monitoring & Logging
  10. Backup & Disaster Recovery
  11. Maintenance
  12. Troubleshooting

Deployment Overview

Architecture

MediKeep uses a multi-stage Docker build that combines:

  • Frontend: React application (built with Node.js 20)
  • Backend: FastAPI (Python 3.12)
  • Database: PostgreSQL 15.8
  • File Storage: Local filesystem with volume mounts
┌──────────────────┐
│   Reverse Proxy  │ (Optional: Nginx, Caddy, Traefik)
│   SSL/TLS Term   │
└────────┬─────────┘
         │
┌────────▼─────────┐
│   MediKeep App   │ Port 8000 (HTTP/HTTPS)
│ Frontend+Backend │
└────────┬─────────┘
         │
┌────────▼─────────┐
│   PostgreSQL 15  │ Port 5432
└──────────────────┘

Deployment Options

  1. Docker Compose (Recommended) - Easiest, most consistent
  2. Docker with External Database - Use your own PostgreSQL server

Security Considerations

  • All passwords and secrets MUST be changed from defaults
  • HTTPS is strongly recommended for production
  • Database should not be exposed externally
  • Regular backups are essential for medical data
  • Access logs must be monitored for suspicious activity

Prerequisites

System Requirements

Minimum:

  • 2 CPU cores
  • 2 GB RAM
  • 20 GB disk space
  • Docker 24.0+ and Docker Compose v2

Recommended:

  • 4 CPU cores
  • 4 GB RAM
  • 100 GB disk space (for medical records and backups)
  • SSD storage for database
  • Regular backup strategy

Software Requirements

  • Docker 24.0 or later
  • Docker Compose v2 (not legacy docker-compose)
  • PostgreSQL 15+ (included in Docker setup)
  • SSL certificates (for HTTPS)

Network Requirements

  • Port 8000 (or custom APP_PORT) available
  • Port 5432 for PostgreSQL (only if external access needed)
  • Outbound internet access (for SSO providers, if used)

Docker Deployment (Recommended)

Quick Start (5 Minutes)

1. Create Project Directory

mkdir medikeep
cd medikeep

2. Create docker-compose.yml

services:
  # PostgreSQL Database Service
  postgres:
    image: postgres:15.8-alpine
    container_name: medikeep-db
    environment:
      POSTGRES_DB: ${DB_NAME:-medical_records}
      POSTGRES_USER: ${DB_USER:-medapp}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - '5432:5432' # Remove this line in production (internal only)
    healthcheck:
      test:
        [
          'CMD-SHELL',
          'pg_isready -U ${DB_USER:-medapp} -d ${DB_NAME:-medical_records}',
        ]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - medikeep-network

  # Combined Frontend + Backend Application Service
  medikeep-app:
    image: ghcr.io/afairgiant/medikeep:latest
    container_name: medikeep-app
    ports:
      - ${APP_PORT:-8000}:8000
    environment:
      # Database Configuration
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: ${DB_NAME:-medical_records}
      DB_USER: ${DB_USER:-medapp}
      DB_PASSWORD: ${DB_PASSWORD}

      # Security
      SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env for persistent JWTs}
      ADMIN_DEFAULT_PASSWORD: ${ADMIN_DEFAULT_PASSWORD:-admin123}

      # Application Settings
      TZ: ${TZ:-America/New_York}
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
      DEBUG: ${DEBUG:-false}
      ENABLE_API_DOCS: ${ENABLE_API_DOCS:-false}

      # SSL Configuration (optional)
      ENABLE_SSL: ${ENABLE_SSL:-false}

      # SSO Configuration (optional)
      SSO_ENABLED: ${SSO_ENABLED:-false}

      # Optional: Enable for bind mounts
      #PUID: ${PUID}
      #PGID: ${PGID}

    volumes:
      - app_uploads:/app/uploads
      - app_logs:/app/logs
      - app_backups:/app/backups
      # Uncomment for HTTPS:
      # - ./certs:/app/certs:ro
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8000/health']
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped
    networks:
      - medikeep-network

# Named volumes for data persistence
volumes:
  postgres_data:
    driver: local
  app_uploads:
    driver: local
  app_logs:
    driver: local
  app_backups:
    driver: local

# Network for service communication
networks:
  medikeep-network:
    driver: bridge

3. Create .env File

# Create .env file with your configuration
cat > .env << 'EOF'
# Database Configuration
DB_NAME=medical_records
DB_USER=medapp
DB_PASSWORD=your-very-secure-database-password-here

# Application Port
APP_PORT=8000

# Security - REQUIRED (auto-generates ephemeral key if not set)
SECRET_KEY=your-very-secure-secret-key-min-32-chars-for-jwt-tokens

# Default Admin Password (only affects fresh installations)
ADMIN_DEFAULT_PASSWORD=your-secure-admin-password

# Timezone
TZ=America/New_York

# Logging
LOG_LEVEL=INFO
DEBUG=false
ENABLE_API_DOCS=false  # Set to true to expose Swagger docs

# SSL (set to true after setting up certificates)
ENABLE_SSL=false

# SSO (optional)
SSO_ENABLED=false
EOF

IMPORTANT: Edit the .env file and change:

  • DB_PASSWORD to a strong, unique password
  • SECRET_KEY to a secure random string (minimum 32 characters)
  • ADMIN_DEFAULT_PASSWORD to a secure password

Generate secure keys:

# Generate SECRET_KEY (Linux/Mac)
openssl rand -hex 32

# Or use Python
python -c "import secrets; print(secrets.token_urlsafe(32))"

4. Start the Application

docker compose up -d

5. Verify Deployment

# Check container status
docker compose ps

# View logs
docker compose logs -f

# Check health
curl http://localhost:8000/health

6. Access the Application

Open your browser to: http://localhost:8000

Default credentials (change immediately):

  • Username: admin
  • Password: Value of ADMIN_DEFAULT_PASSWORD (default: admin123)

Docker Volume Management

Using Docker Volumes (Recommended)

Docker volumes are managed by Docker and have no permission issues:

volumes:
  - app_uploads:/app/uploads
  - app_logs:/app/logs
  - app_backups:/app/backups

Backup volumes:

# Backup a volume
docker run --rm -v medikeep_app_backups:/data -v $(pwd):/backup alpine tar czf /backup/backups.tar.gz -C /data .

# Restore a volume
docker run --rm -v medikeep_app_backups:/data -v $(pwd):/backup alpine tar xzf /backup/backups.tar.gz -C /data

Using Bind Mounts

For easier access to files from the host:

volumes:
  - ./uploads:/app/uploads
  - ./logs:/app/logs
  - ./backups:/app/backups

Important: Fix permissions first:

# Create directories
mkdir -p uploads logs backups

# Set ownership (use your user ID)
sudo chown -R 1000:1000 uploads logs backups

# Or use PUID/PGID in docker-compose.yml:
environment:
  PUID: 1000  # Your user ID: id -u
  PGID: 1000  # Your group ID: id -g

See BIND_MOUNT_PERMISSIONS.md for detailed troubleshooting.

Building from Source

To build your own image instead of using the pre-built one:

medikeep-app:
  build:
    context: .
    dockerfile: docker/Dockerfile
  # ... rest of configuration

Then build and start:

docker compose build
docker compose up -d

Updating the Application

# Pull latest image
docker compose pull

# Recreate containers with new image
docker compose up -d

# Check logs for migration status
docker compose logs -f medikeep-app

Database migrations run automatically on container startup.

Environment Variables Reference

Complete reference of all configuration options from app/core/config.py.

Core Application Settings

Variable Type Default Description
APP_NAME string MediKeep Application name (hardcoded)
VERSION string 0.33.1 Application version (hardcoded)
DEBUG boolean true Enable debug mode (set to false in production)

Database Configuration

Variable Type Default Required Description
DB_HOST string localhost Yes PostgreSQL host
DB_PORT integer 5432 No PostgreSQL port
DB_NAME string - Yes Database name
DB_USER string - Yes Database user
DB_PASSWORD string - Yes Database password
DATABASE_URL string Auto-generated No Full connection string (overrides individual settings)

Example:

DB_HOST=postgres
DB_PORT=5432
DB_NAME=medical_records
DB_USER=medapp
DB_PASSWORD=secure-password-here

Security Configuration

Variable Type Default Required Description
SECRET_KEY string (auto-generated) Recommended JWT signing key (min 32 chars). Auto-generates ephemeral key if not set; JWTs and encrypted configs won't survive restarts without it.
ALGORITHM string HS256 No JWT algorithm (hardcoded)
ACCESS_TOKEN_EXPIRE_MINUTES integer 480 No JWT token expiration (8 hours)
ADMIN_DEFAULT_PASSWORD string admin123 No Default admin password for fresh installs
ALLOW_USER_REGISTRATION boolean true No Allow new user registration
DEBUG boolean false No Enable debug mode
ENABLE_API_DOCS boolean false No Expose OpenAPI/Swagger docs at /api/v1/openapi.json
CORS_ALLOWED_ORIGINS string http://localhost:3000 No Comma-separated list of allowed CORS origins

Example:

SECRET_KEY=c7f9a8b2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4z5
ACCESS_TOKEN_EXPIRE_MINUTES=480
ADMIN_DEFAULT_PASSWORD=MySecurePassword123!
ALLOW_USER_REGISTRATION=true

SSL/HTTPS Configuration

Variable Type Default Required Description
ENABLE_SSL boolean false No Enable HTTPS
SSL_CERTFILE string /app/certs/localhost.crt If SSL enabled Path to SSL certificate
SSL_KEYFILE string /app/certs/localhost.key If SSL enabled Path to SSL private key

Example:

ENABLE_SSL=true
SSL_CERTFILE=/app/certs/medikeep.crt
SSL_KEYFILE=/app/certs/medikeep.key

File Storage Configuration

Variable Type Default Description
UPLOAD_DIR path ./uploads Upload directory path
MAX_FILE_SIZE integer 10485760 Max file size in bytes (10MB)

Example:

UPLOAD_DIR=/app/uploads
MAX_FILE_SIZE=15728640  # 15MB

Backup Configuration

Variable Type Default Description
BACKUP_DIR path ./backups Backup directory path
BACKUP_RETENTION_DAYS integer 7 Days to keep backups
BACKUP_MIN_COUNT integer 5 Minimum backups to always keep
BACKUP_MAX_COUNT integer 50 Warning threshold for backups

Example:

BACKUP_DIR=/app/backups
BACKUP_RETENTION_DAYS=30
BACKUP_MIN_COUNT=10
BACKUP_MAX_COUNT=100

Trash/Soft Delete Configuration

Variable Type Default Description
TRASH_DIR path ./uploads/trash Trash directory for deleted files
TRASH_RETENTION_DAYS integer 30 Days to keep deleted files

Example:

TRASH_DIR=/app/uploads/trash
TRASH_RETENTION_DAYS=60

SSO Configuration

Variable Type Default Required Description
SSO_ENABLED boolean false No Enable SSO authentication
SSO_PROVIDER_TYPE string oidc If SSO enabled Provider: google, github, oidc, authentik, authelia, keycloak
SSO_CLIENT_ID string - If SSO enabled OAuth client ID
SSO_CLIENT_SECRET string - If SSO enabled OAuth client secret
SSO_ISSUER_URL string - For OIDC OIDC issuer URL
SSO_REDIRECT_URI string - If SSO enabled OAuth redirect URI
SSO_ALLOWED_DOMAINS JSON array [] No Allowed email domains
SSO_RATE_LIMIT_ATTEMPTS integer 10 No Max SSO attempts per window
SSO_RATE_LIMIT_WINDOW_MINUTES integer 10 No Rate limit window

Example (Google):

SSO_ENABLED=true
SSO_PROVIDER_TYPE=google
SSO_CLIENT_ID=your-client-id.apps.googleusercontent.com
SSO_CLIENT_SECRET=your-client-secret
SSO_REDIRECT_URI=https://medikeep.example.com/auth/sso/callback
SSO_ALLOWED_DOMAINS=["example.com"]

Example (OIDC/Keycloak):

SSO_ENABLED=true
SSO_PROVIDER_TYPE=keycloak
SSO_CLIENT_ID=medikeep
SSO_CLIENT_SECRET=your-secret
SSO_ISSUER_URL=https://keycloak.example.com/realms/master
SSO_REDIRECT_URI=https://medikeep.example.com/auth/sso/callback

Paperless-ngx Integration

Variable Type Default Description
PAPERLESS_REQUEST_TIMEOUT integer 30 API request timeout (seconds)
PAPERLESS_CONNECT_TIMEOUT integer 10 Connection timeout (seconds)
PAPERLESS_UPLOAD_TIMEOUT integer 300 Upload timeout (5 minutes)
PAPERLESS_PROCESSING_TIMEOUT integer 1800 Max processing time (30 minutes)
PAPERLESS_STATUS_CHECK_INTERVAL integer 10 Status check interval (seconds)
PAPERLESS_MAX_UPLOAD_SIZE integer 52428800 Max upload size (50MB)
PAPERLESS_RETRY_ATTEMPTS integer 3 Number of retry attempts
PAPERLESS_SALT string paperless_integration_salt_v1 Encryption salt

Example:

PAPERLESS_REQUEST_TIMEOUT=60
PAPERLESS_UPLOAD_TIMEOUT=600
PAPERLESS_MAX_UPLOAD_SIZE=104857600  # 100MB

Logging Configuration

Variable Type Default Description
LOG_LEVEL string INFO Log level: DEBUG, INFO, WARNING, ERROR
LOG_DIR string ./logs Log directory path
LOG_RETENTION_DAYS integer 180 Days to keep logs
ENABLE_DEBUG_LOGS boolean false Enable debug logging

Example:

LOG_LEVEL=INFO
LOG_DIR=/app/logs
LOG_RETENTION_DAYS=365

Log Rotation Configuration

Variable Type Default Description
LOG_ROTATION_METHOD string auto Method: auto, python, logrotate
LOG_ROTATION_SIZE string 5M Size threshold (e.g., 5M, 10M, 1G)
LOG_ROTATION_TIME string daily Time interval: daily, weekly, monthly
LOG_ROTATION_BACKUP_COUNT integer 30 Number of rotated files to keep
LOG_COMPRESSION boolean true Compress rotated logs

Example:

LOG_ROTATION_METHOD=logrotate
LOG_ROTATION_SIZE=10M
LOG_ROTATION_TIME=daily
LOG_ROTATION_BACKUP_COUNT=60
LOG_COMPRESSION=true

In Docker, logrotate is automatically configured. See Log Rotation section.

Database Sequence Monitoring

Variable Type Default Description
ENABLE_SEQUENCE_MONITORING boolean true Enable sequence monitoring
SEQUENCE_CHECK_ON_STARTUP boolean true Check sequences at startup
SEQUENCE_AUTO_FIX boolean true Auto-fix sequence issues
SEQUENCE_MONITOR_INTERVAL_HOURS integer 24 Monitoring interval

Example:

ENABLE_SEQUENCE_MONITORING=true
SEQUENCE_CHECK_ON_STARTUP=true
SEQUENCE_AUTO_FIX=true

Docker-Specific Variables

Variable Type Default Description
PUID integer 1000 User ID for file permissions (bind mounts)
PGID integer 1000 Group ID for file permissions (bind mounts)
TZ string America/New_York Container timezone

Example:

PUID=1000
PGID=1000
TZ=Europe/London

Docker Secrets (_FILE Pattern)

MediKeep supports the Docker _FILE convention used by the official PostgreSQL image. Instead of passing secrets as plain environment variables, you can point to a file containing the secret value.

How it works: For any supported variable (e.g., DB_PASSWORD), set DB_PASSWORD_FILE=/run/secrets/db_password and MediKeep will read the secret from that file at startup.

Precedence: If both VAR and VAR_FILE are set, the direct VAR value wins and a warning is logged.

_FILE Variable Corresponding Variable
DB_USER_FILE DB_USER
DB_PASSWORD_FILE DB_PASSWORD
DATABASE_URL_FILE DATABASE_URL
SECRET_KEY_FILE SECRET_KEY
ADMIN_DEFAULT_PASSWORD_FILE ADMIN_DEFAULT_PASSWORD
SSO_CLIENT_ID_FILE SSO_CLIENT_ID
SSO_CLIENT_SECRET_FILE SSO_CLIENT_SECRET
PAPERLESS_SALT_FILE PAPERLESS_SALT
NOTIFICATION_ENCRYPTION_SALT_FILE NOTIFICATION_ENCRYPTION_SALT

Example: Docker Compose with File-Based Secrets

  1. Create secret files:
mkdir -p secrets
echo -n "my-database-password" > secrets/db_password.txt
echo -n "my-jwt-secret-key-min-32-chars-long" > secrets/secret_key.txt
chmod 600 secrets/*.txt
  1. Update docker-compose.yml:
services:
  postgres:
    image: postgres:15.8-alpine
    environment:
      POSTGRES_DB: medical_records
      POSTGRES_USER: medapp
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

  medikeep-app:
    image: ghcr.io/afairgiant/medikeep:latest
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: medical_records
      DB_USER: medapp
      DB_PASSWORD_FILE: /run/secrets/db_password
      SECRET_KEY_FILE: /run/secrets/secret_key
    secrets:
      - db_password
      - secret_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  secret_key:
    file: ./secrets/secret_key.txt

The secrets are processed in two layers:

  • Shell entrypoint resolves _FILE vars before Python starts (critical for DATABASE_URL which is needed at import time)
  • Python helper (app/core/secrets.py) handles any remaining _FILE lookups within the application

SSL/HTTPS Setup

Using Self-Signed Certificates (Development/Testing)

1. Generate Certificates

# Create certificates directory
mkdir certs
cd certs

# Generate self-signed certificate (valid for 1 year)
openssl req -x509 -newkey rsa:2048 \
  -keyout localhost.key \
  -out localhost.crt \
  -days 365 \
  -nodes \
  -subj "/CN=localhost"

cd ..

2. Mount Certificates in Docker

Edit docker-compose.yml:

volumes:
  - app_uploads:/app/uploads
  - app_logs:/app/logs
  - app_backups:/app/backups
  - ./certs:/app/certs:ro # Add this line

3. Enable SSL

Update .env:

ENABLE_SSL=true

4. Restart Container

docker compose down
docker compose up -d

Access via: https://localhost:8000

Note: Browsers will show a security warning for self-signed certificates. Click "Advanced" and "Proceed to localhost".

Using Let's Encrypt (Production)

For production, use Let's Encrypt with a reverse proxy (recommended) or certbot directly.

Option 1: With Reverse Proxy (Recommended)

Use Nginx, Caddy, or Traefik to handle SSL termination. See Reverse Proxy Configuration.

Option 2: Direct with Certbot

# Install certbot
sudo apt-get install certbot

# Generate certificate
sudo certbot certonly --standalone -d medikeep.example.com

# Copy certificates
sudo cp /etc/letsencrypt/live/medikeep.example.com/fullchain.pem ./certs/medikeep.crt
sudo cp /etc/letsencrypt/live/medikeep.example.com/privkey.pem ./certs/medikeep.key
sudo chown $(id -u):$(id -g) ./certs/*

# Update .env
ENABLE_SSL=true
SSL_CERTFILE=/app/certs/medikeep.crt
SSL_KEYFILE=/app/certs/medikeep.key

Set up auto-renewal:

# Add to crontab
0 3 * * * certbot renew --post-hook "cp /etc/letsencrypt/live/medikeep.example.com/*.pem /path/to/certs/ && docker compose restart medikeep-app"

Using Custom Certificates

If you have certificates from your organization:

# Copy certificates to certs directory
cp your-cert.crt certs/medikeep.crt
cp your-key.key certs/medikeep.key

# Update .env
ENABLE_SSL=true
SSL_CERTFILE=/app/certs/medikeep.crt
SSL_KEYFILE=/app/certs/medikeep.key

Testing HTTPS

# Check certificate
openssl s_client -connect localhost:8000 -showcerts

# Test with curl
curl -k https://localhost:8000/health

# Check in browser
# Visit: https://localhost:8000

Reverse Proxy Configuration

Using a reverse proxy is recommended for production to handle SSL termination and additional security.

Nginx Configuration

Basic HTTP

# /etc/nginx/sites-available/medikeep
server {
    listen 80;
    server_name medikeep.example.com;

    # Increase timeouts for large uploads
    client_max_body_size 100M;
    client_body_timeout 300s;

    location / {
        proxy_pass http://localhost:8000;
        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;

        # Timeouts for long uploads
        proxy_connect_timeout 300s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }

    # Health check endpoint (optional)
    location /health {
        proxy_pass http://localhost:8000/health;
        access_log off;
    }
}

HTTPS with Let's Encrypt

# /etc/nginx/sites-available/medikeep
server {
    listen 80;
    server_name medikeep.example.com;
    return 301 https://$server_name$request_uri;
}

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

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/medikeep.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/medikeep.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # HSTS (optional but recommended)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # File upload limits
    client_max_body_size 100M;
    client_body_timeout 300s;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;

    location / {
        proxy_pass http://localhost:8000;
        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 https;

        # Timeouts
        proxy_connect_timeout 300s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }

    # Static files caching (if serving static directly)
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        proxy_pass http://localhost:8000;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Enable and test:

sudo ln -s /etc/nginx/sites-available/medikeep /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Apache Configuration

# /etc/apache2/sites-available/medikeep.conf
<VirtualHost *:80>
    ServerName medikeep.example.com
    Redirect permanent / https://medikeep.example.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName medikeep.example.com

    # SSL Configuration
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/medikeep.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/medikeep.example.com/privkey.pem

    # Security headers
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"

    # Proxy configuration
    ProxyPreserveHost On
    ProxyPass / http://localhost:8000/
    ProxyPassReverse / http://localhost:8000/

    # Upload limits
    LimitRequestBody 104857600  # 100MB

    # Timeout settings
    ProxyTimeout 300
</VirtualHost>

Enable modules and site:

sudo a2enmod proxy proxy_http ssl headers
sudo a2ensite medikeep
sudo apache2ctl configtest
sudo systemctl reload apache2

Traefik Configuration (Docker Labels)

services:
  medikeep-app:
    image: ghcr.io/afairgiant/medikeep:latest
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.medikeep.rule=Host(`medikeep.example.com`)'
      - 'traefik.http.routers.medikeep.entrypoints=websecure'
      - 'traefik.http.routers.medikeep.tls.certresolver=letsencrypt'
      - 'traefik.http.services.medikeep.loadbalancer.server.port=8000'

      # HTTP to HTTPS redirect
      - 'traefik.http.routers.medikeep-http.rule=Host(`medikeep.example.com`)'
      - 'traefik.http.routers.medikeep-http.entrypoints=web'
      - 'traefik.http.routers.medikeep-http.middlewares=redirect-to-https'
      - 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'

Caddy Configuration

Caddy automatically handles HTTPS with Let's Encrypt:

# Caddyfile
medikeep.example.com {
    reverse_proxy localhost:8000

    # Upload limits
    request_body {
        max_size 100MB
    }

    # Timeouts
    timeouts {
        read 5m
        write 5m
    }
}

Start Caddy:

caddy run --config Caddyfile

Database Setup

Using Docker Compose Database (Recommended)

The included docker-compose.yml already configures PostgreSQL. No additional setup needed.

Using External PostgreSQL

If using an existing PostgreSQL server:

1. Create Database

-- Connect as postgres user
psql -U postgres

-- Create database and user
CREATE DATABASE medical_records;
CREATE USER medapp WITH ENCRYPTED PASSWORD 'secure-password-here';
GRANT ALL PRIVILEGES ON DATABASE medical_records TO medapp;

-- Grant schema permissions
\c medical_records
GRANT ALL ON SCHEMA public TO medapp;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO medapp;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO medapp;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO medapp;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO medapp;

2. Update docker-compose.yml

Remove the postgres service and update app environment:

services:
  medikeep-app:
    image: ghcr.io/afairgiant/medikeep:latest
    environment:
      DB_HOST: your-postgres-host.example.com
      DB_PORT: 5432
      DB_NAME: medical_records
      DB_USER: medapp
      DB_PASSWORD: secure-password-here
      # ... other variables

Database Performance Tuning

For production PostgreSQL, tune these settings in postgresql.conf:

# Connection settings
max_connections = 100
shared_buffers = 256MB
effective_cache_size = 1GB
maintenance_work_mem = 64MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1  # For SSD
effective_io_concurrency = 200  # For SSD
work_mem = 4MB
min_wal_size = 1GB
max_wal_size = 4GB

Restart PostgreSQL after changes:

sudo systemctl restart postgresql

Running Migrations Manually

Migrations run automatically on container startup. To run manually:

# Inside container
docker exec -it medikeep-app python -m alembic -c alembic/alembic.ini upgrade head

# Or from host (if you have the code)
cd /path/to/medikeep
python -m alembic -c alembic/alembic.ini upgrade head

Database Backup (PostgreSQL)

# Manual backup
docker exec medikeep-db pg_dump -U medapp medical_records > backup.sql

# Restore
docker exec -i medikeep-db psql -U medapp medical_records < backup.sql

# Automated backup (cron)
0 2 * * * docker exec medikeep-db pg_dump -U medapp medical_records | gzip > /backups/db_$(date +\%Y\%m\%d).sql.gz

See Backup & Disaster Recovery for application-level backups.

Production Deployment Checklist

Use this checklist before going live:

Security

  • Changed default admin password (ADMIN_DEFAULT_PASSWORD)
  • Set strong SECRET_KEY (minimum 32 random characters)
  • Changed database password (DB_PASSWORD)
  • Set DEBUG=false
  • Enabled HTTPS (ENABLE_SSL=true)
  • Configured SSL certificates (not self-signed)
  • Database not exposed externally (removed port 5432 mapping)
  • Reviewed SSO configuration (if enabled)
  • Set up firewall rules
  • Configured fail2ban or similar (optional)

Data Protection

  • Configured automated backups
  • Tested backup restoration process
  • Set appropriate BACKUP_RETENTION_DAYS
  • Configured TRASH_RETENTION_DAYS for file recovery
  • Backups stored on separate server/service
  • Documented recovery procedures

Performance

  • Database on SSD storage
  • Sufficient disk space allocated (100GB+ recommended)
  • Reverse proxy configured with caching
  • Log rotation enabled and tested
  • Database performance tuned
  • Resource limits set (Docker memory/CPU)

Monitoring

  • Application and security logs reviewed regularly
  • Health check endpoint monitored (/health)
  • Disk space monitored (uploads, backups, logs)
  • Database connection verified
  • Log retention policy defined

Documentation

  • Admin credentials documented securely
  • Backup procedures documented
  • Disaster recovery plan created
  • Environment variables documented
  • SSL certificate renewal process documented
  • Contact information for on-call staff

Testing

  • Tested user registration flow
  • Tested file uploads (photos, lab results)
  • Tested backup creation and restoration
  • Verified HTTPS redirects
  • Tested under load (optional)
  • Verified log rotation
  • Tested on target browsers

Compliance (if applicable)

  • HIPAA compliance reviewed (if handling US PHI)
  • GDPR compliance reviewed (if handling EU data)
  • Data retention policies implemented
  • Access logging enabled
  • Encryption at rest configured (if required)
  • Audit trail reviewed

Monitoring & Logging

Application Logs

Log Locations

All logs are stored in /app/logs/ (or LOG_DIR):

  • app.log - Application logs (API calls, user activity)
  • security.log - Security events (failed logins, access attempts)

Accessing Logs

# Docker Compose
docker compose logs -f medikeep-app

# View application logs
docker exec medikeep-app tail -f /app/logs/app.log

# View security logs
docker exec medikeep-app tail -f /app/logs/security.log

# Search logs
docker exec medikeep-app grep "ERROR" /app/logs/app.log

Log Rotation

MediKeep includes automatic log rotation using logrotate (in Docker) or Python rotation.

Docker (Automatic)

Logrotate is pre-configured in Docker images:

  • Rotates when size exceeds 5MB OR daily (whichever comes first)
  • Keeps 30 rotated files
  • Compresses old logs
  • Configuration: /etc/logrotate.d/medikeep

View rotation config:

docker exec medikeep-app cat /etc/logrotate.d/medikeep

Force rotation (testing):

docker exec medikeep-app logrotate -f /etc/logrotate.d/medikeep

Manual/Development

Set in .env:

LOG_ROTATION_METHOD=python
LOG_ROTATION_SIZE=10M
LOG_ROTATION_TIME=daily
LOG_ROTATION_BACKUP_COUNT=30
LOG_COMPRESSION=true

See LOG_ROTATION.md for details.

Health Monitoring

Health Check Endpoint

curl http://localhost:8000/health

Response:

{
  "status": "healthy",
  "version": "0.33.1",
  "database": "connected"
}

Docker Health Checks

Built into docker-compose.yml:

# Check container health
docker ps
# Look for "healthy" status

# View health check logs
docker inspect medikeep-app | jq '.[0].State.Health'

Basic Health Monitoring Script

#!/bin/bash
# monitor.sh - Basic health monitoring

check_health() {
    response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health)
    if [ "$response" != "200" ]; then
        echo "ALERT: MediKeep health check failed (HTTP $response)"
        # Send alert (email, Slack, PagerDuty, etc.)
    fi
}

check_disk() {
    usage=$(df /var/lib/docker/volumes | tail -1 | awk '{print $5}' | sed 's/%//')
    if [ "$usage" -gt 80 ]; then
        echo "ALERT: Disk usage at ${usage}%"
    fi
}

check_health
check_disk

Add to crontab:

*/5 * * * * /path/to/monitor.sh

Backup & Disaster Recovery

Application Backup System

MediKeep includes a comprehensive backup system accessible via:

  1. Admin Dashboard (Web UI)
  2. Backup CLI (Command-line for automation)

Backup Types

  • Database Only - PostgreSQL dump
  • Files Only - Uploaded photos, lab results
  • Full Backup - Database + Files

Using the Backup CLI

See README_BACKUP_CLI.md for complete documentation.

Creating Backups

# Database backup
docker exec medikeep-app backup_db

# Files backup
docker exec medikeep-app backup_files

# Full system backup (recommended)
docker exec medikeep-app backup_full "Daily automated backup"

Listing Backups

# List all backups
docker exec medikeep-app restore list

# List only database backups
docker exec medikeep-app restore list database

Restoring from Backup

# 1. Preview restore (ALWAYS do this first)
docker exec medikeep-app restore preview 123

# 2. Review warnings and get confirmation code

# 3. Execute restore with confirmation code
docker exec medikeep-app restore restore 123 123_1430

Important: The restore process automatically creates a safety backup before restoring.

Automated Backup Schedule

Using Cron

# Add to host crontab
crontab -e
# Daily database backup at 2 AM
0 2 * * * docker exec medikeep-app backup_db "Daily automated backup" >> /var/log/medikeep-backup.log 2>&1

# Weekly full backup on Sunday at 3 AM
0 3 * * 0 docker exec medikeep-app backup_full "Weekly full backup" >> /var/log/medikeep-backup.log 2>&1

# Cleanup old backups monthly (handled by retention policy)
0 4 1 * * docker exec medikeep-app python -c "from app.services.backup_service import BackupService; BackupService().cleanup_old_backups()"

Backup Retention Policy

Configure in .env:

# Keep backups for 30 days
BACKUP_RETENTION_DAYS=30

# Always keep at least 5 backups
BACKUP_MIN_COUNT=5

# Warn if more than 50 backups
BACKUP_MAX_COUNT=50

Backup Storage

Docker Volumes

Backups are stored in /app/backups volume:

# Backup the backup volume
docker run --rm \
  -v medikeep_app_backups:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/backups-$(date +%Y%m%d).tar.gz -C /data .

External Storage

Mount external storage for backups:

volumes:
  - /mnt/nas/medikeep-backups:/app/backups

Or use cloud storage:

# Sync to S3
aws s3 sync /var/lib/docker/volumes/medikeep_app_backups/_data s3://my-bucket/medikeep-backups/

# Sync to Azure Blob
azcopy sync /var/lib/docker/volumes/medikeep_app_backups/_data "https://account.blob.core.windows.net/backups"

Disaster Recovery Procedures

Complete System Failure

  1. Restore Infrastructure:
# Install Docker
curl -fsSL https://get.docker.com | sh

# Create medikeep directory
mkdir -p /opt/medikeep
cd /opt/medikeep
  1. Restore Configuration:
# Copy docker-compose.yml and .env from backups
# Or recreate from documentation
  1. Restore Data Volumes:
# Restore backup volume
docker volume create medikeep_app_backups
docker run --rm \
  -v medikeep_app_backups:/data \
  -v $(pwd):/backup \
  alpine tar xzf /backup/backups-20250104.tar.gz -C /data

# Create empty volumes
docker volume create medikeep_postgres_data
docker volume create medikeep_app_uploads
docker volume create medikeep_app_logs
  1. Start Services:
docker compose up -d
  1. Restore from Backup:
# List available backups
docker exec medikeep-app restore list

# Preview and restore
docker exec medikeep-app restore preview <backup_id>
docker exec medikeep-app restore restore <backup_id> <confirmation_code>

Data Corruption

  1. Identify corruption: Check logs
  2. Stop services: docker compose down
  3. Restore from last known good backup
  4. Verify data integrity
  5. Restart services: docker compose up -d

Testing Backup/Restore

Test quarterly to ensure backups work:

# 1. Create test backup
docker exec medikeep-app backup_full "Disaster recovery test"

# 2. Note the backup ID
BACKUP_ID=$(docker exec medikeep-app restore list | grep "Disaster recovery test" | awk '{print $1}')

# 3. In a TEST environment, restore
docker exec medikeep-app restore preview $BACKUP_ID
# Review and restore
docker exec medikeep-app restore restore $BACKUP_ID <confirmation_code>

# 4. Verify data integrity
# - Check patient records
# - Verify file uploads
# - Test login

Document results and update procedures as needed.

Maintenance

Updating MediKeep

Minor Updates

# Pull latest image
docker compose pull

# Restart with new image
docker compose up -d

# Verify update
docker compose logs -f medikeep-app

Database migrations run automatically.

Major Updates

For major version changes:

  1. Read release notes for breaking changes
  2. Create full backup:
    docker exec medikeep-app backup_full "Pre-upgrade backup v0.33.1 to v1.0.0"
  3. Update image tag in docker-compose.yml (if pinned)
  4. Pull and restart:
    docker compose pull
    docker compose up -d
  5. Monitor logs for migration errors:
    docker compose logs -f medikeep-app
  6. Test critical functions:
    • Login
    • Patient record access
    • File uploads
    • Backup creation

Database Maintenance

Vacuum and Analyze

# Regular maintenance (weekly)
docker exec medikeep-db psql -U medapp -d medical_records -c "VACUUM ANALYZE;"

# Full vacuum (monthly, during low usage)
docker exec medikeep-db psql -U medapp -d medical_records -c "VACUUM FULL ANALYZE;"

Add to crontab:

0 3 * * 0 docker exec medikeep-db psql -U medapp -d medical_records -c "VACUUM ANALYZE;"

Re-index

# Reindex database (quarterly)
docker exec medikeep-db psql -U medapp -d medical_records -c "REINDEX DATABASE medical_records;"

Log Cleanup

Logs are automatically rotated. Manual cleanup:

# Remove old rotated logs (older than 180 days)
docker exec medikeep-app find /app/logs -name "*.gz" -mtime +180 -delete

# Check log disk usage
docker exec medikeep-app du -sh /app/logs

Docker Cleanup

# Remove unused images
docker image prune -a

# Remove unused volumes (CAUTION)
docker volume prune

# Remove unused networks
docker network prune

# Complete cleanup (excludes volumes)
docker system prune -a

Certificate Renewal

Let's Encrypt (with reverse proxy)

Automatic with Caddy. For Nginx/Apache:

# Test renewal
sudo certbot renew --dry-run

# Add to crontab
0 3 1 * * certbot renew --post-hook "systemctl reload nginx"

Manual Certificates

Replace certificates in certs/ directory and restart:

# Copy new certificates
cp new-cert.crt certs/medikeep.crt
cp new-cert.key certs/medikeep.key

# Restart container
docker compose restart medikeep-app

Troubleshooting

Lost Admin Access Recovery

If your admin account was demoted, deleted, or you never had one on a fresh install, MediKeep ships with an emergency recovery script that works against the database directly. This does not require a working admin session — it runs inside the container with database credentials only.

One-line recovery for the default admin account:

docker exec -it <container_name> python app/scripts/create_emergency_admin.py --username admin

The script auto-detects whether the target user exists:

  • User exists and is not admin → promotes them to role='admin' in place, preserving their password, email, and other fields. The user logs in immediately with their existing credentials.
  • User does not exist → creates a new admin user. Prompts interactively for a password (or pass --password non-interactively). The new user is forced to change their password at first login.
  • User already has admin role → exits 0 with "nothing to do" (safe no-op, not an error).

Other scenarios:

# Explicitly promote a non-default user when other admins are present
docker exec -it medikeep-app \
  python app/scripts/create_emergency_admin.py --username alice --promote

# Create an additional admin user when admins already exist (non-emergency)
docker exec -it medikeep-app \
  python app/scripts/create_emergency_admin.py --username extra --force

Audit trail: every successful promotion or creation writes to both logs/security.log (event emergency_admin_promoted or emergency_admin_created) and the activity_logs table with metadata.source='emergency_admin_script', so the action shows up in the admin activity log UI after recovery.

Startup self-check: MediKeep logs a WARNING-level security event on every startup when it detects zero admin users in a non-empty database. The log message includes the exact recovery command. Check logs/security.log for event admin_user_demoted_no_other_admins or no_admin_users_detected if you are unsure whether you are in a recoverable lockout state.

Full documentation of all flags, scenarios, exit codes, and troubleshooting: app/scripts/README_EMERGENCY_ADMIN.md.

Common Deployment Issues

Container Won't Start

Symptoms: Container exits immediately

Diagnosis:

# Check logs
docker compose logs medikeep-app

# Check container status
docker compose ps

Common Causes:

  1. Database connection failure:

    # Check database is running
    docker compose ps postgres
    
    # Test connection
    docker exec medikeep-db pg_isready -U medapp
  2. Missing environment variables:

    # Verify .env file exists
    cat .env
    
    # Check required variables
    docker compose config
  3. Port already in use:

    # Check what's using port 8000
    sudo lsof -i :8000
    
    # Change APP_PORT in .env

Database Connection Problems

Error: could not connect to server: Connection refused

Solutions:

  1. Check database is healthy:

    docker compose ps postgres
    docker compose logs postgres
  2. Verify credentials:

    # Test connection
    docker exec medikeep-db psql -U medapp -d medical_records -c "SELECT 1;"
  3. Check network:

    # Verify containers are on same network
    docker network ls
    docker network inspect medikeep_medikeep-network
  4. Reset database:

    docker compose down -v  # WARNING: Deletes all data
    docker compose up -d

SSL/HTTPS Issues

Error: HTTPS enabled but certificates not found

Solutions:

  1. Verify certificate files:

    # Check files exist
    ls -la certs/
    
    # Should show: localhost.crt, localhost.key
  2. Check volume mount:

    # Verify mount in docker-compose.yml
    docker compose config | grep certs
    
    # Should show: - ./certs:/app/certs:ro
  3. Check inside container:

    docker exec medikeep-app ls -la /app/certs/
  4. Regenerate certificates:

    cd certs
    openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -days 365 -nodes -subj "/CN=localhost"
    docker compose restart medikeep-app

Error: Browser shows "NET::ERR_CERT_AUTHORITY_INVALID"

Solution: This is normal for self-signed certificates. Click "Advanced" → "Proceed to localhost". For production, use proper CA-signed certificates.

Permission Denied Errors

Error: Permission denied: /app/uploads

Solutions:

  1. Use Docker volumes (recommended):

    volumes:
      - app_uploads:/app/uploads # Not ./uploads
  2. Fix bind mount permissions:

    # Set ownership
    sudo chown -R 1000:1000 uploads logs backups
    
    # Or use PUID/PGID
    PUID=1000 PGID=1000 docker compose up -d

See BIND_MOUNT_PERMISSIONS.md for complete guide.

Migration Failures

Error: alembic.util.exc.CommandError: Can't locate revision identified by

Solutions:

  1. Check migration status:

    docker exec medikeep-app python -m alembic -c alembic/alembic.ini current
    docker exec medikeep-app python -m alembic -c alembic/alembic.ini history
  2. Force to head (if database is empty):

    docker exec medikeep-app python -m alembic -c alembic/alembic.ini stamp head
    docker exec medikeep-app python -m alembic -c alembic/alembic.ini upgrade head
  3. Reset migrations (WARNING: loses data):

    docker compose down
    docker volume rm medikeep_postgres_data
    docker compose up -d

Performance Problems

Slow Application Response

Diagnosis:

# Check container resources
docker stats medikeep-app

# Check database performance
docker exec medikeep-db psql -U medapp -d medical_records -c "
SELECT pid, age(clock_timestamp(), query_start), usename, query
FROM pg_stat_activity
WHERE query != '<IDLE>' AND query NOT ILIKE '%pg_stat_activity%'
ORDER BY query_start desc;
"

Solutions:

  1. Increase container resources:

    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 4G
        reservations:
          cpus: '1'
          memory: 2G
  2. Tune PostgreSQL (see Database Performance Tuning)

  3. Enable caching with reverse proxy

  4. Check disk I/O:

    docker exec medikeep-db iostat -x 1

High Memory Usage

Diagnosis:

docker stats medikeep-app

Solutions:

  1. Reduce workers (if customized)
  2. Adjust database connections
  3. Enable log rotation
  4. Clear old backups:
    docker exec medikeep-app ls -lh /app/backups

Log Analysis

Finding Errors

# Recent errors
docker exec medikeep-app grep -i error /app/logs/app.log | tail -20

# Failed logins
docker exec medikeep-app grep "failed login" /app/logs/security.log

# Database errors
docker compose logs postgres | grep -i error

Debug Mode

Enable detailed logging temporarily:

# .env
LOG_LEVEL=DEBUG
DEBUG=true

Restart:

docker compose restart medikeep-app

Remember: Disable debug mode in production after troubleshooting.

Getting Help

If issues persist:

  1. Check existing issues: https://github.com/afairgiant/MediKeep/issues

  2. Collect diagnostic information:

    # System info
    docker version
    docker compose version
    
    # Container status
    docker compose ps
    docker compose logs > medikeep-logs.txt
    
    # Configuration (remove secrets!)
    docker compose config
  3. Create GitHub issue with:

    • MediKeep version
    • Docker version
    • docker-compose.yml (sanitized)
    • Error logs
    • Steps to reproduce

Additional Resources


Last Updated: 2026-02-20 MediKeep Version: 0.53.0

Clone this wiki locally