-
-
Notifications
You must be signed in to change notification settings - Fork 27
Deployment Guide
Complete guide for deploying MediKeep in production environments.
- Deployment Overview
- Prerequisites
- Docker Deployment (Recommended)
- Environment Variables Reference
- SSL/HTTPS Setup
- Reverse Proxy Configuration
- Database Setup
- Production Deployment Checklist
- Monitoring & Logging
- Backup & Disaster Recovery
- Maintenance
- Troubleshooting
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
└──────────────────┘
- Docker Compose (Recommended) - Easiest, most consistent
- Docker with External Database - Use your own PostgreSQL server
- 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
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
- Docker 24.0 or later
- Docker Compose v2 (not legacy
docker-compose) - PostgreSQL 15+ (included in Docker setup)
- SSL certificates (for HTTPS)
- Port 8000 (or custom APP_PORT) available
- Port 5432 for PostgreSQL (only if external access needed)
- Outbound internet access (for SSO providers, if used)
mkdir medikeep
cd medikeepservices:
# 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# 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
EOFIMPORTANT: Edit the .env file and change:
-
DB_PASSWORDto a strong, unique password -
SECRET_KEYto a secure random string (minimum 32 characters) -
ADMIN_DEFAULT_PASSWORDto 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))"docker compose up -d# Check container status
docker compose ps
# View logs
docker compose logs -f
# Check health
curl http://localhost:8000/healthOpen your browser to: http://localhost:8000
Default credentials (change immediately):
- Username:
admin - Password: Value of
ADMIN_DEFAULT_PASSWORD(default:admin123)
Docker volumes are managed by Docker and have no permission issues:
volumes:
- app_uploads:/app/uploads
- app_logs:/app/logs
- app_backups:/app/backupsBackup 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 /dataFor easier access to files from the host:
volumes:
- ./uploads:/app/uploads
- ./logs:/app/logs
- ./backups:/app/backupsImportant: 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 -gSee BIND_MOUNT_PERMISSIONS.md for detailed troubleshooting.
To build your own image instead of using the pre-built one:
medikeep-app:
build:
context: .
dockerfile: docker/Dockerfile
# ... rest of configurationThen build and start:
docker compose build
docker compose up -d# 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-appDatabase migrations run automatically on container startup.
Complete reference of all configuration options from app/core/config.py.
| 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) |
| 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| 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| 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| 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| 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| 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| 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| 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| 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| 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=trueIn Docker, logrotate is automatically configured. See Log Rotation section.
| 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| 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/LondonMediKeep 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 |
- 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- 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.txtThe secrets are processed in two layers:
-
Shell entrypoint resolves
_FILEvars before Python starts (critical forDATABASE_URLwhich is needed at import time) -
Python helper (
app/core/secrets.py) handles any remaining_FILElookups within the application
# 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 ..Edit docker-compose.yml:
volumes:
- app_uploads:/app/uploads
- app_logs:/app/logs
- app_backups:/app/backups
- ./certs:/app/certs:ro # Add this lineUpdate .env:
ENABLE_SSL=truedocker compose down
docker compose up -dAccess via: https://localhost:8000
Note: Browsers will show a security warning for self-signed certificates. Click "Advanced" and "Proceed to localhost".
For production, use Let's Encrypt with a reverse proxy (recommended) or certbot directly.
Use Nginx, Caddy, or Traefik to handle SSL termination. See Reverse Proxy Configuration.
# 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.keySet 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"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# Check certificate
openssl s_client -connect localhost:8000 -showcerts
# Test with curl
curl -k https://localhost:8000/health
# Check in browser
# Visit: https://localhost:8000Using a reverse proxy is recommended for production to handle SSL termination and additional security.
# /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;
}
}# /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# /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 apache2services:
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 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 CaddyfileThe included docker-compose.yml already configures PostgreSQL. No additional setup needed.
If using an existing PostgreSQL server:
-- 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;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 variablesFor 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 = 4GBRestart PostgreSQL after changes:
sudo systemctl restart postgresqlMigrations 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# 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.gzSee Backup & Disaster Recovery for application-level backups.
Use this checklist before going live:
- 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
5432mapping) - Reviewed SSO configuration (if enabled)
- Set up firewall rules
- Configured fail2ban or similar (optional)
- Configured automated backups
- Tested backup restoration process
- Set appropriate
BACKUP_RETENTION_DAYS - Configured
TRASH_RETENTION_DAYSfor file recovery - Backups stored on separate server/service
- Documented recovery procedures
- 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)
- Application and security logs reviewed regularly
- Health check endpoint monitored (
/health) - Disk space monitored (uploads, backups, logs)
- Database connection verified
- Log retention policy defined
- 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
- 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
- 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
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)
# 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.logMediKeep includes automatic log rotation using logrotate (in Docker) or Python rotation.
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/medikeepForce rotation (testing):
docker exec medikeep-app logrotate -f /etc/logrotate.d/medikeepSet in .env:
LOG_ROTATION_METHOD=python
LOG_ROTATION_SIZE=10M
LOG_ROTATION_TIME=daily
LOG_ROTATION_BACKUP_COUNT=30
LOG_COMPRESSION=trueSee LOG_ROTATION.md for details.
curl http://localhost:8000/healthResponse:
{
"status": "healthy",
"version": "0.33.1",
"database": "connected"
}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'#!/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_diskAdd to crontab:
*/5 * * * * /path/to/monitor.shMediKeep includes a comprehensive backup system accessible via:
- Admin Dashboard (Web UI)
- Backup CLI (Command-line for automation)
- Database Only - PostgreSQL dump
- Files Only - Uploaded photos, lab results
- Full Backup - Database + Files
See README_BACKUP_CLI.md for complete documentation.
# 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"# List all backups
docker exec medikeep-app restore list
# List only database backups
docker exec medikeep-app restore list database# 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_1430Important: The restore process automatically creates a safety backup before restoring.
# 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()"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=50Backups 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 .Mount external storage for backups:
volumes:
- /mnt/nas/medikeep-backups:/app/backupsOr 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"- Restore Infrastructure:
# Install Docker
curl -fsSL https://get.docker.com | sh
# Create medikeep directory
mkdir -p /opt/medikeep
cd /opt/medikeep- Restore Configuration:
# Copy docker-compose.yml and .env from backups
# Or recreate from documentation- 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- Start Services:
docker compose up -d- 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>- Identify corruption: Check logs
-
Stop services:
docker compose down - Restore from last known good backup
- Verify data integrity
-
Restart services:
docker compose up -d
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 loginDocument results and update procedures as needed.
# Pull latest image
docker compose pull
# Restart with new image
docker compose up -d
# Verify update
docker compose logs -f medikeep-appDatabase migrations run automatically.
For major version changes:
- Read release notes for breaking changes
-
Create full backup:
docker exec medikeep-app backup_full "Pre-upgrade backup v0.33.1 to v1.0.0"
- Update image tag in docker-compose.yml (if pinned)
-
Pull and restart:
docker compose pull docker compose up -d
-
Monitor logs for migration errors:
docker compose logs -f medikeep-app
-
Test critical functions:
- Login
- Patient record access
- File uploads
- Backup creation
# 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;"# Reindex database (quarterly)
docker exec medikeep-db psql -U medapp -d medical_records -c "REINDEX DATABASE medical_records;"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# 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 -aAutomatic 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"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-appIf 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 adminThe 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
--passwordnon-interactively). The new user is forced to change their password at first login. -
User already has admin role → exits
0with "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 --forceAudit 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.
Symptoms: Container exits immediately
Diagnosis:
# Check logs
docker compose logs medikeep-app
# Check container status
docker compose psCommon Causes:
-
Database connection failure:
# Check database is running docker compose ps postgres # Test connection docker exec medikeep-db pg_isready -U medapp
-
Missing environment variables:
# Verify .env file exists cat .env # Check required variables docker compose config
-
Port already in use:
# Check what's using port 8000 sudo lsof -i :8000 # Change APP_PORT in .env
Error: could not connect to server: Connection refused
Solutions:
-
Check database is healthy:
docker compose ps postgres docker compose logs postgres
-
Verify credentials:
# Test connection docker exec medikeep-db psql -U medapp -d medical_records -c "SELECT 1;"
-
Check network:
# Verify containers are on same network docker network ls docker network inspect medikeep_medikeep-network -
Reset database:
docker compose down -v # WARNING: Deletes all data docker compose up -d
Error: HTTPS enabled but certificates not found
Solutions:
-
Verify certificate files:
# Check files exist ls -la certs/ # Should show: localhost.crt, localhost.key
-
Check volume mount:
# Verify mount in docker-compose.yml docker compose config | grep certs # Should show: - ./certs:/app/certs:ro
-
Check inside container:
docker exec medikeep-app ls -la /app/certs/ -
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.
Error: Permission denied: /app/uploads
Solutions:
-
Use Docker volumes (recommended):
volumes: - app_uploads:/app/uploads # Not ./uploads
-
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.
Error: alembic.util.exc.CommandError: Can't locate revision identified by
Solutions:
-
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
-
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
-
Reset migrations (WARNING: loses data):
docker compose down docker volume rm medikeep_postgres_data docker compose up -d
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:
-
Increase container resources:
deploy: resources: limits: cpus: '2' memory: 4G reservations: cpus: '1' memory: 2G
-
Tune PostgreSQL (see Database Performance Tuning)
-
Enable caching with reverse proxy
-
Check disk I/O:
docker exec medikeep-db iostat -x 1
Diagnosis:
docker stats medikeep-appSolutions:
- Reduce workers (if customized)
- Adjust database connections
- Enable log rotation
-
Clear old backups:
docker exec medikeep-app ls -lh /app/backups
# 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 errorEnable detailed logging temporarily:
# .env
LOG_LEVEL=DEBUG
DEBUG=trueRestart:
docker compose restart medikeep-appRemember: Disable debug mode in production after troubleshooting.
If issues persist:
-
Check existing issues: https://github.com/afairgiant/MediKeep/issues
-
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
-
Create GitHub issue with:
- MediKeep version
- Docker version
- docker-compose.yml (sanitized)
- Error logs
- Steps to reproduce
- Main README: README.md
- Backup CLI Guide: README_BACKUP_CLI.md
- SSO Setup: SSO_SETUP_GUIDE.md
- SSO Quick Start: SSO_QUICK_START.md
- Bind Mount Permissions: BIND_MOUNT_PERMISSIONS.md
- Log Rotation: LOG_ROTATION.md
- HTTPS Setup: README_HTTPS.md
Last Updated: 2026-02-20 MediKeep Version: 0.53.0
Resources