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:
2025-11-21 19:16:54 -07:00
parent d3c3e8dc6b
commit 01dcaba86b
22 changed files with 6353 additions and 18 deletions

848
deployment/README.md Normal file
View 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
View 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 "$@"

View 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
View 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
View 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 ""

View 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

View 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

View 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

View 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