- Restructure to pnpm monorepo (site moved to apps/web) - Integrate @mintel/tsconfig, @mintel/eslint-config, @mintel/husky-config - Implement Docker service architecture (Varnish, Directus, Gatekeeper) - Setup environment-aware Gitea Actions deployment
7.2 KiB
Hetzner Deployment Guide
Complete deployment plan for your Astro blog on Hetzner using Docker, with your existing Gitea + Woodpecker setup.
🏗️ Architecture Overview
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Hetzner VM │ │ Caddy Reverse │ │ Plausible │
│ (Ubuntu) │◄──►│ Proxy │◄──►│ Analytics │
│ │ │ (SSL/HTTP2) │ │ (Existing) │
└────────┬────────┘ └──────────────────┘ └─────────────────┘
│
│ Docker Network
│
┌────▼─────┐ ┌──────────────┐ ┌──────────────┐
│ Nginx │ │ Redis │ │ Website │
│ Static │ │ Cache │ │ (Astro) │
│ Server │ │ │ │ │
└──────────┘ └──────────────┘ └──────────────┘
📁 Project Structure
mintel.me/
├── docker/
│ ├── Dockerfile # Multi-stage build
│ ├── nginx.conf # Nginx config with caching
│ └── Caddyfile # SSL reverse proxy
├── docker-compose.yml # Local & Hetzner deployment
├── .woodpecker.yml # CI/CD pipeline
├── deploy.sh # Manual deployment script
├── src/
│ ├── utils/
│ │ └── cache/ # Cache abstraction layer
│ │ ├── interfaces.ts
│ │ ├── memory-adapter.ts
│ │ ├── redis-adapter.ts
│ │ ├── composite-adapter.ts
│ │ └── index.ts
│ ├── components/
│ │ └── Analytics.astro # Plausible integration
│ └── layouts/
│ └── BaseLayout.astro # Includes analytics
└── package.json # Added ioredis dependency
🚀 Deployment Options
Option 1: Manual Deployment (Quick Start)
# 1. Provision Hetzner VM (Ubuntu 22.04)
# 2. Install Docker + Docker Compose
ssh root@YOUR_HETZNER_IP "apt update && apt install -y docker.io docker-compose"
# 3. Copy files to server
scp -r . root@YOUR_HETZNER_IP:/opt/mintel
# 4. Set environment variables
ssh root@YOUR_HETZNER_IP "cat > /opt/mintel/.env << EOF
DOMAIN=mintel.me
ADMIN_EMAIL=admin@mintel.me
REDIS_URL=redis://redis:6379
EOF"
# 5. Deploy
ssh root@YOUR_HETZNER_IP "cd /opt/mintel && ./deploy.sh"
Option 2: CI/CD with Woodpecker + Gitea (Recommended)
Your existing Woodpecker + Gitea setup will handle automatic deployments:
-
Configure Woodpecker secrets in your Gitea repo:
SSH_PRIVATE_KEY- Private key for Hetzner accessDEPLOY_HOST- Hetzner server IPDEPLOY_USER- UsuallyrootDOMAIN- Your domainADMIN_EMAIL- Contact email
-
Push to main branch triggers automatic deployment
🔧 Environment Variables
See ENV_SETUP.md for detailed configuration guide.
Quick setup:
cp .env.example .env
# Edit .env with your values
Required variables:
DOMAIN- Your domain (e.g., mintel.me)ADMIN_EMAIL- SSL certificate contact
Optional variables:
REDIS_URL- Redis connection (default:redis://redis:6379)PLAUSIBLE_DOMAIN- Analytics domainPLAUSIBLE_SCRIPT_URL- Plausible script URL
🎯 Performance Optimization
Redis Caching Strategy
The cache abstraction layer provides:
// Usage in your Astro pages
import { withCache } from '../utils/cache';
// Cache expensive operations
const blogPosts = await withCache(
'blog:posts',
() => fetchBlogPosts(),
3600 // 1 hour TTL
);
Cache Layers:
- Memory Cache - Fast, single instance
- Redis Cache - Distributed, multi-instance
- Composite Cache - Auto-fallback Redis → Memory
Nginx Caching
- Static assets: 1 year cache
- HTML: No cache (always fresh)
- Gzip/Brotli: Enabled
Caddy Benefits
- Automatic SSL with Let's Encrypt
- HTTP/2 out of the box
- Automatic redirects HTTP → HTTPS
- Security headers configured
📊 Analytics Integration
Plausible Setup
Your existing Plausible instance is already self-hosted. Integration:
-
Update Analytics.astro with your Plausible domain:
src="https://plausible.yourdomain.com/js/script.js" -
Add subdomain to Caddyfile:
analytics.mintel.me { reverse_proxy http://YOUR_PLAUSIBLE_SERVER:8000 } -
Track custom events:
window.plausible('Page Load', { props: { loadTime: 123 }});
🔐 Security
Caddy Security Headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Server "mintel"
}
Docker Security
- Non-root user in containers
- Read-only filesystem where possible
- Limited capabilities
📈 Monitoring & Health
Health Check Endpoint
https://mintel.me/health
Returns: healthy
Docker Commands
# View logs
docker logs -f mintel-website
# Check Redis
docker exec -it mintel-redis redis-cli ping
# Restart services
docker-compose restart
# Update deployment
docker-compose pull && docker-compose up -d
🔄 CI/CD Pipeline Flow
- Push to Gitea → Woodpecker triggers
- Test → Run smoke tests
- Build → Create Docker image
- Push → Registry (optional)
- Deploy → SSH to Hetzner, pull & restart
🚨 Troubleshooting
SSL Issues
# Check Caddy logs
docker logs mintel-caddy
# Force certificate renewal
docker exec mintel-caddy caddy renew
Redis Connection
# Test Redis
docker exec mintel-redis redis-cli ping
# Check Redis logs
docker logs mintel-redis
Cache Issues
# Clear all cache
docker exec mintel-redis redis-cli FLUSHALL
📦 Dependencies
Required
ioredis- Redis client- Already have: Astro, React, Tailwind
Docker Images
nginx:alpine- Web serverredis:7-alpine- Cachecaddy:2-alpine- Reverse proxy
🎯 Next Steps
- ✅ Configure environment variables
- ✅ Test locally with
docker-compose up - ✅ Setup Woodpecker secrets
- ✅ Deploy to Hetzner
- ✅ Monitor with Plausible analytics
💡 Tips
- First deployment: Use manual
deploy.shto verify everything works - Updates: CI/CD handles everything on git push
- Rollback:
docker-compose down && docker-compose up -dwith previous image - Scaling: Add more web containers, Redis handles distributed cache
Total setup time: ~30 minutes
Maintenance: Near zero (automated)
Performance: Excellent (Redis + Nginx caching)
Analytics: Privacy-focused (Plausible)