An automated, hardened, and containerized multi-site web hosting environment deployed on Hetzner Cloud. This infrastructure serves isolated static and dynamic applications behind a single Nginx reverse proxy utilizing wildcard subdomain routing via Cloudflare DNS.
The environment is currently deployed, hardened, and fully accessible from the public internet:
- Public Server IP:
157.180.42.147(Helsinki, Finland) - Static Website 1: http://site1.leoleiden.xyz
- Static Website 2: http://site2.leoleiden.xyz
- Dynamic PHP App: http://php.leoleiden.xyz (Displays real-time UTC timestamp, PHP version, and container hostname)
All three distinct web applications operating concurrently through a single ingress gateway:
The underlying virtual machine is hosted on an Ubuntu 24.04 LTS instance utilizing the cost-optimized CX23 x86 architecture (2 vCPU, 4 GB RAM, 40 GB NVMe SSD) located in the eu-central (Helsinki) availability zone.
Subdomain traffic is routed via Cloudflare DNS. Three explicit A records map the subdomains directly to the public IPv4 address 157.180.42.147. Proxying is intentionally set to DNS Only (Grey Cloud) to ensure pure, uncached HTTP requests reach the Docker reverse proxy directly during resilience evaluation.
+-------------------+
| Public Internet |
+---------+---------+
|
| HTTP (Port 80)
v
+-----------+-----------+
| UFW Firewall |
+-----------+-----------+
|
| Allow 80/tcp
v
+-----------------------------------------+-----------------------------------------+
| HETZNER CLOUD INSTANCE (157.180.42.147) |
| |
| +-----------------------------------------------------------------------------+ |
| | DOCKER BRIDGE NETWORK: hosting-net (Isolated) | |
| | | |
| | +---------------------+ | |
| | | nginx-proxy | <-- Single Public Ingress (0.0.0.0:80 -> 80) | |
| | +----------+----------+ | |
| | | | |
| | +-----------------------+-----------------------+ | |
| | | Host: site1 | Host: site2 | Host: php | |
| | v v v | |
| | +----------+----------+ +----------+----------+ +----------+----------+ | |
| | | site1-container | | site2-container | | php-container | | |
| | | (nginx:alpine) | | (nginx:alpine) | | (php:8.2-apache) | | |
| | +---------------------+ +---------------------+ +---------------------+ | |
| | Internal Port 80 Internal Port 80 Internal Port 80 | |
| +-----------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------+
- Single Public Ingress (
nginx-proxy): Nginx Alpine listening strictly on public port80:80. Acts as the sole entry point for all incoming HTTP traffic. - Zero Exposed Backend Ports: Backend containers (
site1,site2,php-app) do not publish ports to the host interface. All inter-service communication occurs strictly within the isolated Docker bridge network (hosting-net). - Decoupled Routing: Nginx routes requests dynamically based on HTTP
Hostheaders, ensuring complete network isolation and preventing direct external access to internal microservices.
The instance is secured following industry-standard Linux and Cloud Native hardening guidelines:
- Firewall (UFW): Configured with a strict Default Deny Incoming policy. Exclusively ports
22/tcp(OpenSSH) and80/tcp(HTTP) are allowed from the outside world. - SSH Authentication: Password-based login is permanently disabled in
/etc/ssh/sshd_config. Access is restricted strictly to cryptographic Ed25519 SSH keys. - Secret & Repository Hygiene: Zero hardcoded credentials,
.envfiles, or private keys are committed to the version control system.
To satisfy high-availability requirements, all containers are orchestrated with restart: unless-stopped policies. The environment is engineered to survive process crashes, graceful service terminations, and unexpected host reboots without manual intervention.
Simulating an unexpected application crash by terminating the primary Nginx process inside the container:
docker exec site2-container kill -15 1
docker psResult: The Docker daemon detects PID 1 termination and immediately revives the container within 1 second (Up 1 second).
Simulating an internal service shutdown using native application stop signals:
docker exec site2-container nginx -s stop
docker psResult: Upon graceful exit of the Nginx master process, the container is instantly resurrected by the orchestration engine (Up 2 seconds).
Simulating a hard server reboot or power-cycle event:
sudo reboot
# Wait 30 seconds -> Reconnect via SSH -> Check container state
docker psResult: Upon OS boot, Docker daemon automatically restores the entire hosting-net bridge network and starts all 4 containers concurrently (Up About a minute).
To deploy this environment from scratch on a clean Ubuntu 24.04 LTS instance:
sudo apt update
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw --force enable
sudo ufw status verbosecurl -fsSL [https://get.docker.com](https://get.docker.com) -o get-docker.sh
sudo sh get-docker.sh
docker --version
docker compose versiongit clone [https://github.com/leoleiden/multi-site-hosting.git](https://github.com/leoleiden/multi-site-hosting.git)
cd multi-site-hosting
docker compose up -d
docker compose psLeonid Lachman
DevOps & Data Engineer
- GitHub Repository: https://github.com/leoleiden/multi-site-hosting
- Tech Stack: Docker, Docker Compose, Nginx Reverse Proxy, Hetzner Cloud, Ubuntu Linux, Cloudflare DNS, UFW Hardening, Bash/PowerShell Scripting.









