# nixos-hetzner-demo
Continuous, rapid NixOS deployments to Hetzner Cloud with FlakeHub and OpenTofu
## Continuous, rapid, NixOS deployments to Hetzner Cloud
# Continuous, rapid, NixOS deployments to Hetzner Cloud
OpenTofu.
>
> [(https://img.shields.io/badge/doc-outskirtslabs-orange.svg)]
> https://docs.outskirtslabs.com/open-source-vital-signs#static[image:https://img.shields.io/badge/status-static-blue.svg[status:
> static]]
>
>
- 📌 NOTE
-
> This repo was based on https://determinate.systems[Determinate
> Systems]'s https://github.com/determinatesystems/demo[repo for AWS
> AMIs]. This is a proof-of-concept repo maintained by me and not DetSys.
> I use something like this in prod, so it works, but don’t expect this
> repo to be updated regularly.
>
>
> This project shows you how to continuously deploy a
> [NixOS](https://zero-to-nix.com/concepts/nixos) configuration to a
> [Hetzner Cloud](https://www.hetzner.com/cloud) server using
> [OpenTofu](https://opentofu.org) and [FlakeHub](https://flakehub.com) in
> seconds.
>
> == Differences from the AWS demo
>
> This repo is a port of the
> [Determinate Systems AWS demo](https://github.com/determinatesystems/demo)
> to Hetzner Cloud. Key differences:
>
> | | | |
> | --- | --- | --- |
> | Aspect | AWS Demo | Hetzner Demo |
> | Demo app | EtherCalc (removed from nixpkgs) | CryptPad |
> | Base image | Pre-built AMIs from Determinate Systems | Custom image built from [nixos-hetzner](https://github.com/outskirtslabs/nixos-hetzner) and uploaded |
> | FlakeHub auth | IAM role (`determinate-nixd login aws`) | API token (`determinate-nixd login token`) |
> | Deployment method | AWS Systems Manager (SSM) | SSH with deploy key |
> | Networking | VPC, subnets, security groups | Simple firewall rules |
> | GitHub auth | OIDC federation with AWS | SSH deploy key as secret |
>
> The core FlakeHub workflow remains the same: build closures in CI,
> publish to FlakeHub Cache, and apply pre-built configurations in
> seconds.
>
> However there are a few caveats:
>
> Since Hetzner Cloud does not have a public marketplace for cloud images,
> you have to build the cloud image yourself (with the help of
> [nixos-hetzner](https://github.com/outskirtslabs/nixos-hetzner)) and
> upload it. Hetzner Cloud API keys are scoped to a project, so you will
> need to upload the image on a per-project basis.
>
> We rely on the tool
> [hcloud-upload-image](https://github.com/apricote/hcloud-upload-image) to
> perform the image creation. It can take awhile. In my experience
> uploading a 4GB image can take 8-12 minutes (assuming a fast pipe and
> bandwidth is not the bottleneck).
>
> If you build the image on a system using FlakeHub Cache, then the cloud
> image will be available to other systems from which you want to deploy
> (that are also using FlakeHub Cache).
>
> Once you have a NixOS image uploaded to HCloud, it is straightforward to
> create a nixos server from the snapshot. You can clickops at
> console.hetzner.com, use the hcloud cli tool, or use terraform/opentofu.
>
> In this demo we use opentofu to provision the server, firewall, ssh key,
> etc.
>
> The deployment process involves fetching a pre-built NixOS
> [closure](https://zero-to-nix.com/concepts/closures) from
> [FlakeHub](https://flakehub.com) and applying it to the Hetzner Cloud
> server, streamlining the deployment process and ensuring consistency
> across deployments.
>
> Timings (YMMV):
>
> | | |
> | --- | --- |
> | Step | Time |
> | Image upload | ~9 minutes |
> | Initial tofu apply | ~4 minutes |
> | - Hetzner resources creation | 3m5s |
> | - FlakeHub nixos provisioning | ~60s |
> | Subsequent deploy (GitHub Actions) | ~3 minutes |
> | - Build {plus} publish | 2m55s |
> | - fh apply via SSH | 14s |
>
> == Sign-up for the FlakeHub beta
>
> To experience this streamlined NixOS deployment pipeline for yourself,
> [sign up for the FlakeHub beta](https://determinate.systems) at
> https://determinate.systems. FlakeHub provides the enterprise-grade Nix
> infrastructure needed to fully use these advanced deployment techniques.
>
> == Prerequisites
>
> * Paid [Hetzner Cloud account](https://www.hetzner.com/cloud) with an API
> token
> * Paid [FlakeHub account](https://flakehub.com) with an API token
> * [Detsys Nix](https://docs.determinate.systems/determinate-nix) with
> flakes enabled
> * [OpenTofu](https://opentofu.org) (available in the dev shell)
> Project status:
> **[Static](https://docs.outskirtslabs.com/open-source-vital-signs#static)**.
>
> == Getting Started
>
> This demo deploys [CryptPad](https://cryptpad.net), a collaborative
> document editing platform, to a Hetzner Cloud server.
>
> Deployment is a two-step process:
>
> 1. [Manual deployment](#manual-deployment) - Run OpenTofu locally to
> create the server infrastructure (required first)
> 2. [Automated deployment](#automated-deployment-with-github-actions) -
> Once the server exists, push to GitHub for CI/CD deployments
> - 💡 TIP
-
> For a full rundown of how everything in the demo works, see
> [How it works](#how-it-works) below.
>
>
> === Manual deployment
>
> ==== 1. Build and upload the base image
>
> First, build and upload a NixOS base image to Hetzner Cloud. This only
> needs to be done once (or when you want to update the base image).
>
> ```shell
> # Enter the dev shell
> nix develop
>
> # Create .env file with your tokens (keeps them out of shell history)
> cat > .env << 'EOF'
> HCLOUD_TOKEN=your-hetzner-token
> FLAKEHUB_TOKEN=your-flakehub-token
> EOF
> chmod 600 .env
>
> # Load and export tokens, then build/upload the image
> set -a && source .env && set +a
> ./scripts/upload-image.sh
>
> # Note the image ID from the output (e.g., 123456789)
> ```
>
> ==== 2. Generate a deploy key
>
> Generate an SSH key for GitHub Actions deployments:
>
> ```shell
> ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
> ```
>
> ==== 3. Configure OpenTofu
>
> ```shell
> cd setup
>
> # Copy the example config
> cp terraform.tfvars.example terraform.tfvars
>
> # Edit terraform.tfvars with your values:
> # - hcloud_token: Your Hetzner Cloud API token
> # - hcloud_image_id: The image ID from step 1
> # - flakehub_token: Your FlakeHub API token
> # - deploy_ssh_public_key: Contents of deploy_key.pub
> # - deploy_ssh_private_key: Contents of deploy_key (private key, for first-boot provisioning)
> # - (optional) ssh_public_key: Your personal SSH key for manual access
> ```
>
> ==== 4. Create the infrastructure
>
> ```shell
> # Initialize OpenTofu
> tofu init
>
> # Validate the configuration
> tofu validate
>
> # Create the resources
> tofu apply -auto-approve
>
> # Get the website URL
> export CRYPTPAD_URL=$(tofu output --json | jq -r .website.value)
>
> # Open the website (wait ~60 seconds for first deployment)
> open "${CRYPTPAD_URL}"
>
> # (optional) When you're done, destroy the resources
> tofu destroy -auto-approve
> ```
>
> === Automated deployment with GitHub Actions
>
> Once the server exists, GitHub Actions can automatically deploy NixOS
> configuration changes. Configure the following repository secrets:
>
> | | |
> | --- | --- |
> | Secret | Description |
> | `DEPLOY++_++SSH++_++PRIVATE++_++KEY` | The contents of the `deploy++_++key` file (private key) |
> | `HETZNER++_++SERVER++_++IP` | The server IP from `tofu output server++_++ip` |
>
> Using the `gh` CLI after completing link:#manual-deployment[manual
> deployment]:
>
> ```shell
> # Create a production environment (optional)
> gh api repos/{owner}/{repo}/environments/production --method PUT
>
> # Set secrets
> cat deploy_key | gh secret set DEPLOY_SSH_PRIVATE_KEY
> tofu output -raw server_ip | gh secret set HETZNER_SERVER_IP
> ```
>
> The workflow will automatically build, publish to FlakeHub, and deploy
> on pushes to main.
>
> == Documentation
>
> * [Docs](https://docs.outskirtslabs.com/nixos-hetzner-demo/next/)
> * https://docs.outskirtslabs.com/nixos-hetzner-demo/next/api[API
> Reference]
> * https://github.com/outskirtslabs/nixos-hetzner-demo/issues[Support via
> GitHub Issues]
> == How it works
>
> Here’s a high level walkthrough of what’s going on
>
> Initial setup (one-time)
>
> 1. Build NixOS base image locally using
> [nixos-hetzner](https://github.com/outskirtslabs/nixos-hetzner)
> 2. Upload image to Hetzner Cloud via hcloud-upload-image
> 3. Generate SSH deploy key for GitHub Actions
> 4. Configure terraform.tfvars with tokens, image ID, and keys
> First deployment (tofu apply)
>
> 1. OpenTofu creates Hetzner resources (SSH key, firewall, server)
> 2. Server boots with base NixOS image (includes determinate-nixd and fh)
> 3. Provisioner SSHs into server
> 4. Authenticates with FlakeHub using `determinate-nixd login token`
> 5. Runs `fh apply nixos ++<++flakeref++>++` to pull config from FlakeHub
> 6. Server downloads pre-built closure and switches to new configuration
> Subsequent deployments (via GitHub Actions)
>
> 1. Developer pushes changes to flake.nix
> 2. CI builds the NixOS closure
> 3. flakehub-push publishes closure to FlakeHub (with output paths cached)
> 4. Deploy job SSHs to server with the exact flakeref
> 5. Server runs `fh apply nixos` - downloads closure from FlakeHub Cache
> 6. Configuration applied in seconds (no building on server)
> === Nix flake
>
> The [`flake.nix`](./flake.nix) defines the NixOS configuration for
> the demo system:
>
> * Inputs:
> * `nixpkgs`: Custom nixpkgs fork with Hetzner Cloud tools (can be
> reverted to upstream nixpkgs once
> [PR #375551](https://github.com/NixOS/nixpkgs/pull/375551) is merged
> * [`nixos-hetzner`](https://github.com/outskirtslabs/nixos-hetzner):
> Hetzner Cloud image building tools
> * `determinate`: Determinate Nix distribution from FlakeHub
> * `fh`: FlakeHub CLI from FlakeHub
> * Outputs:
> * `nixosConfigurations.cryptpad-demo`: The CryptPad server
> configuration
> * `devShells.default`: Development environment with required tools
> === OpenTofu configuration
>
> The [`setup/`](./setup/) directory contains OpenTofu configuration
> for Hetzner Cloud:
>
> * `providers.tf`: Hetzner Cloud provider configuration
> * `variables.tf`: Input variables (API tokens, image ID, SSH keys, etc.)
> * `main.tf`: Server, firewall, and SSH key resources
> * `outputs.tf`: Server IP and website URL
> After server creation, Terraform provisions the server via SSH:
>
> 1. Authenticates with FlakeHub using `determinate-nixd login token`
> 2. Applies the NixOS configuration using `fh apply nixos`
> === GitHub Actions workflow
>
> The [`.github/workflows/ci.yml`](./.github/workflows/ci.yml)
> workflow:
>
> 1. Build job: Builds the NixOS closure and publishes to FlakeHub
> 2. Deploy job: SSHs to the server and runs `fh apply nixos` with the new
> closure
> === Continuous deployment
>
> Continuous deployments work by:
>
> 1. Pushing changes to the `flake.nix`
> 2. GitHub Actions builds the new closure
> 3. The closure is published to FlakeHub Cache
> 4. The deploy job SSHs to the server and applies the new configuration
> To demonstrate, make a change to the CryptPad configuration in
> `flake.nix` and push the changes.
>
> === Triggering rollbacks
>
> Use the `workflow++_++dispatch` event to manually trigger a deployment
> of a previous version.
>
> Why FlakeHub?
>
> Applying fully evaluated NixOS closures via
> [FlakeHub](https://flakehub.com) differs from typical deployments using
> Nix in several key ways:
>
> _Deployment speed_
>
> * FlakeHub deployment: The NixOS configuration is evaluated and built
> ahead of time. As the closure is pre-built and cached, the deployment
> process is faster. The server only needs to download and apply the
> pre-built closure.
> * Typical Nix deployment: The evaluation and build process happens
> during deployment, which can be time-consuming.
> _Resource utilization_
>
> * FlakeHub deployment: Offloads the computationally intensive tasks of
> evaluation and building to a controlled environment (e.g., a CI/CD
> pipeline), freeing up resources on the target server.
> * Typical Nix deployment: The target server must handle the evaluation
> and build process, which can be resource-intensive.
> _Scalability_
>
> * FlakeHub deployment: The pre-built and cached nature allows for rapid
> instance provisioning, making it ideal for auto-scaling scenarios.
> * Typical Nix deployment: The time required for evaluation and building
> on each new instance can introduce significant delays.
> In summary, applying a fully evaluated NixOS closure from
> [FlakeHub](https://flakehub.com) ensures that the exact same configuration
> is deployed every time, as the closure is a fixed, immutable artifact.