Deploy to Hetzner Cloud¶
Hetzner Cloud is the cheapest credible EU-sovereign option. Data centres: Falkenstein (DE), Nuremberg (DE), Helsinki (FI). GDPR- friendly by default; ISO 27001 certified.
OSS v0.1 is not one-click
The instructions below are a manual flow. Commercial edition ships a
tested Terraform module that does all of this in one
terraform apply. See
strategy/04_ONE_CLICK_EU_DEPLOY.md
for the commercial design.
Step 1 — provision a server¶
Use the Hetzner Cloud Console or the
hcloud CLI.
Recommended:
- Image: Ubuntu 24.04 LTS
- Type:
CCX13(dedicated vCPU, 2 vCPU, 8 GB RAM, 80 GB NVMe) — €14.39/mo, plenty for a single-tenant demo or small production. - Location: Falkenstein or Helsinki (whichever is closer to your users).
- Firewall: allow 22 (from your IP), 80, 443.
With hcloud:
hcloud server create \
--type ccx13 \
--image ubuntu-24.04 \
--name lex-custis-prod \
--location fsn1 \
--ssh-key your-ssh-key-name
Takes ~30 seconds.
Step 2 — bootstrap the server¶
SSH in:
Install Docker + Compose v2:
Create a non-root user:
Step 3 — clone + install¶
git clone https://github.com/vbalagovic/lex-custis.git lex-custis
cd lex-custis
./install.sh # Mistral path
# or
./install.sh --with-ollama # self-hosted, requires a GPU for 7B+ models
Takes ~10 minutes. When it's done, the stack is running on
localhost:3000 (frontend) and localhost:8081 (backend). You still
need to put TLS in front.
Step 4 — TLS with Caddy¶
The repo includes a Caddyfile that auto-provisions Let's Encrypt
certs when $DOMAIN is set. Add to your .env:
Update DNS so lexcustis.yourdomain.eu points at your Hetzner server's
public IP.
Run Caddy:
docker run -d --name caddy --restart unless-stopped \
--network lex-custis_default \
-p 80:80 -p 443:443 \
-v ./Caddyfile:/etc/caddy/Caddyfile:ro \
-v caddy_data:/data \
caddy:2-alpine
First request on :443 triggers the cert provisioning. docker logs
caddy -f to watch.
Step 5 — firewall¶
In the Hetzner Cloud Console, attach a firewall that allows:
- 22/tcp from your SSH source IPs only.
- 80/tcp from the world (for HTTP-01 challenge).
- 443/tcp from the world.
- Outbound: everything (Docker pulls, Mistral API calls, etc.).
Step 6 — backups¶
Install the Hetzner Storage Box or use a Hetzner Object Storage bucket in the same region:
# Nightly at 03:00 Europe/Berlin
0 3 * * * cd /home/lexcustis/lex-custis && docker compose exec -T postgres pg_dump -U lexcustis lexcustis | gzip | sshfs ...
Commercial edition automates this + Qdrant snapshots.
Step 7 — monitoring¶
Minimum: UptimeRobot on https://lexcustis.yourdomain.eu/health. If
the check fails for 3 consecutive 60-second windows, alert a pager.
Hetzner-specific tips¶
- Use the private network feature if you expand to multi-server: backend on one, DB on another.
- The Load Balancer product can terminate TLS and forward to N backend servers — useful for >100 concurrent users.
- Hetzner's Snapshots feature ≠ Postgres backups — it's VM-level. Cheap, useful for rollback, but still do app-level backups.
- Backups feature in Hetzner Cloud Console does daily VM backups automatically for +20% of server cost. Turn it on for belt-and- braces.
Common pitfalls¶
- GPU models. Hetzner has dedicated GPU line (
GEX44, €430/mo) if you want to self-host 13B+ Ollama models. For 7B and smaller, a CCX33 CPU box is fine. - Inbound rate limit. Hetzner rate-limits inbound bandwidth at 1 Gbps — irrelevant for a chat app, worth knowing for bulk-import flows.
- Falkenstein is the most popular DC; Helsinki has more headroom. If you see slot unavailability at provisioning time, try Helsinki.
When to graduate off Hetzner¶
At ~200 concurrent users you start wanting:
- Dedicated Postgres (RDS-style managed)
- Dedicated Qdrant cluster
- Multiple backend replicas with a load balancer
At that point either scale up on Hetzner Cloud (bigger dedicated instances, managed Postgres) or move to Scaleway/OVH managed services — or contact us for the commercial edition where we operate the whole stack for you on EU-sovereign infra.