This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

cleanup

Jakob Ankarhem (Jun 8, 2026, 1:51 PM +0200) 872d2cf0 60d7aa69

+16 -27
+16 -27
content/posts/deploying-this-blog.md
··· 13 13 14 14 The setup involves two repositories: 15 15 16 - - **[`ankarhem/site`](https://github.com/ankarhem/site)** — the blog source. Zola static site, a Nix flake for building and deploying it. 17 - - **[`ankarhem/nix-config`](https://github.com/ankarhem/nix-config)** — my system configuration. NixOS modules for every service running on the homelab, including the nginx vhost and the blog content itself as a flake input. 16 + - **[`ankarhem/site`](https://github.com/ankarhem/site)** - the blog source. Zola static site, a Nix flake for building and deploying it. 17 + - **[`ankarhem/nix-config`](https://github.com/ankarhem/nix-config)** - my system configuration. NixOS modules for every service running on the homelab, including the nginx vhost and the blog content itself as a flake input. 18 18 19 19 The connection point is that `nix-config` pulls the blog in as a flake input, and the blog's flake knows how to deploy itself to the homelab using deploy-rs. The flake input is used to provide the initial "seed" version. Subsequent versions come from the CI/CD pipeline. 20 20 ··· 49 49 50 50 ## deploy-rs 51 51 52 - [deploy-rs](https://github.com/serokell/deploy-rs) is a Nix-aware deployment tool. The key idea is that it builds a *profile* — an activation script bundled with a store path — and pushes it to a remote machine. For a static site there's no service to restart, so the activation is just swapping a symlink: 52 + [deploy-rs](https://github.com/serokell/deploy-rs) is a Nix-aware deployment tool. The key idea is that it builds a *profile* - an activation script bundled with a store path - and pushes it to a remote machine. For a static site there's no service to restart, so the activation is just swapping a symlink: 53 53 54 54 ```nix 55 55 flake.deploy.nodes.homelab = { ··· 80 80 81 81 The homelab's configuration has two small modules for this. 82 82 83 - **`modules/deploy.nix`** — a generic module you apply to any host you want to make deployable via deploy-rs. It adds the CI key to root's authorized keys: 83 + **`modules/deploy.nix`** - a generic module you apply to any host you want to make deployable via deploy-rs. It adds the CI key to root's authorized keys: 84 84 85 85 ```nix 86 86 { ··· 92 92 } 93 93 ``` 94 94 95 - **`modules/blog.nix`** — sets up the web root and the nginx vhost: 95 + **`modules/blog.nix`** - sets up the web root and the nginx vhost: 96 96 97 97 ```nix,hl_lines=7-24 98 98 # This is the format of a flake-parts module for NixOS. ··· 122 122 } 123 123 ``` 124 124 125 - The `systemd.tmpfiles` `L` rule creates the symlink at boot if it doesn't exist yet, pointing at the build that was current when the system last rebuilt. That means on a fresh deploy of nix-config, the site is immediately live with whatever version of the blog was locked in `flake.lock` at that time — before any GitHub Action has run. 125 + The `systemd.tmpfiles` `L` rule creates the symlink at boot if it doesn't exist yet, pointing at the build that was current when the system last rebuilt. That means on a fresh deploy of nix-config, the site is immediately live with whatever version of the blog was locked in `flake.lock` at that time - before any GitHub Action has run. 126 126 127 127 When deploy-rs runs later, it replaces that symlink with the new build. Next `nixos-rebuild switch` won't touch it because tmpfiles `L` only creates the symlink if it's absent. 128 128 ··· 136 136 137 137 ## The GitHub Actions workflow 138 138 139 - The workflow runs on push to `main` and on manual dispatch. It needs to reach the homelab, which isn't publicly exposed — the blog's nginx port is, but SSH isn't. The connection goes through [Tailscale](https://tailscale.com/). 139 + The workflow runs on push to `main` and on manual dispatch. It needs to reach the homelab, which isn't publicly exposed - the blog's nginx port is, but SSH isn't. The connection goes through [Tailscale](https://tailscale.com/). 140 140 141 141 ```yaml 142 142 - name: Connect to Tailscale ··· 156 156 run: nix develop --command deploy .#homelab.blog 157 157 ``` 158 158 159 - The deploy-rs CLI runs inside the flake's devShell, which pins it to the same version locked in `flake.lock`. That matters because the CLI and the `activate` lib used in the flake need to match — using a system-installed deploy-rs of a different version can cause subtle failures. 159 + The deploy-rs CLI runs inside the flake's devShell, which pins it to the same version locked in `flake.lock`. That matters because the CLI and the `activate` lib used in the flake need to match - using a system-installed deploy-rs of a different version can cause subtle failures. 160 160 161 161 The three secrets needed in the repository: 162 - - `TS_OAUTH_CLIENT_ID` and `TS_OAUTH_SECRET` — from a Tailscale OAuth client scoped to `tag:ci` with write access to `auth_keys`. 163 - - `BLOG_DEPLOY_SSH_KEY` — the private half of the ed25519 key whose public half is committed in `modules/deploy.nix`. 162 + - `TS_OAUTH_CLIENT_ID` and `TS_OAUTH_SECRET` - from a Tailscale OAuth client scoped to `tag:ci` with write access to `auth_keys`. 163 + - `BLOG_DEPLOY_SSH_KEY` - the private half of the ed25519 key whose public half is committed in `modules/deploy.nix`. 164 164 165 - This is a root SSH key living in GitHub secrets, which is the biggest attack-surface item in the setup. It's bounded by the fact that SSH is never publicly exposed — reaching the host at all requires being on the tailnet, and the only way onto the tailnet is the ephemeral `tag:ci` node, which exists for the duration of the job and is scoped to a single host. 165 + This is a root SSH key living in GitHub secrets, which is the biggest attack-surface item in the setup. It's bounded by the fact that SSH is never publicly exposed - reaching the host at all requires being on the tailnet, and the only way onto the tailnet is the ephemeral `tag:ci` node, which exists for the duration of the job and is scoped to a single host. 166 166 167 - ## What actually happens on a push 167 + ## Why not just use a flake input? 168 168 169 - 1. The workflow checks out the repo with `submodules: recursive`. 170 - 2. Nix is installed via `cachix/install-nix-action`. 171 - 3. The runner joins the Tailscale tailnet as an ephemeral `tag:ci` node. 172 - 4. SSH is configured with the deploy key. 173 - 5. The workflow polls until `ssh homelab true` succeeds (usually one attempt). 174 - 6. `nix develop --command deploy .#homelab.blog` runs. deploy-rs evaluates `self.packages.x86_64-linux.blog`, builds it (or fetches it from cache), copies the store path to the homelab, and runs the activation script. 175 - 7. `/var/www/ankarhem.dev` is atomically repointed to the new `public/` directory. 176 - 8. The Tailscale node exits and is removed from the tailnet. 169 + The blog is already a flake input in nix-config - that's how the NixOS module gets the store path for the initial symlink. So why add deploy-rs on top of it? 177 170 178 - The whole thing takes about 60 seconds from push to live, most of which is building Zola and the site. On a warm cache it's faster. 179 - 180 - ## Why this over the simple approach 181 - 182 - The obvious alternative is making the blog a flake input in nix-config and letting the existing daily `auto-upgrade` service pull it. That's zero extra attack surface — no SSH key in CI, no Tailscale in the workflow, no deploy-rs machinery. The tradeoff is latency: up to 24 hours between a push and going live. 171 + The flake input is only updated when `nix-config` itself is rebuilt. My homelab runs a daily `auto-upgrade` service, so in practice a new blog post could sit up to 24 hours before going live. For a personal blog that's probably fine, but I want the same pipeline for other projects too, and for those the latency matters more. 183 172 184 - For a personal blog that's probably fine. I went with deploy-rs because I want the same pipeline for other projects too, and for those the latency matters more. The `deploy.nix` module is generic — adding the next project's CI key is one line. The homelab already has everything it needs. 173 + deploy-rs gives me push-to-deploy in about a minute, while the flake input still serves as the seed for fresh system deploys. The `deploy.nix` module is generic - adding the next project's CI key is one line. The homelab already has everything it needs. 185 174 186 175 ## The nix-config module system 187 176 188 - One thing worth noting: `nix-config` uses [flake-parts](https://flake.parts/) with [import-tree](https://github.com/vic/import-tree) to auto-discover modules. Every `.nix` file under `modules/` is automatically imported. Adding `blog.nix` and `deploy.nix` to that directory is all it takes to wire them into the system — no explicit import list to update. 177 + One thing worth noting: `nix-config` uses [flake-parts](https://flake.parts/) with [import-tree](https://github.com/vic/import-tree) to auto-discover modules. Every `.nix` file under `modules/` is automatically imported. Adding `blog.nix` and `deploy.nix` to that directory is all it takes to wire them into the system. 189 178 190 179 The modules use `flake.modules.nixos.<name>` which is a flake-parts convention for defining NixOS modules that get composed into the system configuration. `blog.nix` receives `inputs` as an argument (the flake inputs, including the locked blog source), which is how it gets the store path for the `tmpfiles` symlink. 191 180