273 lines
6.3 KiB
Markdown
273 lines
6.3 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# Clone repository
|
|
git clone https://github.com/your-org/cairnquire.git
|
|
cd cairnquire
|
|
|
|
# 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
|
|
|
|
```yaml
|
|
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
|
|
|
|
```bash
|
|
#!/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/cairnquire/backup.sh`
|
|
|
|
### Restore from Backup
|
|
|
|
```bash
|
|
# 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)
|
|
```bash
|
|
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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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)
|
|
```yaml
|
|
# 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
|
|
|
|
- **Documentation**: `/docs` route (when authenticated)
|
|
- **Health Check**: `GET /health`
|
|
- **Metrics**: `GET /metrics` (Prometheus format)
|
|
- **Logs**: `docker-compose logs -f app`
|
|
- **Issues**: https://github.com/your-org/cairnquire/issues
|