Production-ready single-server platform for hosting multiple projects. Provisions a Hetzner Cloud server with Docker Swarm, Traefik reverse proxy, and DNS records for N domains — all via a single
hcloudprovider. Each project deploys itself via its own CI/CD.
Single hcloud provider (v1.56+)
|-- hcloud_server -> cx43 (16 GB, 8 vCPU)
|-- hcloud_firewall -> ports 22, 80, 443 + Docker Swarm
|-- hcloud_ssh_key -> SSH access
|-- hcloud_volume -> optional extra disk (/mnt/data)
|-- hcloud_zone_rrset (A) -> vreshch.com, diffractwd.com, mcpxhub.io, crystallography.io
|-- hcloud_zone_rrset (A) -> dev.mcpxhub.io (subdomain)
|-- hcloud_zone_rrset (CNAME) -> www.vreshch.com, www.diffractwd.com
\-- hcloud_zone_rrset (CNAME) -> traefik.X, swarmpit.X, logs.X (admin subdomains)
Infrastructure repo: terraform apply -> server + DNS + Traefik + Swarmpit + Dozzle
Each project repo: GitHub Actions -> build image -> SSH -> docker stack deploy
This repo only provisions the platform. Each project (vreshch.com, mcpxhub.io, etc.) has its own docker-compose.yml and CI/CD pipeline that deploys to this server.
- Single Provider —
hcloudhandles both compute and DNS (no third-party DNS provider) - Multi-Domain DNS — A records for N domains, all pointing to the same server
- Optional Volume — Extra disk for large datasets (e.g., crystallography.io COD data)
- Secure by Default — Firewall (SSH + HTTP/S only), automatic SSL via Let's Encrypt, bcrypt auth
- Built-in Monitoring — Traefik dashboard, Swarmpit UI, Dozzle logs
- No App Stacks — Each project deploys itself; this repo is infrastructure only
- Hetzner Cloud account with API token
- Domain zones already created in Hetzner DNS
- Terraform >= 1.12
git clone https://github.com/YOUR_USERNAME/infrastructure.git
cd infrastructure
# Interactive setup — collects all config, generates password hash
./scripts/setup-fill-tfvars.sh prodcd terraform
terraform init
terraform plan -var-file="terraform.prod.tfvars"
terraform apply -var-file="terraform.prod.tfvars"- Traefik Dashboard:
https://traefik.yourdomain.com - Swarmpit UI:
https://swarmpit.yourdomain.com - Dozzle Logs:
https://logs.yourdomain.com
# Single Hetzner Cloud token (compute + DNS)
hetzner_token = "your-cloud-api-token"
# Domains (zones must already exist in Hetzner DNS)
domains = [
{ name = "vreshch.com", enable_www = true },
{ name = "diffractwd.com", enable_www = true },
{ name = "mcpxhub.io", enable_www = false, subdomains = ["dev"] },
{ name = "crystallography.io", enable_www = false },
]
# Admin services — subdomains on any domain you own
admin_domain = "vreshch.com"
traefik_host = "traefik.vreshch.com"
swarmpit_host = "swarmpit.vreshch.com"
dozzle_host = "logs.vreshch.com"
traefik_acme_email = "admin@vreshch.com"
# Server
server_name = "prod-server"
server_type = "cx43"
location = "nbg1"
# SSH keys
ssh_public_key = "ssh-ed25519 AAAAC3..."
ssh_private_key = "-----BEGIN OPENSSH PRIVATE KEY-----\n..."
# Auth (base64-encoded htpasswd)
admin_password_hash = "YWRtaW46JDJ5JDA1JC4uLg=="
# Optional: extra disk for large datasets
enable_volume = true
volume_size = 100
volume_mount_path = "/mnt/data"Each entry in domains creates:
- An A record for
@pointing to the server IP - A www CNAME (if
enable_www = true) - A records for each subdomain in
subdomainslist
Admin subdomains (traefik_host, swarmpit_host, dozzle_host) are created as CNAMEs on the admin_domain.
| Type | vCPU | RAM | Storage | Recommended For |
|---|---|---|---|---|
| cx23 | 2 | 4 GB | 40 GB | Development |
| cx33 | 4 | 8 GB | 80 GB | Staging |
| cx43 | 8 | 16 GB | 160 GB | Production |
| cx53 | 16 | 32 GB | 320 GB | High Traffic |
.
|-- terraform/ # Terraform infrastructure code
| |-- main.tf # Compute module + DNS resources (hcloud_zone_rrset)
| |-- variables.tf # Variable definitions (domains, volume, etc.)
| |-- outputs.tf # Output definitions
| |-- versions.tf # Provider configuration (hcloud ~> 1.56)
| \-- modules/
| \-- compute/ # Server, firewall, SSH key, volume
| |-- main.tf
| |-- variables.tf
| |-- outputs.tf
| \-- scripts/ # Server initialization scripts
| |-- init-docker.sh
| |-- init-docker-swarm.sh
| \-- deploy-services.sh
|-- configs/ # Environment configuration examples
| |-- dev.example.tfvars
| \-- prod.example.tfvars
|-- scripts/ # Automation scripts
| |-- setup-env.sh # Interactive environment setup
| |-- setup-fill-tfvars.sh # One-step config + password generation
| |-- deploy-env.sh # Multi-environment deployment
| \-- utils/
| |-- generate-ssh-keys.sh
| |-- generate-password.sh
| \-- validate-config.sh
|-- docs/ # Documentation
\-- README.md
This infrastructure repo does not manage application stacks. Each project has its own deployment:
# Example: project's GitHub Actions workflow
- name: Deploy to server
run: |
ssh root@${{ secrets.SERVER_IP }} << 'EOF'
docker stack deploy -c docker-compose.yml myapp
EOFThe only shared resource is the traefik-public overlay network, which Traefik uses to discover and route to services. Each project's docker-compose.yml should attach its public-facing service to this network and add Traefik labels.
| Script | Description |
|---|---|
setup-fill-tfvars.sh <env> |
Interactive config + password hash generation |
setup-env.sh <env> |
Interactive or template-based env setup |
deploy-env.sh <env> <action> |
Deploy/plan/destroy with backend selection |
utils/generate-ssh-keys.sh |
Generate SSH key pairs |
utils/generate-password.sh |
Generate bcrypt password hashes |
utils/validate-config.sh |
Validate tfvars before deployment |
- Firewall: Only ports 22, 80, 443 exposed (Swarm ports restricted to private networks)
- SSL/TLS: Automatic certificates via Let's Encrypt
- Authentication: Admin tools protected with bcrypt password hashing
- SSH: Key-based authentication only
- Secrets: Never committed to Git (.gitignore configured)
Terraform init fails
cd terraform/
rm -rf .terraform/ .terraform.lock.hcl
terraform initDNS not resolving
# Check DNS records
dig +short yourdomain.com
# Verify zone exists in Hetzner DNS consoleServices not accessible
ssh root@YOUR_SERVER_IP
docker service ls
docker service logs traefikThis project is licensed under the MIT License - see the LICENSE file for details.