# 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) ```bash # 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: 1. **Configure Woodpecker secrets** in your Gitea repo: - `SSH_PRIVATE_KEY` - Private key for Hetzner access - `DEPLOY_HOST` - Hetzner server IP - `DEPLOY_USER` - Usually `root` - `DOMAIN` - Your domain - `ADMIN_EMAIL` - Contact email 2. **Push to main branch** triggers automatic deployment ## 🔧 Environment Variables See [ENV_SETUP.md](./ENV_SETUP.md) for detailed configuration guide. **Quick setup:** ```bash 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 domain - `PLAUSIBLE_SCRIPT_URL` - Plausible script URL ## 🎯 Performance Optimization ### Redis Caching Strategy The cache abstraction layer provides: ```typescript // 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:** 1. **Memory Cache** - Fast, single instance 2. **Redis Cache** - Distributed, multi-instance 3. **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: 1. **Update Analytics.astro** with your Plausible domain: ```astro src="https://plausible.yourdomain.com/js/script.js" ``` 2. **Add subdomain** to Caddyfile: ```caddy analytics.mintel.me { reverse_proxy http://YOUR_PLAUSIBLE_SERVER:8000 } ``` 3. **Track custom events**: ```javascript window.plausible('Page Load', { props: { loadTime: 123 }}); ``` ## 🔐 Security ### Caddy Security Headers ```caddy 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 ```bash # 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 1. **Push to Gitea** → Woodpecker triggers 2. **Test** → Run smoke tests 3. **Build** → Create Docker image 4. **Push** → Registry (optional) 5. **Deploy** → SSH to Hetzner, pull & restart ## 🚨 Troubleshooting ### SSL Issues ```bash # Check Caddy logs docker logs mintel-caddy # Force certificate renewal docker exec mintel-caddy caddy renew ``` ### Redis Connection ```bash # Test Redis docker exec mintel-redis redis-cli ping # Check Redis logs docker logs mintel-redis ``` ### Cache Issues ```bash # 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 server - `redis:7-alpine` - Cache - `caddy:2-alpine` - Reverse proxy ## 🎯 Next Steps 1. ✅ **Configure** environment variables 2. ✅ **Test** locally with `docker-compose up` 3. ✅ **Setup** Woodpecker secrets 4. ✅ **Deploy** to Hetzner 5. ✅ **Monitor** with Plausible analytics ## 💡 Tips - **First deployment**: Use manual `deploy.sh` to verify everything works - **Updates**: CI/CD handles everything on git push - **Rollback**: `docker-compose down && docker-compose up -d` with 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)