feat(deploy): merge Phase 5a deployment configuration
Complete containerized deployment system with Docker/Podman support. Key features: - Multi-stage Dockerfile with Python 3.11-slim base - Docker Compose configurations for production and development - Nginx reverse proxy with security headers and rate limiting - Systemd service units for Docker, Podman, and docker-compose - Backup/restore scripts with integrity verification - Podman compatibility (ADR-009) All tests pass including Podman verification testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
848
deployment/README.md
Normal file
848
deployment/README.md
Normal file
@@ -0,0 +1,848 @@
|
||||
# Gondulf Deployment Guide
|
||||
|
||||
This guide covers deploying Gondulf IndieAuth Server using OCI-compliant containers with both **Podman** (recommended) and **Docker** (alternative).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Quick Start](#quick-start)
|
||||
2. [Container Engine Support](#container-engine-support)
|
||||
3. [Prerequisites](#prerequisites)
|
||||
4. [Building the Container Image](#building-the-container-image)
|
||||
5. [Development Deployment](#development-deployment)
|
||||
6. [Production Deployment](#production-deployment)
|
||||
7. [Backup and Restore](#backup-and-restore)
|
||||
8. [systemd Integration](#systemd-integration)
|
||||
9. [Troubleshooting](#troubleshooting)
|
||||
10. [Security Considerations](#security-considerations)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Podman (Rootless - Recommended)
|
||||
|
||||
```bash
|
||||
# 1. Clone and configure
|
||||
git clone https://github.com/yourusername/gondulf.git
|
||||
cd gondulf
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
|
||||
# 2. Build image
|
||||
podman build -t gondulf:latest .
|
||||
|
||||
# 3. Run container
|
||||
podman run -d --name gondulf \
|
||||
-p 8000:8000 \
|
||||
-v gondulf_data:/data:Z \
|
||||
--env-file .env \
|
||||
gondulf:latest
|
||||
|
||||
# 4. Verify health
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
### Docker (Alternative)
|
||||
|
||||
```bash
|
||||
# 1. Clone and configure
|
||||
git clone https://github.com/yourusername/gondulf.git
|
||||
cd gondulf
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
|
||||
# 2. Build and run with compose
|
||||
docker-compose up -d
|
||||
|
||||
# 3. Verify health
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
## Container Engine Support
|
||||
|
||||
Gondulf supports both Podman and Docker with identical functionality.
|
||||
|
||||
### Podman (Primary)
|
||||
|
||||
**Advantages**:
|
||||
- Daemonless architecture (no background process)
|
||||
- Rootless mode for enhanced security
|
||||
- Native systemd integration
|
||||
- Pod support for multi-container applications
|
||||
- OCI-compliant
|
||||
|
||||
**Recommended for**: Production deployments, security-focused environments
|
||||
|
||||
### Docker (Alternative)
|
||||
|
||||
**Advantages**:
|
||||
- Wide ecosystem and tooling support
|
||||
- Familiar to most developers
|
||||
- Extensive documentation
|
||||
|
||||
**Recommended for**: Development, existing Docker environments
|
||||
|
||||
### Compatibility Matrix
|
||||
|
||||
| Feature | Podman | Docker |
|
||||
|---------|--------|--------|
|
||||
| Container build | ✅ | ✅ |
|
||||
| Container runtime | ✅ | ✅ |
|
||||
| Compose files | ✅ (podman-compose) | ✅ (docker-compose) |
|
||||
| Rootless mode | ✅ Native | ⚠️ Experimental |
|
||||
| systemd integration | ✅ Built-in | ⚠️ Manual |
|
||||
| Health checks | ✅ | ✅ |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Operating System**: Linux (recommended), macOS, Windows (WSL2)
|
||||
- **CPU**: 1 core minimum, 2+ cores recommended
|
||||
- **RAM**: 512 MB minimum, 1 GB+ recommended
|
||||
- **Disk**: 5 GB available space
|
||||
|
||||
### Container Engine
|
||||
|
||||
Choose ONE:
|
||||
|
||||
**Option 1: Podman** (Recommended)
|
||||
|
||||
```bash
|
||||
# Fedora/RHEL/CentOS
|
||||
sudo dnf install podman podman-compose
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install podman podman-compose
|
||||
|
||||
# Verify installation
|
||||
podman --version
|
||||
podman-compose --version
|
||||
```
|
||||
|
||||
**Option 2: Docker**
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install docker.io docker-compose
|
||||
|
||||
# Or install from Docker's repository:
|
||||
# https://docs.docker.com/engine/install/
|
||||
|
||||
# Verify installation
|
||||
docker --version
|
||||
docker-compose --version
|
||||
```
|
||||
|
||||
### Rootless Podman Setup (Recommended)
|
||||
|
||||
For enhanced security, configure rootless Podman:
|
||||
|
||||
```bash
|
||||
# 1. Check subuid/subgid configuration
|
||||
grep $USER /etc/subuid
|
||||
grep $USER /etc/subgid
|
||||
|
||||
# Should show: username:100000:65536 (or similar)
|
||||
# If missing, run:
|
||||
sudo usermod --add-subuids 100000-165535 $USER
|
||||
sudo usermod --add-subgids 100000-165535 $USER
|
||||
|
||||
# 2. Enable user lingering (services persist after logout)
|
||||
loginctl enable-linger $USER
|
||||
|
||||
# 3. Verify rootless setup
|
||||
podman system info | grep rootless
|
||||
# Should show: runRoot: /run/user/1000/...
|
||||
```
|
||||
|
||||
## Building the Container Image
|
||||
|
||||
### Using Podman
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
podman build -t gondulf:latest .
|
||||
|
||||
# Verify build
|
||||
podman images | grep gondulf
|
||||
|
||||
# Test run
|
||||
podman run --rm gondulf:latest python -m gondulf --version
|
||||
```
|
||||
|
||||
### Using Docker
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t gondulf:latest .
|
||||
|
||||
# Verify build
|
||||
docker images | grep gondulf
|
||||
|
||||
# Test run
|
||||
docker run --rm gondulf:latest python -m gondulf --version
|
||||
```
|
||||
|
||||
### Build Arguments
|
||||
|
||||
The Dockerfile supports multi-stage builds that include testing:
|
||||
|
||||
```bash
|
||||
# Build with tests (default)
|
||||
podman build -t gondulf:latest .
|
||||
|
||||
# If build fails, tests have failed - check build output
|
||||
```
|
||||
|
||||
## Development Deployment
|
||||
|
||||
Development deployment includes:
|
||||
- Live code reload
|
||||
- MailHog for local email testing
|
||||
- Debug logging enabled
|
||||
- No TLS requirements
|
||||
|
||||
### Using Podman Compose
|
||||
|
||||
```bash
|
||||
# Start development environment
|
||||
podman-compose -f docker-compose.yml -f docker-compose.development.yml up
|
||||
|
||||
# Access services:
|
||||
# - Gondulf: http://localhost:8000
|
||||
# - MailHog UI: http://localhost:8025
|
||||
|
||||
# View logs
|
||||
podman-compose logs -f gondulf
|
||||
|
||||
# Stop environment
|
||||
podman-compose down
|
||||
```
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
```bash
|
||||
# Start development environment
|
||||
docker-compose -f docker-compose.yml -f docker-compose.development.yml up
|
||||
|
||||
# Access services:
|
||||
# - Gondulf: http://localhost:8000
|
||||
# - MailHog UI: http://localhost:8025
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f gondulf
|
||||
|
||||
# Stop environment
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Development Configuration
|
||||
|
||||
Create `.env` file from `.env.example`:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with development settings:
|
||||
|
||||
```env
|
||||
GONDULF_SECRET_KEY=dev-secret-key-minimum-32-characters
|
||||
GONDULF_BASE_URL=http://localhost:8000
|
||||
GONDULF_DATABASE_URL=sqlite:///./data/gondulf.db
|
||||
GONDULF_SMTP_HOST=mailhog
|
||||
GONDULF_SMTP_PORT=1025
|
||||
GONDULF_SMTP_USE_TLS=false
|
||||
GONDULF_DEBUG=true
|
||||
GONDULF_LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
Production deployment includes:
|
||||
- nginx reverse proxy with TLS termination
|
||||
- Rate limiting and security headers
|
||||
- Persistent volume for database
|
||||
- Health checks and auto-restart
|
||||
- Proper logging configuration
|
||||
|
||||
### Step 1: Configuration
|
||||
|
||||
```bash
|
||||
# 1. Copy environment template
|
||||
cp .env.example .env
|
||||
|
||||
# 2. Generate secret key
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
|
||||
# 3. Edit .env with your production settings
|
||||
nano .env
|
||||
```
|
||||
|
||||
Production `.env` example:
|
||||
|
||||
```env
|
||||
GONDULF_SECRET_KEY=<generated-secret-key-from-step-2>
|
||||
GONDULF_BASE_URL=https://auth.example.com
|
||||
GONDULF_DATABASE_URL=sqlite:////data/gondulf.db
|
||||
GONDULF_SMTP_HOST=smtp.sendgrid.net
|
||||
GONDULF_SMTP_PORT=587
|
||||
GONDULF_SMTP_USERNAME=apikey
|
||||
GONDULF_SMTP_PASSWORD=<your-sendgrid-api-key>
|
||||
GONDULF_SMTP_FROM=noreply@example.com
|
||||
GONDULF_SMTP_USE_TLS=true
|
||||
GONDULF_HTTPS_REDIRECT=true
|
||||
GONDULF_TRUST_PROXY=true
|
||||
GONDULF_SECURE_COOKIES=true
|
||||
GONDULF_DEBUG=false
|
||||
GONDULF_LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
### Step 2: TLS Certificates
|
||||
|
||||
Obtain TLS certificates (Let's Encrypt recommended):
|
||||
|
||||
```bash
|
||||
# Create SSL directory
|
||||
mkdir -p deployment/nginx/ssl
|
||||
|
||||
# Option 1: Let's Encrypt (recommended)
|
||||
sudo certbot certonly --standalone -d auth.example.com
|
||||
sudo cp /etc/letsencrypt/live/auth.example.com/fullchain.pem deployment/nginx/ssl/
|
||||
sudo cp /etc/letsencrypt/live/auth.example.com/privkey.pem deployment/nginx/ssl/
|
||||
|
||||
# Option 2: Self-signed (development/testing only)
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout deployment/nginx/ssl/privkey.pem \
|
||||
-out deployment/nginx/ssl/fullchain.pem
|
||||
|
||||
# Secure permissions
|
||||
chmod 600 deployment/nginx/ssl/privkey.pem
|
||||
chmod 644 deployment/nginx/ssl/fullchain.pem
|
||||
```
|
||||
|
||||
### Step 3: nginx Configuration
|
||||
|
||||
Edit `deployment/nginx/conf.d/gondulf.conf`:
|
||||
|
||||
```nginx
|
||||
# Change server_name to your domain
|
||||
server_name auth.example.com; # ← CHANGE THIS
|
||||
```
|
||||
|
||||
### Step 4: Deploy with Podman (Recommended)
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
podman build -t gondulf:latest .
|
||||
|
||||
# Start services
|
||||
podman-compose -f docker-compose.yml -f docker-compose.production.yml up -d
|
||||
|
||||
# Verify health
|
||||
curl https://auth.example.com/health
|
||||
|
||||
# View logs
|
||||
podman-compose logs -f
|
||||
```
|
||||
|
||||
### Step 5: Deploy with Docker (Alternative)
|
||||
|
||||
```bash
|
||||
# Build and start
|
||||
docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d
|
||||
|
||||
# Verify health
|
||||
curl https://auth.example.com/health
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Step 6: Verify Deployment
|
||||
|
||||
```bash
|
||||
# 1. Check health endpoint
|
||||
curl https://auth.example.com/health
|
||||
# Expected: {"status":"healthy","database":"connected"}
|
||||
|
||||
# 2. Check OAuth metadata
|
||||
curl https://auth.example.com/.well-known/oauth-authorization-server | jq
|
||||
# Expected: JSON with issuer, authorization_endpoint, token_endpoint
|
||||
|
||||
# 3. Verify HTTPS redirect
|
||||
curl -I http://auth.example.com/
|
||||
# Expected: 301 redirect to HTTPS
|
||||
|
||||
# 4. Check security headers
|
||||
curl -I https://auth.example.com/ | grep -E "(Strict-Transport|X-Frame|X-Content)"
|
||||
# Expected: HSTS, X-Frame-Options, X-Content-Type-Options headers
|
||||
|
||||
# 5. Test TLS configuration
|
||||
# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=auth.example.com
|
||||
# Target: Grade A or higher
|
||||
```
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### Automated Backups
|
||||
|
||||
The backup scripts auto-detect Podman or Docker.
|
||||
|
||||
#### Create Backup
|
||||
|
||||
```bash
|
||||
# Using included script (works with both Podman and Docker)
|
||||
./deployment/scripts/backup.sh
|
||||
|
||||
# Or with custom backup directory
|
||||
./deployment/scripts/backup.sh /path/to/backups
|
||||
|
||||
# Or using compose (Podman)
|
||||
podman-compose --profile backup run --rm backup
|
||||
|
||||
# Or using compose (Docker)
|
||||
docker-compose --profile backup run --rm backup
|
||||
```
|
||||
|
||||
Backup details:
|
||||
- Uses SQLite `VACUUM INTO` for safe hot backups
|
||||
- No downtime required
|
||||
- Automatic compression (gzip)
|
||||
- Integrity verification
|
||||
- Automatic cleanup of old backups (default: 7 days retention)
|
||||
|
||||
#### Scheduled Backups with cron
|
||||
|
||||
```bash
|
||||
# Create cron job for daily backups at 2 AM
|
||||
crontab -e
|
||||
|
||||
# Add this line:
|
||||
0 2 * * * cd /path/to/gondulf && ./deployment/scripts/backup.sh >> /var/log/gondulf-backup.log 2>&1
|
||||
```
|
||||
|
||||
### Restore from Backup
|
||||
|
||||
**CAUTION**: This will replace the current database!
|
||||
|
||||
```bash
|
||||
# Restore from backup
|
||||
./deployment/scripts/restore.sh /path/to/backups/gondulf_backup_20251120_120000.db.gz
|
||||
|
||||
# The script will:
|
||||
# 1. Stop the container (if running)
|
||||
# 2. Create a safety backup of current database
|
||||
# 3. Restore from the specified backup
|
||||
# 4. Verify integrity
|
||||
# 5. Restart the container (if it was running)
|
||||
```
|
||||
|
||||
### Test Backup/Restore
|
||||
|
||||
```bash
|
||||
# Run automated backup/restore tests
|
||||
./deployment/scripts/test-backup-restore.sh
|
||||
|
||||
# This verifies:
|
||||
# - Backup creation
|
||||
# - Backup integrity
|
||||
# - Database structure
|
||||
# - Compression
|
||||
# - Queryability
|
||||
```
|
||||
|
||||
## systemd Integration
|
||||
|
||||
### Rootless Podman (Recommended)
|
||||
|
||||
**Method 1: Podman-Generated Unit** (Recommended)
|
||||
|
||||
```bash
|
||||
# 1. Start container normally first
|
||||
podman run -d --name gondulf \
|
||||
-p 8000:8000 \
|
||||
-v gondulf_data:/data:Z \
|
||||
--env-file /home/$USER/gondulf/.env \
|
||||
gondulf:latest
|
||||
|
||||
# 2. Generate systemd unit file
|
||||
cd ~/.config/systemd/user/
|
||||
podman generate systemd --new --files --name gondulf
|
||||
|
||||
# 3. Stop the manually-started container
|
||||
podman stop gondulf
|
||||
podman rm gondulf
|
||||
|
||||
# 4. Enable and start service
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now container-gondulf.service
|
||||
|
||||
# 5. Enable lingering (service runs without login)
|
||||
loginctl enable-linger $USER
|
||||
|
||||
# 6. Verify status
|
||||
systemctl --user status container-gondulf
|
||||
```
|
||||
|
||||
**Method 2: Custom Unit File**
|
||||
|
||||
```bash
|
||||
# 1. Copy unit file
|
||||
mkdir -p ~/.config/systemd/user/
|
||||
cp deployment/systemd/gondulf-podman.service ~/.config/systemd/user/gondulf.service
|
||||
|
||||
# 2. Edit paths if needed
|
||||
nano ~/.config/systemd/user/gondulf.service
|
||||
|
||||
# 3. Reload and enable
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now gondulf.service
|
||||
loginctl enable-linger $USER
|
||||
|
||||
# 4. Verify status
|
||||
systemctl --user status gondulf
|
||||
```
|
||||
|
||||
**systemd User Service Commands**:
|
||||
|
||||
```bash
|
||||
# Start service
|
||||
systemctl --user start gondulf
|
||||
|
||||
# Stop service
|
||||
systemctl --user stop gondulf
|
||||
|
||||
# Restart service
|
||||
systemctl --user restart gondulf
|
||||
|
||||
# Check status
|
||||
systemctl --user status gondulf
|
||||
|
||||
# View logs
|
||||
journalctl --user -u gondulf -f
|
||||
|
||||
# Disable service
|
||||
systemctl --user disable gondulf
|
||||
```
|
||||
|
||||
### Docker (System Service)
|
||||
|
||||
```bash
|
||||
# 1. Copy unit file
|
||||
sudo cp deployment/systemd/gondulf-docker.service /etc/systemd/system/gondulf.service
|
||||
|
||||
# 2. Edit paths in the file
|
||||
sudo nano /etc/systemd/system/gondulf.service
|
||||
# Change WorkingDirectory to your installation path
|
||||
|
||||
# 3. Reload and enable
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now gondulf.service
|
||||
|
||||
# 4. Verify status
|
||||
sudo systemctl status gondulf
|
||||
```
|
||||
|
||||
**systemd System Service Commands**:
|
||||
|
||||
```bash
|
||||
# Start service
|
||||
sudo systemctl start gondulf
|
||||
|
||||
# Stop service
|
||||
sudo systemctl stop gondulf
|
||||
|
||||
# Restart service
|
||||
sudo systemctl restart gondulf
|
||||
|
||||
# Check status
|
||||
sudo systemctl status gondulf
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u gondulf -f
|
||||
|
||||
# Disable service
|
||||
sudo systemctl disable gondulf
|
||||
```
|
||||
|
||||
### Compose-Based systemd Service
|
||||
|
||||
For deploying with docker-compose or podman-compose:
|
||||
|
||||
```bash
|
||||
# For Podman (rootless):
|
||||
cp deployment/systemd/gondulf-compose.service ~/.config/systemd/user/gondulf.service
|
||||
# Edit to use podman-compose
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now gondulf.service
|
||||
|
||||
# For Docker (rootful):
|
||||
sudo cp deployment/systemd/gondulf-compose.service /etc/systemd/system/gondulf.service
|
||||
# Edit to use docker-compose and add docker.service dependency
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now gondulf.service
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container Won't Start
|
||||
|
||||
**Check logs**:
|
||||
|
||||
```bash
|
||||
# Podman
|
||||
podman logs gondulf
|
||||
# or
|
||||
podman-compose logs gondulf
|
||||
|
||||
# Docker
|
||||
docker logs gondulf
|
||||
# or
|
||||
docker-compose logs gondulf
|
||||
```
|
||||
|
||||
**Common issues**:
|
||||
|
||||
1. **Missing SECRET_KEY**:
|
||||
```
|
||||
ERROR: GONDULF_SECRET_KEY is required
|
||||
```
|
||||
Solution: Set `GONDULF_SECRET_KEY` in `.env` (minimum 32 characters)
|
||||
|
||||
2. **Missing BASE_URL**:
|
||||
```
|
||||
ERROR: GONDULF_BASE_URL is required
|
||||
```
|
||||
Solution: Set `GONDULF_BASE_URL` in `.env`
|
||||
|
||||
3. **Port already in use**:
|
||||
```
|
||||
Error: bind: address already in use
|
||||
```
|
||||
Solution:
|
||||
```bash
|
||||
# Check what's using port 8000
|
||||
sudo ss -tlnp | grep 8000
|
||||
|
||||
# Use different port
|
||||
podman run -p 8001:8000 ...
|
||||
```
|
||||
|
||||
### Database Issues
|
||||
|
||||
**Check database file**:
|
||||
|
||||
```bash
|
||||
# Podman
|
||||
podman exec gondulf ls -la /data/
|
||||
|
||||
# Docker
|
||||
docker exec gondulf ls -la /data/
|
||||
```
|
||||
|
||||
**Check database integrity**:
|
||||
|
||||
```bash
|
||||
# Podman
|
||||
podman exec gondulf sqlite3 /data/gondulf.db "PRAGMA integrity_check;"
|
||||
|
||||
# Docker
|
||||
docker exec gondulf sqlite3 /data/gondulf.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
**Expected output**: `ok`
|
||||
|
||||
### Permission Errors (Rootless Podman)
|
||||
|
||||
If you see permission errors with volumes:
|
||||
|
||||
```bash
|
||||
# 1. Check subuid/subgid configuration
|
||||
grep $USER /etc/subuid
|
||||
grep $USER /etc/subgid
|
||||
|
||||
# 2. Add if missing
|
||||
sudo usermod --add-subuids 100000-165535 $USER
|
||||
sudo usermod --add-subgids 100000-165535 $USER
|
||||
|
||||
# 3. Restart user services
|
||||
systemctl --user daemon-reload
|
||||
|
||||
# 4. Use :Z label for SELinux systems
|
||||
podman run -v ./data:/data:Z ...
|
||||
```
|
||||
|
||||
### SELinux Issues
|
||||
|
||||
On SELinux-enabled systems (RHEL, Fedora, CentOS):
|
||||
|
||||
```bash
|
||||
# Check for SELinux denials
|
||||
sudo ausearch -m AVC -ts recent
|
||||
|
||||
# Solution 1: Add :Z label to volumes (recommended)
|
||||
podman run -v gondulf_data:/data:Z ...
|
||||
|
||||
# Solution 2: Temporarily permissive (testing only)
|
||||
sudo setenforce 0
|
||||
|
||||
# Solution 3: Create SELinux policy (advanced)
|
||||
# Use audit2allow to generate policy from denials
|
||||
```
|
||||
|
||||
### Email Not Sending
|
||||
|
||||
**Check SMTP configuration**:
|
||||
|
||||
```bash
|
||||
# Test SMTP connection from container
|
||||
podman exec gondulf sh -c "timeout 5 bash -c '</dev/tcp/smtp.example.com/587' && echo 'Port open' || echo 'Port closed'"
|
||||
|
||||
# Check logs for SMTP errors
|
||||
podman logs gondulf | grep -i smtp
|
||||
```
|
||||
|
||||
**Common SMTP issues**:
|
||||
|
||||
1. **Authentication failure**: Verify username/password (use app-specific password for Gmail)
|
||||
2. **TLS error**: Check `GONDULF_SMTP_USE_TLS` matches port (587=STARTTLS, 465=TLS, 25=none)
|
||||
3. **Firewall**: Ensure outbound connections allowed on SMTP port
|
||||
|
||||
### Health Check Failing
|
||||
|
||||
```bash
|
||||
# Check health status
|
||||
podman inspect gondulf --format='{{.State.Health.Status}}'
|
||||
|
||||
# View health check logs
|
||||
podman inspect gondulf --format='{{range .State.Health.Log}}{{.Output}}{{end}}'
|
||||
|
||||
# Test health endpoint manually
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
### nginx Issues
|
||||
|
||||
**Test nginx configuration**:
|
||||
|
||||
```bash
|
||||
# Podman
|
||||
podman exec gondulf_nginx nginx -t
|
||||
|
||||
# Docker
|
||||
docker exec gondulf_nginx nginx -t
|
||||
```
|
||||
|
||||
**Check nginx logs**:
|
||||
|
||||
```bash
|
||||
# Podman
|
||||
podman logs gondulf_nginx
|
||||
|
||||
# Docker
|
||||
docker logs gondulf_nginx
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Container Security (Rootless Podman)
|
||||
|
||||
Rootless Podman provides defense-in-depth:
|
||||
|
||||
- No root daemon
|
||||
- User namespace isolation
|
||||
- UID mapping (container UID 1000 → host subuid range)
|
||||
- Limited attack surface
|
||||
|
||||
### TLS/HTTPS Requirements
|
||||
|
||||
IndieAuth **requires HTTPS in production**:
|
||||
|
||||
- Obtain valid TLS certificate (Let's Encrypt recommended)
|
||||
- Configure nginx for TLS termination
|
||||
- Enable HSTS headers
|
||||
- Use strong ciphers (TLS 1.2+)
|
||||
|
||||
### Secrets Management
|
||||
|
||||
**Never commit secrets to version control**:
|
||||
|
||||
```bash
|
||||
# Verify .env is gitignored
|
||||
git check-ignore .env
|
||||
# Should output: .env
|
||||
|
||||
# Ensure .env has restrictive permissions
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
**Production secrets best practices**:
|
||||
- Use strong SECRET_KEY (32+ characters)
|
||||
- Use app-specific passwords for email (Gmail, etc.)
|
||||
- Rotate secrets regularly
|
||||
- Consider secrets management tools (Vault, AWS Secrets Manager)
|
||||
|
||||
### Network Security
|
||||
|
||||
**Firewall configuration**:
|
||||
|
||||
```bash
|
||||
# Allow HTTPS (443)
|
||||
sudo ufw allow 443/tcp
|
||||
|
||||
# Allow HTTP (80) for Let's Encrypt challenges and redirects
|
||||
sudo ufw allow 80/tcp
|
||||
|
||||
# Block direct access to container port (8000)
|
||||
# Don't expose port 8000 externally in production
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
nginx configuration includes rate limiting:
|
||||
- Authorization endpoint: 10 req/s (burst 20)
|
||||
- Token endpoint: 20 req/s (burst 40)
|
||||
- General endpoints: 30 req/s (burst 60)
|
||||
|
||||
Adjust in `deployment/nginx/conf.d/gondulf.conf` as needed.
|
||||
|
||||
### Security Headers
|
||||
|
||||
The following security headers are automatically set:
|
||||
- `Strict-Transport-Security` (HSTS)
|
||||
- `X-Frame-Options: DENY`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `X-XSS-Protection: 1; mode=block`
|
||||
- `Referrer-Policy`
|
||||
- Content-Security-Policy (set by application)
|
||||
|
||||
### Regular Security Updates
|
||||
|
||||
```bash
|
||||
# Update base image
|
||||
podman pull python:3.12-slim-bookworm
|
||||
|
||||
# Rebuild container
|
||||
podman build -t gondulf:latest .
|
||||
|
||||
# Recreate container
|
||||
podman stop gondulf
|
||||
podman rm gondulf
|
||||
podman run -d --name gondulf ...
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Gondulf Documentation](../docs/)
|
||||
- [Podman Documentation](https://docs.podman.io/)
|
||||
- [Docker Documentation](https://docs.docker.com/)
|
||||
- [W3C IndieAuth Specification](https://www.w3.org/TR/indieauth/)
|
||||
- [Let's Encrypt](https://letsencrypt.org/)
|
||||
- [Rootless Containers](https://rootlesscontaine.rs/)
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub Issues: https://github.com/yourusername/gondulf/issues
|
||||
- Documentation: https://github.com/yourusername/gondulf/docs
|
||||
- Security: security@yourdomain.com
|
||||
41
deployment/docker/entrypoint.sh
Executable file
41
deployment/docker/entrypoint.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
# Gondulf Container Entrypoint Script
|
||||
# Handles runtime initialization for both Podman and Docker
|
||||
|
||||
set -e
|
||||
|
||||
echo "Gondulf IndieAuth Server - Starting..."
|
||||
|
||||
# Ensure data directory exists with correct permissions
|
||||
if [ ! -d "/data" ]; then
|
||||
echo "Creating /data directory..."
|
||||
mkdir -p /data
|
||||
fi
|
||||
|
||||
# Create backups directory if it doesn't exist
|
||||
if [ ! -d "/data/backups" ]; then
|
||||
echo "Creating /data/backups directory..."
|
||||
mkdir -p /data/backups
|
||||
fi
|
||||
|
||||
# Set ownership if running as gondulf user (UID 1000)
|
||||
# In rootless Podman, UID 1000 in container maps to host user's subuid range
|
||||
# This chown will only succeed if we have appropriate permissions
|
||||
if [ "$(id -u)" = "1000" ]; then
|
||||
echo "Ensuring correct ownership for /data..."
|
||||
chown -R 1000:1000 /data 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check if database exists, if not initialize it
|
||||
# Note: Gondulf will auto-create the database on first run
|
||||
if [ ! -f "/data/gondulf.db" ]; then
|
||||
echo "Database not found - will be created on first request"
|
||||
fi
|
||||
|
||||
echo "Starting Gondulf application..."
|
||||
echo "User: $(whoami) (UID: $(id -u))"
|
||||
echo "Data directory: /data"
|
||||
echo "Database location: ${GONDULF_DATABASE_URL:-sqlite:////data/gondulf.db}"
|
||||
|
||||
# Execute the main command (passed as arguments)
|
||||
exec "$@"
|
||||
147
deployment/nginx/conf.d/gondulf.conf
Normal file
147
deployment/nginx/conf.d/gondulf.conf
Normal file
@@ -0,0 +1,147 @@
|
||||
# Gondulf IndieAuth Server - nginx Configuration
|
||||
# TLS termination, reverse proxy, rate limiting, and security headers
|
||||
|
||||
# Rate limiting zones
|
||||
limit_req_zone $binary_remote_addr zone=gondulf_auth:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=gondulf_token:10m rate=20r/s;
|
||||
limit_req_zone $binary_remote_addr zone=gondulf_general:10m rate=30r/s;
|
||||
|
||||
# Upstream backend
|
||||
upstream gondulf_backend {
|
||||
server gondulf:8000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# HTTP server - redirect to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name auth.example.com; # CHANGE THIS to your domain
|
||||
|
||||
# Allow Let's Encrypt ACME challenges
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Redirect all other HTTP traffic to HTTPS
|
||||
location / {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name auth.example.com; # CHANGE THIS to your domain
|
||||
|
||||
# SSL/TLS configuration
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
|
||||
# Modern TLS configuration
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# SSL session cache
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# OCSP stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
# CSP will be set by the application
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/gondulf_access.log combined;
|
||||
error_log /var/log/nginx/gondulf_error.log warn;
|
||||
|
||||
# Client request limits
|
||||
client_max_body_size 1M;
|
||||
client_body_timeout 10s;
|
||||
client_header_timeout 10s;
|
||||
|
||||
# Authorization endpoint - stricter rate limiting
|
||||
location ~ ^/(authorize|auth) {
|
||||
limit_req zone=gondulf_auth burst=20 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass http://gondulf_backend;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# Proxy timeouts
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# Token endpoint - moderate rate limiting
|
||||
location /token {
|
||||
limit_req zone=gondulf_token burst=40 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass http://gondulf_backend;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# Health check endpoint - no rate limiting, no logging
|
||||
location /health {
|
||||
access_log off;
|
||||
proxy_pass http://gondulf_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
}
|
||||
|
||||
# All other endpoints - general rate limiting
|
||||
location / {
|
||||
limit_req zone=gondulf_general burst=60 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass http://gondulf_backend;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# Buffer settings
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
}
|
||||
}
|
||||
156
deployment/scripts/backup.sh
Executable file
156
deployment/scripts/backup.sh
Executable file
@@ -0,0 +1,156 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Gondulf SQLite Database Backup Script
|
||||
# Compatible with both Podman and Docker (auto-detects)
|
||||
#
|
||||
# Usage: ./backup.sh [backup_dir]
|
||||
#
|
||||
# Environment Variables:
|
||||
# GONDULF_DATABASE_URL - Database URL (default: sqlite:////data/gondulf.db)
|
||||
# BACKUP_DIR - Backup directory (default: ./backups)
|
||||
# BACKUP_RETENTION_DAYS - Days to keep backups (default: 7)
|
||||
# COMPRESS_BACKUPS - Compress backups with gzip (default: true)
|
||||
# CONTAINER_NAME - Container name (default: gondulf)
|
||||
# CONTAINER_ENGINE - Force specific engine: podman or docker (default: auto-detect)
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Auto-detect container engine
|
||||
detect_container_engine() {
|
||||
if [ -n "${CONTAINER_ENGINE:-}" ]; then
|
||||
echo "$CONTAINER_ENGINE"
|
||||
elif command -v podman &> /dev/null; then
|
||||
echo "podman"
|
||||
elif command -v docker &> /dev/null; then
|
||||
echo "docker"
|
||||
else
|
||||
echo "ERROR: Neither podman nor docker found" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ENGINE=$(detect_container_engine)
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-gondulf}"
|
||||
|
||||
echo "========================================="
|
||||
echo "Gondulf Database Backup"
|
||||
echo "========================================="
|
||||
echo "Container engine: $ENGINE"
|
||||
echo "Container name: $CONTAINER_NAME"
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
DATABASE_URL="${GONDULF_DATABASE_URL:-sqlite:////data/gondulf.db}"
|
||||
BACKUP_DIR="${1:-${BACKUP_DIR:-./backups}}"
|
||||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
|
||||
COMPRESS="${COMPRESS_BACKUPS:-true}"
|
||||
|
||||
# Extract database path from URL (handle both 3-slash and 4-slash formats)
|
||||
if [[ "$DATABASE_URL" =~ ^sqlite:////(.+)$ ]]; then
|
||||
# Four slashes = absolute path
|
||||
DB_PATH="/${BASH_REMATCH[1]}"
|
||||
elif [[ "$DATABASE_URL" =~ ^sqlite:///(.+)$ ]]; then
|
||||
# Three slashes = relative path (assume /data in container)
|
||||
DB_PATH="/data/${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "ERROR: Invalid DATABASE_URL format: $DATABASE_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Database path: $DB_PATH"
|
||||
|
||||
# Verify container is running
|
||||
if ! $ENGINE ps | grep -q "$CONTAINER_NAME"; then
|
||||
echo "ERROR: Container '$CONTAINER_NAME' is not running" >&2
|
||||
echo "Start the container first with: $ENGINE start $CONTAINER_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create backup directory on host if it doesn't exist
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Generate backup filename with timestamp
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE_CONTAINER="/tmp/gondulf_backup_${TIMESTAMP}.db"
|
||||
BACKUP_FILE_HOST="$BACKUP_DIR/gondulf_backup_${TIMESTAMP}.db"
|
||||
|
||||
echo "Starting backup..."
|
||||
echo " Backup file: $BACKUP_FILE_HOST"
|
||||
echo ""
|
||||
|
||||
# Perform backup using SQLite VACUUM INTO (safe hot backup)
|
||||
# This creates a clean, optimized copy of the database
|
||||
echo "Creating database backup (this may take a moment)..."
|
||||
$ENGINE exec "$CONTAINER_NAME" sqlite3 "$DB_PATH" "VACUUM INTO '$BACKUP_FILE_CONTAINER'" || {
|
||||
echo "ERROR: Backup failed" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy backup out of container to host
|
||||
echo "Copying backup to host..."
|
||||
$ENGINE cp "$CONTAINER_NAME:$BACKUP_FILE_CONTAINER" "$BACKUP_FILE_HOST" || {
|
||||
echo "ERROR: Failed to copy backup from container" >&2
|
||||
$ENGINE exec "$CONTAINER_NAME" rm -f "$BACKUP_FILE_CONTAINER" 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Clean up temporary file in container
|
||||
$ENGINE exec "$CONTAINER_NAME" rm -f "$BACKUP_FILE_CONTAINER"
|
||||
|
||||
# Verify backup was created on host
|
||||
if [ ! -f "$BACKUP_FILE_HOST" ]; then
|
||||
echo "ERROR: Backup file was not created on host" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify backup integrity
|
||||
echo "Verifying backup integrity..."
|
||||
if sqlite3 "$BACKUP_FILE_HOST" "PRAGMA integrity_check;" | grep -q "ok"; then
|
||||
echo "✓ Backup integrity check passed"
|
||||
else
|
||||
echo "ERROR: Backup integrity check failed" >&2
|
||||
rm -f "$BACKUP_FILE_HOST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Backup created successfully"
|
||||
|
||||
# Compress backup if enabled
|
||||
if [ "$COMPRESS" = "true" ]; then
|
||||
echo "Compressing backup..."
|
||||
gzip "$BACKUP_FILE_HOST"
|
||||
BACKUP_FILE_HOST="$BACKUP_FILE_HOST.gz"
|
||||
echo "✓ Backup compressed"
|
||||
fi
|
||||
|
||||
# Calculate and display backup size
|
||||
BACKUP_SIZE=$(du -h "$BACKUP_FILE_HOST" | cut -f1)
|
||||
echo "Backup size: $BACKUP_SIZE"
|
||||
|
||||
# Clean up old backups
|
||||
echo ""
|
||||
echo "Cleaning up backups older than $RETENTION_DAYS days..."
|
||||
DELETED_COUNT=$(find "$BACKUP_DIR" -name "gondulf_backup_*.db*" -type f -mtime +$RETENTION_DAYS -delete -print | wc -l)
|
||||
if [ "$DELETED_COUNT" -gt 0 ]; then
|
||||
echo "✓ Deleted $DELETED_COUNT old backup(s)"
|
||||
else
|
||||
echo " No old backups to delete"
|
||||
fi
|
||||
|
||||
# List current backups
|
||||
echo ""
|
||||
echo "Current backups:"
|
||||
if ls "$BACKUP_DIR"/gondulf_backup_*.db* 1> /dev/null 2>&1; then
|
||||
ls -lht "$BACKUP_DIR"/gondulf_backup_*.db* | head -10
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Backup complete!"
|
||||
echo "========================================="
|
||||
echo "Backup location: $BACKUP_FILE_HOST"
|
||||
echo "Container engine: $ENGINE"
|
||||
echo ""
|
||||
206
deployment/scripts/restore.sh
Executable file
206
deployment/scripts/restore.sh
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Gondulf SQLite Database Restore Script
|
||||
# Compatible with both Podman and Docker (auto-detects)
|
||||
#
|
||||
# Usage: ./restore.sh <backup_file>
|
||||
#
|
||||
# CAUTION: This will REPLACE the current database!
|
||||
# A safety backup will be created before restoration.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Auto-detect container engine
|
||||
detect_container_engine() {
|
||||
if [ -n "${CONTAINER_ENGINE:-}" ]; then
|
||||
echo "$CONTAINER_ENGINE"
|
||||
elif command -v podman &> /dev/null; then
|
||||
echo "podman"
|
||||
elif command -v docker &> /dev/null; then
|
||||
echo "docker"
|
||||
else
|
||||
echo "ERROR: Neither podman nor docker found" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check arguments
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <backup_file>"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 ./backups/gondulf_backup_20251120_120000.db.gz"
|
||||
echo " $0 ./backups/gondulf_backup_20251120_120000.db"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_FILE="$1"
|
||||
ENGINE=$(detect_container_engine)
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-gondulf}"
|
||||
|
||||
echo "========================================="
|
||||
echo "Gondulf Database Restore"
|
||||
echo "========================================="
|
||||
echo "Container engine: $ENGINE"
|
||||
echo "Container name: $CONTAINER_NAME"
|
||||
echo "Backup file: $BACKUP_FILE"
|
||||
echo ""
|
||||
echo "⚠️ WARNING: This will REPLACE the current database!"
|
||||
echo ""
|
||||
|
||||
# Validate backup file exists
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
echo "ERROR: Backup file not found: $BACKUP_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
DATABASE_URL="${GONDULF_DATABASE_URL:-sqlite:////data/gondulf.db}"
|
||||
|
||||
# Extract database path from URL
|
||||
if [[ "$DATABASE_URL" =~ ^sqlite:////(.+)$ ]]; then
|
||||
DB_PATH="/${BASH_REMATCH[1]}"
|
||||
elif [[ "$DATABASE_URL" =~ ^sqlite:///(.+)$ ]]; then
|
||||
DB_PATH="/data/${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "ERROR: Invalid DATABASE_URL format: $DATABASE_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Database path in container: $DB_PATH"
|
||||
|
||||
# Check if container is running
|
||||
CONTAINER_RUNNING=false
|
||||
if $ENGINE ps | grep -q "$CONTAINER_NAME"; then
|
||||
CONTAINER_RUNNING=true
|
||||
echo "Container status: running"
|
||||
echo ""
|
||||
echo "⚠️ Container is running. It will be stopped during restoration."
|
||||
read -p "Continue? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Restore cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Stopping container..."
|
||||
$ENGINE stop "$CONTAINER_NAME"
|
||||
else
|
||||
echo "Container status: stopped"
|
||||
fi
|
||||
|
||||
# Decompress if needed
|
||||
TEMP_FILE=""
|
||||
RESTORE_FILE=""
|
||||
if [[ "$BACKUP_FILE" == *.gz ]]; then
|
||||
echo "Decompressing backup..."
|
||||
TEMP_FILE=$(mktemp)
|
||||
gunzip -c "$BACKUP_FILE" > "$TEMP_FILE"
|
||||
RESTORE_FILE="$TEMP_FILE"
|
||||
echo "✓ Decompressed to temporary file"
|
||||
else
|
||||
RESTORE_FILE="$BACKUP_FILE"
|
||||
fi
|
||||
|
||||
# Verify backup integrity before restore
|
||||
echo "Verifying backup integrity..."
|
||||
if ! sqlite3 "$RESTORE_FILE" "PRAGMA integrity_check;" | grep -q "ok"; then
|
||||
echo "ERROR: Backup integrity check failed" >&2
|
||||
[ -n "$TEMP_FILE" ] && rm -f "$TEMP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Backup integrity verified"
|
||||
|
||||
# Create temporary container to access volume if container is stopped
|
||||
if [ "$CONTAINER_RUNNING" = false ]; then
|
||||
echo "Creating temporary container to access volume..."
|
||||
TEMP_CONTAINER="${CONTAINER_NAME}_restore_temp"
|
||||
$ENGINE run -d --name "$TEMP_CONTAINER" \
|
||||
-v gondulf_data:/data \
|
||||
alpine:latest sleep 300
|
||||
CONTAINER_NAME="$TEMP_CONTAINER"
|
||||
fi
|
||||
|
||||
# Create safety backup of current database
|
||||
echo "Creating safety backup of current database..."
|
||||
SAFETY_BACKUP_CONTAINER="/data/gondulf_pre_restore_$(date +%Y%m%d_%H%M%S).db"
|
||||
if $ENGINE exec "$CONTAINER_NAME" test -f "$DB_PATH" 2>/dev/null; then
|
||||
$ENGINE exec "$CONTAINER_NAME" cp "$DB_PATH" "$SAFETY_BACKUP_CONTAINER" || {
|
||||
echo "WARNING: Failed to create safety backup" >&2
|
||||
}
|
||||
echo "✓ Safety backup created: $SAFETY_BACKUP_CONTAINER"
|
||||
else
|
||||
echo " No existing database found (first time setup)"
|
||||
fi
|
||||
|
||||
# Copy restore file into container
|
||||
RESTORE_FILE_CONTAINER="/tmp/restore_db.tmp"
|
||||
echo "Copying backup to container..."
|
||||
$ENGINE cp "$RESTORE_FILE" "$CONTAINER_NAME:$RESTORE_FILE_CONTAINER"
|
||||
|
||||
# Perform restore
|
||||
echo "Restoring database..."
|
||||
$ENGINE exec "$CONTAINER_NAME" sh -c "cp '$RESTORE_FILE_CONTAINER' '$DB_PATH'"
|
||||
|
||||
# Verify restored database
|
||||
echo "Verifying restored database..."
|
||||
if $ENGINE exec "$CONTAINER_NAME" sqlite3 "$DB_PATH" "PRAGMA integrity_check;" | grep -q "ok"; then
|
||||
echo "✓ Restored database integrity verified"
|
||||
else
|
||||
echo "ERROR: Restored database integrity check failed" >&2
|
||||
echo "Attempting to restore from safety backup..."
|
||||
|
||||
if $ENGINE exec "$CONTAINER_NAME" test -f "$SAFETY_BACKUP_CONTAINER" 2>/dev/null; then
|
||||
$ENGINE exec "$CONTAINER_NAME" cp "$SAFETY_BACKUP_CONTAINER" "$DB_PATH"
|
||||
echo "✓ Reverted to safety backup"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
$ENGINE exec "$CONTAINER_NAME" rm -f "$RESTORE_FILE_CONTAINER"
|
||||
[ -n "$TEMP_FILE" ] && rm -f "$TEMP_FILE"
|
||||
|
||||
# Stop temporary container if created
|
||||
if [ "$CONTAINER_RUNNING" = false ]; then
|
||||
$ENGINE stop "$TEMP_CONTAINER" 2>/dev/null || true
|
||||
$ENGINE rm "$TEMP_CONTAINER" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up temporary restore file in container
|
||||
$ENGINE exec "$CONTAINER_NAME" rm -f "$RESTORE_FILE_CONTAINER"
|
||||
|
||||
# Clean up temporary decompressed file on host
|
||||
[ -n "$TEMP_FILE" ] && rm -f "$TEMP_FILE"
|
||||
|
||||
# Stop and remove temporary container if we created one
|
||||
if [ "$CONTAINER_RUNNING" = false ]; then
|
||||
echo "Cleaning up temporary container..."
|
||||
$ENGINE stop "$TEMP_CONTAINER" 2>/dev/null || true
|
||||
$ENGINE rm "$TEMP_CONTAINER" 2>/dev/null || true
|
||||
CONTAINER_NAME="${CONTAINER_NAME%_restore_temp}" # Restore original name
|
||||
fi
|
||||
|
||||
# Restart original container if it was running
|
||||
if [ "$CONTAINER_RUNNING" = true ]; then
|
||||
echo "Starting container..."
|
||||
$ENGINE start "$CONTAINER_NAME"
|
||||
echo "Waiting for container to be healthy..."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Restore complete!"
|
||||
echo "========================================="
|
||||
echo "Backup restored from: $BACKUP_FILE"
|
||||
echo "Safety backup location: $SAFETY_BACKUP_CONTAINER"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Verify the application is working correctly"
|
||||
echo "2. Once verified, you may delete the safety backup with:"
|
||||
echo " $ENGINE exec $CONTAINER_NAME rm $SAFETY_BACKUP_CONTAINER"
|
||||
echo ""
|
||||
169
deployment/scripts/test-backup-restore.sh
Executable file
169
deployment/scripts/test-backup-restore.sh
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Gondulf Backup and Restore Test Script
|
||||
# Tests backup and restore procedures without modifying production data
|
||||
#
|
||||
# Usage: ./test-backup-restore.sh
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Auto-detect container engine
|
||||
detect_container_engine() {
|
||||
if [ -n "${CONTAINER_ENGINE:-}" ]; then
|
||||
echo "$CONTAINER_ENGINE"
|
||||
elif command -v podman &> /dev/null; then
|
||||
echo "podman"
|
||||
elif command -v docker &> /dev/null; then
|
||||
echo "docker"
|
||||
else
|
||||
echo "ERROR: Neither podman nor docker found" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ENGINE=$(detect_container_engine)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEST_DIR="/tmp/gondulf-backup-test-$$"
|
||||
|
||||
echo "========================================="
|
||||
echo "Gondulf Backup/Restore Test"
|
||||
echo "========================================="
|
||||
echo "Container engine: $ENGINE"
|
||||
echo "Test directory: $TEST_DIR"
|
||||
echo ""
|
||||
|
||||
# Create test directory
|
||||
mkdir -p "$TEST_DIR"
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Cleaning up test directory..."
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Test 1: Create a backup
|
||||
echo "Test 1: Creating backup..."
|
||||
echo "----------------------------------------"
|
||||
if BACKUP_DIR="$TEST_DIR" "$SCRIPT_DIR/backup.sh"; then
|
||||
echo "✓ Test 1 PASSED: Backup created successfully"
|
||||
else
|
||||
echo "✗ Test 1 FAILED: Backup creation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Verify backup file exists
|
||||
BACKUP_FILE=$(ls -t "$TEST_DIR"/gondulf_backup_*.db.gz 2>/dev/null | head -1)
|
||||
if [ -z "$BACKUP_FILE" ]; then
|
||||
echo "✗ Test FAILED: No backup file found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Backup file: $BACKUP_FILE"
|
||||
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
echo "Backup size: $BACKUP_SIZE"
|
||||
echo ""
|
||||
|
||||
# Test 2: Verify backup integrity
|
||||
echo "Test 2: Verifying backup integrity..."
|
||||
echo "----------------------------------------"
|
||||
TEMP_DB=$(mktemp)
|
||||
gunzip -c "$BACKUP_FILE" > "$TEMP_DB"
|
||||
|
||||
if sqlite3 "$TEMP_DB" "PRAGMA integrity_check;" | grep -q "ok"; then
|
||||
echo "✓ Test 2 PASSED: Backup integrity check successful"
|
||||
else
|
||||
echo "✗ Test 2 FAILED: Backup integrity check failed"
|
||||
rm -f "$TEMP_DB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Test 3: Verify backup contains expected tables
|
||||
echo "Test 3: Verifying backup structure..."
|
||||
echo "----------------------------------------"
|
||||
TABLES=$(sqlite3 "$TEMP_DB" "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")
|
||||
echo "Tables found in backup:"
|
||||
echo "$TABLES"
|
||||
|
||||
# Check for expected tables (based on Gondulf schema)
|
||||
# Tables: authorization_codes, domains, migrations, tokens, sqlite_sequence
|
||||
EXPECTED_TABLES=("authorization_codes" "domains" "tokens")
|
||||
ALL_TABLES_FOUND=true
|
||||
|
||||
for table in "${EXPECTED_TABLES[@]}"; do
|
||||
if echo "$TABLES" | grep -q "^$table$"; then
|
||||
echo "✓ Found table: $table"
|
||||
else
|
||||
echo "✗ Missing table: $table"
|
||||
ALL_TABLES_FOUND=false
|
||||
fi
|
||||
done
|
||||
|
||||
rm -f "$TEMP_DB"
|
||||
|
||||
if [ "$ALL_TABLES_FOUND" = true ]; then
|
||||
echo "✓ Test 3 PASSED: All expected tables found"
|
||||
else
|
||||
echo "✗ Test 3 FAILED: Missing expected tables"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Test 4: Test decompression
|
||||
echo "Test 4: Testing backup decompression..."
|
||||
echo "----------------------------------------"
|
||||
UNCOMPRESSED_DB="$TEST_DIR/test_uncompressed.db"
|
||||
if gunzip -c "$BACKUP_FILE" > "$UNCOMPRESSED_DB"; then
|
||||
if [ -f "$UNCOMPRESSED_DB" ] && [ -s "$UNCOMPRESSED_DB" ]; then
|
||||
echo "✓ Test 4 PASSED: Backup decompression successful"
|
||||
UNCOMPRESSED_SIZE=$(du -h "$UNCOMPRESSED_DB" | cut -f1)
|
||||
echo " Uncompressed size: $UNCOMPRESSED_SIZE"
|
||||
else
|
||||
echo "✗ Test 4 FAILED: Decompressed file is empty or missing"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✗ Test 4 FAILED: Decompression failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Test 5: Verify backup can be queried
|
||||
echo "Test 5: Testing backup database queries..."
|
||||
echo "----------------------------------------"
|
||||
if DOMAIN_COUNT=$(sqlite3 "$UNCOMPRESSED_DB" "SELECT COUNT(*) FROM domains;" 2>/dev/null); then
|
||||
echo "✓ Test 5 PASSED: Backup database is queryable"
|
||||
echo " Domain count: $DOMAIN_COUNT"
|
||||
else
|
||||
echo "✗ Test 5 FAILED: Cannot query backup database"
|
||||
rm -f "$UNCOMPRESSED_DB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$UNCOMPRESSED_DB"
|
||||
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "========================================="
|
||||
echo "All Tests Passed!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo " Backup file: $BACKUP_FILE"
|
||||
echo " Backup size: $BACKUP_SIZE"
|
||||
echo " Container engine: $ENGINE"
|
||||
echo ""
|
||||
echo "The backup and restore system is working correctly."
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
68
deployment/systemd/gondulf-compose.service
Normal file
68
deployment/systemd/gondulf-compose.service
Normal file
@@ -0,0 +1,68 @@
|
||||
# Gondulf IndieAuth Server - systemd Unit for Compose (Podman or Docker)
|
||||
#
|
||||
# This unit works with both podman-compose and docker-compose
|
||||
#
|
||||
# Installation (Podman rootless):
|
||||
# 1. Copy this file to ~/.config/systemd/user/gondulf.service
|
||||
# 2. Edit ExecStart/ExecStop to use podman-compose
|
||||
# 3. systemctl --user daemon-reload
|
||||
# 4. systemctl --user enable --now gondulf
|
||||
# 5. loginctl enable-linger $USER
|
||||
#
|
||||
# Installation (Docker):
|
||||
# 1. Copy this file to /etc/systemd/system/gondulf.service
|
||||
# 2. Edit ExecStart/ExecStop to use docker-compose
|
||||
# 3. Edit Requires= and After= to include docker.service
|
||||
# 4. sudo systemctl daemon-reload
|
||||
# 5. sudo systemctl enable --now gondulf
|
||||
#
|
||||
# Management:
|
||||
# systemctl --user status gondulf # For rootless
|
||||
# sudo systemctl status gondulf # For rootful/Docker
|
||||
#
|
||||
|
||||
[Unit]
|
||||
Description=Gondulf IndieAuth Server (Compose)
|
||||
Documentation=https://github.com/yourusername/gondulf
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
# For Docker, add:
|
||||
# Requires=docker.service
|
||||
# After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
|
||||
# Working directory (adjust to your installation path)
|
||||
# Rootless Podman: WorkingDirectory=/home/%u/gondulf
|
||||
# Docker: WorkingDirectory=/opt/gondulf
|
||||
WorkingDirectory=/home/%u/gondulf
|
||||
|
||||
# Start services (choose one based on your container engine)
|
||||
|
||||
# For Podman (rootless):
|
||||
ExecStart=/usr/bin/podman-compose -f docker-compose.yml -f docker-compose.production.yml up -d
|
||||
|
||||
# For Docker (rootful):
|
||||
# ExecStart=/usr/bin/docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d
|
||||
|
||||
# Stop services (choose one based on your container engine)
|
||||
|
||||
# For Podman:
|
||||
ExecStop=/usr/bin/podman-compose down
|
||||
|
||||
# For Docker:
|
||||
# ExecStop=/usr/bin/docker-compose down
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30s
|
||||
|
||||
[Install]
|
||||
# For rootless Podman:
|
||||
WantedBy=default.target
|
||||
|
||||
# For Docker:
|
||||
# WantedBy=multi-user.target
|
||||
53
deployment/systemd/gondulf-docker.service
Normal file
53
deployment/systemd/gondulf-docker.service
Normal file
@@ -0,0 +1,53 @@
|
||||
# Gondulf IndieAuth Server - systemd Unit for Docker
|
||||
#
|
||||
# Installation:
|
||||
# 1. Copy this file to /etc/systemd/system/gondulf.service
|
||||
# 2. sudo systemctl daemon-reload
|
||||
# 3. sudo systemctl enable --now gondulf
|
||||
#
|
||||
# Management:
|
||||
# sudo systemctl status gondulf
|
||||
# sudo systemctl restart gondulf
|
||||
# sudo systemctl stop gondulf
|
||||
# sudo journalctl -u gondulf -f
|
||||
#
|
||||
|
||||
[Unit]
|
||||
Description=Gondulf IndieAuth Server (Docker)
|
||||
Documentation=https://github.com/yourusername/gondulf
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
TimeoutStartSec=60s
|
||||
TimeoutStopSec=30s
|
||||
|
||||
# Working directory (adjust to your installation path)
|
||||
WorkingDirectory=/opt/gondulf
|
||||
|
||||
# Stop and remove any existing container
|
||||
ExecStartPre=-/usr/bin/docker stop gondulf
|
||||
ExecStartPre=-/usr/bin/docker rm gondulf
|
||||
|
||||
# Start container
|
||||
ExecStart=/usr/bin/docker run \
|
||||
--name gondulf \
|
||||
--rm \
|
||||
-p 8000:8000 \
|
||||
-v gondulf_data:/data \
|
||||
--env-file /opt/gondulf/.env \
|
||||
--health-cmd "wget --no-verbose --tries=1 --spider http://localhost:8000/health || exit 1" \
|
||||
--health-interval 30s \
|
||||
--health-timeout 5s \
|
||||
--health-retries 3 \
|
||||
gondulf:latest
|
||||
|
||||
# Stop container gracefully
|
||||
ExecStop=/usr/bin/docker stop -t 10 gondulf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
62
deployment/systemd/gondulf-podman.service
Normal file
62
deployment/systemd/gondulf-podman.service
Normal file
@@ -0,0 +1,62 @@
|
||||
# Gondulf IndieAuth Server - systemd Unit for Rootless Podman
|
||||
#
|
||||
# Installation (rootless - recommended):
|
||||
# 1. Copy this file to ~/.config/systemd/user/gondulf.service
|
||||
# 2. systemctl --user daemon-reload
|
||||
# 3. systemctl --user enable --now gondulf
|
||||
# 4. loginctl enable-linger $USER # Allow service to run without login
|
||||
#
|
||||
# Installation (rootful - not recommended):
|
||||
# 1. Copy this file to /etc/systemd/system/gondulf.service
|
||||
# 2. sudo systemctl daemon-reload
|
||||
# 3. sudo systemctl enable --now gondulf
|
||||
#
|
||||
# Management:
|
||||
# systemctl --user status gondulf
|
||||
# systemctl --user restart gondulf
|
||||
# systemctl --user stop gondulf
|
||||
# journalctl --user -u gondulf -f
|
||||
#
|
||||
|
||||
[Unit]
|
||||
Description=Gondulf IndieAuth Server (Rootless Podman)
|
||||
Documentation=https://github.com/yourusername/gondulf
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
TimeoutStartSec=60s
|
||||
TimeoutStopSec=30s
|
||||
|
||||
# Working directory (adjust to your installation path)
|
||||
WorkingDirectory=/home/%u/gondulf
|
||||
|
||||
# Stop and remove any existing container
|
||||
ExecStartPre=-/usr/bin/podman stop gondulf
|
||||
ExecStartPre=-/usr/bin/podman rm gondulf
|
||||
|
||||
# Start container
|
||||
ExecStart=/usr/bin/podman run \
|
||||
--name gondulf \
|
||||
--rm \
|
||||
-p 8000:8000 \
|
||||
-v gondulf_data:/data:Z \
|
||||
--env-file /home/%u/gondulf/.env \
|
||||
--health-cmd "wget --no-verbose --tries=1 --spider http://localhost:8000/health || exit 1" \
|
||||
--health-interval 30s \
|
||||
--health-timeout 5s \
|
||||
--health-retries 3 \
|
||||
gondulf:latest
|
||||
|
||||
# Stop container gracefully
|
||||
ExecStop=/usr/bin/podman stop -t 10 gondulf
|
||||
|
||||
# Security settings (rootless already provides good isolation)
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Reference in New Issue
Block a user