Files
mintel.me/apps/web/DEPLOYMENT.md
Marc Mintel 103d71851c
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
chore: overhaul infrastructure and integrate @mintel packages
- 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
2026-02-05 14:18:51 +01:00

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"

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 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 domain
  • PLAUSIBLE_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:

  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:

    src="https://plausible.yourdomain.com/js/script.js"
    
  2. Add subdomain to Caddyfile:

    analytics.mintel.me {
        reverse_proxy http://YOUR_PLAUSIBLE_SERVER:8000
    }
    
  3. 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

  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

# 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 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)