Pull request previews on Hetzner Cloud
Run each pull request's stagelet on its own server in your Hetzner Cloud project. See what stagely setup creates, the registry you bring and the iptables rules.
Stagely gives each GitHub pull request a stagelet: a private, running copy of your app. On Hetzner Cloud, each stagelet runs on its own server in your project. You run stagely setup hetzner once. After that, the customer agent creates a server when a pull request deploys, and deletes it when the stagelet ends.
What setup creates
Setup applies a Terraform module with OpenTofu or Terraform. It creates these resources in the location that you choose:
| Resource | Details |
|---|---|
| Private network | 10.42.0.0/20, for the hosts, stagelet machines and build workers |
| Agent host | Server of type cx23, Ubuntu 24.04, with a public IPv4 address |
| Gateway host | Server of type cx23, Ubuntu 24.04, with a primary IP that stays when the gateway server is replaced |
| Firewalls | One for each host, and one with no rules for stagelet machines and build workers |
| SSH key | The key that setup uses to install the agent and the gateway |
Hetzner Cloud has no container registry, so setup creates none. You bring your own. See Registry.
Credentials
Setup needs two sets of credentials:
- An API token of the Hetzner Cloud project with read and write access, in
HCLOUD_TOKEN. - A user name and password for your registry, in
-registry-usernameandSTAGELY_REGISTRY_PASSWORD.
Both stay on your computer and on the agent host. On the agent host, they are in /etc/stagely/stagely-agent.env, which only root can read. The API token never goes to a machine that runs your app. The registry password does: see Registry.
Stagelet machines and build workers
- The agent creates one server for each stagelet. Its type is
cx23. Setup has no flag to change this type. - The agent keeps two build workers, also of type
cx23. They stay when stagelets come and go. - Each server starts from Ubuntu 24.04, and cloud-init installs Docker at the first boot. With
-golden-image, servers start from a snapshot that already has Docker. Thestagelyrepository has the Packer template for this snapshot. - Each server joins the private network. The agent connects to it there over SSH, and accepts only the host key that it gave the server.
- Each server keeps a public IPv4 address, but only to reach registries, package mirrors and the internet. It accepts no connections on that address.
- A Hetzner network holds at most 100 servers. With the agent host and the gateway host, an environment runs at most 98 stagelet machines and build workers at a time.
Registry
Build workers push images to a registry that you already run, such as Docker Hub, GitHub Container Registry, GitLab’s registry, a registry on another cloud, or CNCF Distribution on your own server. Pass the image name prefix in -registry, such as registry.example.com/stagely or ghcr.io/example.
Every machine signs in with the user name and password that you gave setup. This includes stagelet machines, which run the code of each pull request. So a password that can push images is on each stagelet machine. On the other clouds, a stagelet machine gets short-lived credentials that can only pull.
Use a registry token that can push and pull only under the -registry prefix, and nothing else.
Isolation
Hetzner’s firewalls filter only public interfaces. Traffic inside a private network passes them unfiltered. So each machine adds iptables rules at every boot, before Docker starts:
- A stagelet machine or build worker accepts new connections from the network only from the agent host and the gateway host.
- Containers cannot open connections to the private network. So a stagelet cannot reach another stagelet, the agent host or the gateway host there.
- The agent host and the gateway host accept no new connections from the private network.
On the public interfaces:
- The firewall of stagelet machines and build workers has no rules, so they accept no connections there.
- The agent host accepts only SSH, and only from your
-operator-cidrranges. - The gateway host accepts ports 80 and 443 from anywhere, and SSH from your ranges.
On each server, containers also cannot open new connections to the metadata service at 169.254.169.254.
Run setup
export HCLOUD_TOKEN=<token>
export STAGELY_REGISTRY_PASSWORD=<registry password>
stagely login
stagely setup hetzner \
-environment production \
-location fsn1 \
-stagelet-domain stagelets.example.com \
-registry registry.example.com/stagely \
-registry-username stagely \
-operator-cidr 203.0.113.7/32 \
-binaries bin/linux-amd64 \
-acme-email ops@example.com
| Flag | Meaning |
|---|---|
-location |
Hetzner location, required |
-registry |
Image name prefix in your container registry, required |
-registry-username |
Registry user name. The password comes from STAGELY_REGISTRY_PASSWORD. |
-golden-image |
Snapshot id of a golden image for stagelet machines and build workers |
Setup also takes the flags that every cloud takes. At the end, it prints the gateway’s primary IP. Point DNS at the gateway with an A record.
After setup
- Add
.stagely.ymlto your repository. - Add config values and secrets.
- Read the Security model before you add real secrets.