---
title: "Pull request previews on Hetzner Cloud"
description: "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."
url: https://stagely.dev/docs/setup/cloud/hetzner
---

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](#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-username` and `STAGELY_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](#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. The `stagely` repository 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-cidr` ranges.
- 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

```sh
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](https://stagely.dev/docs/setup/cloud.md#flags-every-cloud-takes). At the end, it prints the gateway's primary IP. [Point DNS at the gateway](https://stagely.dev/docs/setup/cloud.md#point-dns-at-the-gateway) with an A record.

## After setup

1. [Add `.stagely.yml`](https://stagely.dev/docs/app/stagely-yml.md) to your repository.
2. [Add config values and secrets](https://stagely.dev/docs/setup/config.md).
3. Read the [Security model](https://stagely.dev/docs/security.md) before you add real secrets.
