Files
cairnquire/docs/specs/deployment-guide.md

6.3 KiB

Deployment Guide

Requirements

  • Docker 24.0+
  • Docker Compose 2.20+
  • 1GB RAM minimum (2GB recommended)
  • 10GB disk space
  • TLS certificate (Let's Encrypt or custom)

Quick Start

# Clone repository
git clone https://github.com/your-org/md-hub-secure.git
cd md-hub-secure

# Copy and edit configuration
cp .env.example .env
# Edit .env with your domain, email, Postmark API key

# Start services
docker-compose up -d

# Verify health
curl https://your-domain.com/health

Configuration

Environment Variables

Variable Required Default Description
DOMAIN Yes Public domain name
DB_PATH No /data/db.sqlite Database file path
FILESTORE_PATH No /data/files Content-addressed file storage
NOTES_PATH No /notes Watched markdown directory
POSTMARK_API_KEY Yes Postmark server API token
POSTMARK_FROM No notifications@DOMAIN From email address
SESSION_SECRET Yes 32-byte hex for session signing
ENV No production development or production

Docker Compose

version: "3.8"

services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ./data:/data
      - ./notes:/notes:ro
    environment:
      - DOMAIN=${DOMAIN}
      - DB_PATH=/data/db.sqlite
      - FILESTORE_PATH=/data/files
      - NOTES_PATH=/notes
      - POSTMARK_API_KEY=${POSTMARK_API_KEY}
      - SESSION_SECRET=${SESSION_SECRET}
    env_file:
      - .env
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=100m

  # Optional: Traefik for auto-HTTPS
  traefik:
    image: traefik:v3.0
    command:
      - "--api.insecure=false"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    labels:
      - "traefik.enable=true"
    restart: unless-stopped

Production Checklist

Pre-Deployment

  • Change default SESSION_SECRET (generate: openssl rand -hex 32)
  • Configure POSTMARK_API_KEY with valid token
  • Set DOMAIN to public hostname
  • Configure DNS A/AAAA records
  • Set up SPF and DKIM for email domain
  • Review .env file — no default secrets

Security

  • TLS 1.3 enforced (Traefik or reverse proxy)
  • HSTS header configured
  • Firewall rules: only 80/443 open
  • Fail2ban or rate limiting on SSH
  • Automatic security updates enabled
  • Backup encryption key stored securely

Monitoring

  • Health check endpoint accessible
  • Log rotation configured
  • Disk space alerting (>80%)
  • Memory usage alerting
  • Uptime monitoring (external)

Backup Procedures

Automated Backup

#!/bin/bash
# backup.sh

BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/md-hub-$TIMESTAMP.tar.gz"

# Create backup
tar czf "$BACKUP_FILE" \
  -C /data \
  db.sqlite \
  files/

# Encrypt (optional)
gpg --symmetric --cipher-algo AES256 "$BACKUP_FILE"
rm "$BACKUP_FILE"

# Keep only last 30 days
find "$BACKUP_DIR" -name "md-hub-*.tar.gz.gpg" -mtime +30 -delete

Cron: 0 2 * * * /opt/md-hub-secure/backup.sh

Restore from Backup

# Stop application
docker-compose stop app

# Extract backup
tar xzf md-hub-20240115_020000.tar.gz -C /data

# Restart
docker-compose up -d app

# Verify
curl https://your-domain.com/health

Upgrade Procedures

Minor Update (patch version)

docker-compose pull
docker-compose up -d

Major Update

  1. Read release notes for breaking changes
  2. Create backup: ./backup.sh
  3. Pull new image: docker-compose pull
  4. Run database migrations (auto on startup)
  5. Update configuration if needed
  6. Deploy: docker-compose up -d
  7. Verify health and functionality
  8. Rollback if issues: restore from backup

Troubleshooting

Application Won't Start

# Check logs
docker-compose logs -f app

# Verify configuration
docker-compose exec app /server -check-config

# Test database
docker-compose exec app sqlite3 /data/db.sqlite ".tables"

Sync Not Working

# Check file watcher
docker-compose exec app ls -la /notes

# Verify WebSocket
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
  -H "Sec-WebSocket-Version: 13" \
  https://your-domain.com/ws

Email Not Sending

# Test Postmark connection
curl -X POST \
  https://api.postmarkapp.com/server \
  -H "X-Postmark-Server-Token: YOUR_TOKEN" \
  -H "Accept: application/json"

# Check email queue
docker-compose exec app sqlite3 /data/db.sqlite \
  "SELECT * FROM email_queue WHERE status='failed'"

Scaling

Vertical Scaling

  • Increase container memory/CPU limits
  • SQLite WAL mode handles concurrent reads well
  • For heavy write loads, consider libsql replication

Horizontal Scaling (Advanced)

# Requires shared storage and load balancer
services:
  app1:
    # ... same config ...
    environment:
      - DB_PATH=/shared/db.sqlite
      - FILESTORE_PATH=/shared/files
  
  app2:
    # ... same config ...
    environment:
      - DB_PATH=/shared/db.sqlite
      - FILESTORE_PATH=/shared/files
  
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "80:80"

Note: Horizontal scaling requires:

  • Shared filesystem (NFS, EFS)
  • libsql replication or external database
  • Sticky sessions for WebSocket

Support