From memstack
Provisions, hardens, and deploys apps to Hetzner Cloud VPS with Docker, Nginx/Caddy reverse proxy, SSL certs, database setup, monitoring, and backups.
npx claudepluginhub cwinvestments/memstack --plugin memstackThis skill uses the workspace's default tool permissions.
*Provision a Hetzner Cloud server with security hardening, reverse proxy, SSL, database setup, monitoring, and automated backups.*
Use this skill when the user wants to provision a Hetzner VPS, create a cloud server, deploy to Hetzner, set up a development server, configure server security (UFW, fail2ban), or estimate cloud hosting costs. Handles secure VPS provisioning with Claude Code pre-installed.
Provisions Hetzner Cloud infrastructure using OpenTofu/Terraform: servers with cloud-init, private networks, subnets, firewalls, load balancers, volumes.
Guides hcloud CLI for Hetzner Cloud: provision servers, networks, firewalls, load balancers, volumes with decision trees and commands.
Share bugs, ideas, or general feedback.
Provision a Hetzner Cloud server with security hardening, reverse proxy, SSL, database setup, monitoring, and automated backups.
When this skill activates, output:
π₯οΈ Hetzner Setup β Provisioning your server...
| Context | Status |
|---|---|
| User says "Hetzner", "VPS setup", "server provisioning" | ACTIVE |
| User wants to deploy to a cloud server (Hetzner specifically) | ACTIVE |
| User mentions SSH hardening, fail2ban, or server security | ACTIVE |
| User wants Docker containerization guidance | DORMANT β see docker-setup |
| User wants CI/CD pipeline (not server setup) | DORMANT β see ci-cd-pipeline |
| User wants managed hosting (Railway, Netlify, Vercel) | DORMANT β see railway-deploy or netlify-deploy |
Ask the user for:
Match workload to Hetzner Cloud server:
| Instance | vCPU | RAM | SSD | Traffic | Price | Best For |
|---|---|---|---|---|---|---|
| CX22 | 2 | 4 GB | 40 GB | 20 TB | ~$4.5/mo | Small API, static site, hobby project |
| CX32 | 4 | 8 GB | 80 GB | 20 TB | ~$8/mo | Medium web app, small DB |
| CPX31 | 4 | 8 GB | 160 GB | 20 TB | ~$13/mo | High-perf apps (AMD EPYC), scrapers |
| CPX41 | 8 | 16 GB | 240 GB | 20 TB | ~$24/mo | Large apps, multiple services |
| CPX51 | 16 | 32 GB | 360 GB | 20 TB | ~$47/mo | Heavy workloads, large databases |
| CCX13 | 2 | 8 GB | 80 GB | 20 TB | ~$13/mo | Dedicated vCPU β consistent performance |
Recommendation logic:
Include: location recommendation (Nuremberg for EU, Ashburn for US).
Generate a complete setup script:
#!/bin/bash
# Hetzner Server Initial Setup
# Run as root on fresh Ubuntu 22.04/24.04 LTS
set -euo pipefail
echo "=== SYSTEM UPDATE ==="
apt update && apt upgrade -y
apt install -y curl wget git htop ufw fail2ban unattended-upgrades \
apt-listchanges software-properties-common
echo "=== CREATE DEPLOY USER ==="
useradd -m -s /bin/bash deploy
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
usermod -aG sudo deploy
# Set password for sudo (optional, SSH key preferred)
echo "deploy ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/deploy
echo "=== SSH HARDENING ==="
cat > /etc/ssh/sshd_config.d/hardened.conf << 'SSHEOF'
Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy
SSHEOF
systemctl restart sshd
echo "=== FIREWALL (UFW) ==="
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw --force enable
echo "=== FAIL2BAN ==="
cat > /etc/fail2ban/jail.local << 'F2BEOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
F2BEOF
systemctl enable fail2ban
systemctl restart fail2ban
echo "=== UNATTENDED UPGRADES ==="
cat > /etc/apt/apt.conf.d/20auto-upgrades << 'UUEOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
UUEOF
echo "=== SWAP (for low-RAM instances) ==="
if [ $(free -m | awk '/^Mem:/{print $2}') -lt 8192 ]; then
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl vm.swappiness=10
echo 'vm.swappiness=10' >> /etc/sysctl.conf
fi
echo "=== KERNEL TUNING ==="
cat >> /etc/sysctl.conf << 'KERNEOF'
# Network performance
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
# Security
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
KERNEOF
sysctl -p
echo "=== SETUP COMPLETE ==="
echo "SSH port changed to 2222"
echo "Root login disabled"
echo "Deploy user created with SSH key"
echo "Firewall enabled (ports: 2222, 80, 443)"
echo "Fail2ban active"
echo "Unattended upgrades enabled"
IMPORTANT: After running this script, reconnect using:
ssh -p 2222 deploy@[server-ip]
Option A: Docker deployment (recommended):
# Install Docker
curl -fsSL https://get.docker.com | sh
usermod -aG docker deploy
# Install Docker Compose
apt install -y docker-compose-plugin
# Create app directory
mkdir -p /opt/app
chown deploy:deploy /opt/app
See docker-setup skill for Dockerfile and docker-compose.yml generation.
Option B: Direct deployment (Node.js example):
# Install Node.js via nvm
su - deploy
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install --lts
# Clone and setup
cd /opt/app
git clone [repo-url] .
npm ci --production
# PM2 process manager
npm install -g pm2
pm2 start ecosystem.config.js
pm2 save
pm2 startup
PM2 ecosystem config:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'app',
script: 'dist/index.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
max_memory_restart: '500M',
error_file: '/var/log/app/error.log',
out_file: '/var/log/app/out.log',
merge_logs: true,
}],
};
Option A: Caddy (auto-SSL, simplest):
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy
# /etc/caddy/Caddyfile
example.com {
reverse_proxy localhost:3000
encode gzip zstd
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /var/log/caddy/access.log
format json
}
}
Option B: Nginx + Certbot:
apt install -y nginx certbot python3-certbot-nginx
# /etc/nginx/sites-available/app
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
# Get SSL certificate
certbot --nginx -d example.com
# Auto-renewal is configured automatically by certbot
PostgreSQL:
apt install -y postgresql postgresql-contrib
sudo -u postgres createuser --interactive appuser
sudo -u postgres createdb appdb -O appuser
sudo -u postgres psql -c "ALTER USER appuser PASSWORD 'CHANGE_ME_STRONG_PASSWORD';"
# Listen only on localhost (default β safe)
# For remote access, use SSH tunnel, never expose port 5432
Redis:
apt install -y redis-server
# Secure Redis
sed -i 's/^# requirepass.*/requirepass CHANGE_ME_STRONG_PASSWORD/' /etc/redis/redis.conf
sed -i 's/^bind .*/bind 127.0.0.1 ::1/' /etc/redis/redis.conf
systemctl restart redis-server
Connection security:
127.0.0.1Resource monitoring script:
#!/bin/bash
# /opt/scripts/health-check.sh
# Disk usage alert (>85%)
DISK_USAGE=$(df / | awk 'NR==2{print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 85 ]; then
echo "ALERT: Disk usage at ${DISK_USAGE}%"
# Send notification (webhook, email, etc.)
curl -X POST "https://hooks.slack.com/services/XXX" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"π΄ Disk usage at ${DISK_USAGE}% on $(hostname)\"}"
fi
# Memory usage alert (>90%)
MEM_USAGE=$(free | awk '/Mem:/{printf "%.0f", $3/$2 * 100}')
if [ "$MEM_USAGE" -gt 90 ]; then
echo "ALERT: Memory usage at ${MEM_USAGE}%"
curl -X POST "https://hooks.slack.com/services/XXX" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"π‘ Memory usage at ${MEM_USAGE}% on $(hostname)\"}"
fi
# Check if app is responding
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$HTTP_CODE" != "200" ]; then
echo "ALERT: App health check failed (HTTP $HTTP_CODE)"
curl -X POST "https://hooks.slack.com/services/XXX" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"π΄ App health check failed on $(hostname) β HTTP ${HTTP_CODE}\"}"
fi
# Add to crontab: every 5 minutes
crontab -e
*/5 * * * * /opt/scripts/health-check.sh >> /var/log/health-check.log 2>&1
External uptime monitoring:
Log management:
# Logrotate for application logs
cat > /etc/logrotate.d/app << 'LREOF'
/var/log/app/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
copytruncate
}
LREOF
Hetzner snapshots (server-level):
# Create snapshot via Hetzner CLI
hcloud server create-image --type snapshot --description "Weekly backup $(date +%F)" [server-id]
# Automate weekly snapshots
# Add to crontab (Sunday 3 AM):
0 3 * * 0 hcloud server create-image --type snapshot --description "auto-$(date +\%F)" [server-id]
# Retention: Keep last 4 snapshots, delete older
# Snapshot cost: ~$0.012/GB/month
Database backups (daily):
#!/bin/bash
# /opt/scripts/backup-db.sh
BACKUP_DIR="/opt/backups/db"
RETENTION_DAYS=14
DATE=$(date +%F_%H%M)
mkdir -p "$BACKUP_DIR"
# PostgreSQL
pg_dump -U appuser -h localhost appdb | gzip > "$BACKUP_DIR/appdb_${DATE}.sql.gz"
# Optional: Upload to off-site storage (Hetzner Storage Box or S3)
# rsync -avz "$BACKUP_DIR/" u123456@u123456.your-storagebox.de:backups/
# Cleanup old backups
find "$BACKUP_DIR" -type f -mtime +${RETENTION_DAYS} -delete
echo "Backup complete: appdb_${DATE}.sql.gz"
# Daily at 2 AM
0 2 * * * /opt/scripts/backup-db.sh >> /var/log/backup.log 2>&1
Off-site backup options:
Final security hardening verification:
ββ SERVER HARDENING CHECKLIST βββββββββββββ
SSH:
[x] Root login disabled
[x] Password authentication disabled
[x] SSH port changed from 22
[x] Key-based auth only
[x] MaxAuthTries set to 3
[x] Idle timeout configured
Firewall:
[x] UFW enabled with deny-by-default
[x] Only required ports open (SSH, HTTP, HTTPS)
[x] No database ports exposed
Services:
[x] Fail2ban active on SSH
[x] Unattended security upgrades enabled
[x] Unused services disabled
[x] No unnecessary packages installed
Application:
[x] App runs as non-root user (deploy)
[x] Environment variables for secrets (not in code)
[x] Database listens on localhost only
[x] Redis password set, localhost only
Monitoring:
[x] Disk/memory/CPU alerts configured
[x] App health check endpoint monitored
[x] External uptime monitoring active
[x] Log rotation configured
Backups:
[x] Database backed up daily
[x] Server snapshots weekly
[x] Off-site backup configured
[x] Backup restoration tested
Present the complete server setup:
βββ HETZNER SERVER SETUP βββββββββββββββββ
Instance: [type] β [vCPU] vCPU, [RAM] GB RAM
Location: [datacenter]
OS: Ubuntu [version] LTS
Cost: ~$[X]/mo
ββ PROVISIONING SCRIPT ββββββββββββββββββββ
[complete setup script]
ββ APPLICATION DEPLOYMENT βββββββββββββββββ
Method: [Docker / Direct]
Process manager: [Docker / PM2]
Config: [ecosystem.config.js or docker-compose.yml]
ββ REVERSE PROXY ββββββββββββββββββββββββββ
Server: [Caddy / Nginx]
SSL: Let's Encrypt (auto-renewal)
Config: [Caddyfile or nginx.conf]
ββ DATABASE βββββββββββββββββββββββββββββββ
[PostgreSQL/Redis setup]
ββ MONITORING βββββββββββββββββββββββββββββ
Internal: [health check script + cron]
External: [uptime monitoring service]
Alerts: [notification channel]
ββ BACKUPS ββββββββββββββββββββββββββββββββ
Database: Daily, [retention] days
Snapshots: Weekly, last [N] kept
Off-site: [storage solution]
ββ HARDENING ββββββββββββββββββββββββββββββ
[checklist with status]
ββ DEPLOYMENT COMMANDS ββββββββββββββββββββ
[quick reference for common operations]