diff --git a/content/posts/deploying-this-blog.md b/content/posts/deploying-this-blog.md index 182dd21..9e4b7ed 100644 --- a/content/posts/deploying-this-blog.md +++ b/content/posts/deploying-this-blog.md @@ -13,8 +13,8 @@ This blog runs on a server in my apartment. It's a NixOS homelab box that also r The setup involves two repositories: -- **[`ankarhem/site`](https://github.com/ankarhem/site)** — the blog source. Zola static site, a Nix flake for building and deploying it. -- **[`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. +- **[`ankarhem/site`](https://github.com/ankarhem/site)** - the blog source. Zola static site, a Nix flake for building and deploying it. +- **[`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. 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. @@ -49,7 +49,7 @@ The output is a store path with a `public/` directory inside containing the rend ## deploy-rs -[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: +[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: ```nix flake.deploy.nodes.homelab = { @@ -80,7 +80,7 @@ checks = lib.optionalAttrs (system == "x86_64-linux") ( The homelab's configuration has two small modules for this. -**`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: +**`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: ```nix { @@ -92,7 +92,7 @@ The homelab's configuration has two small modules for this. } ``` -**`modules/blog.nix`** — sets up the web root and the nginx vhost: +**`modules/blog.nix`** - sets up the web root and the nginx vhost: ```nix,hl_lines=7-24 # This is the format of a flake-parts module for NixOS. @@ -122,7 +122,7 @@ The homelab's configuration has two small modules for this. } ``` -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. +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. 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. @@ -136,7 +136,7 @@ The `?submodules=1` is needed because the theme is a git submodule and nix-confi ## The GitHub Actions workflow -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/). +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/). ```yaml - name: Connect to Tailscale @@ -156,36 +156,25 @@ After joining the tailnet, the workflow writes an SSH config pointing `homelab` run: nix develop --command deploy .#homelab.blog ``` -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. +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. The three secrets needed in the repository: -- `TS_OAUTH_CLIENT_ID` and `TS_OAUTH_SECRET` — from a Tailscale OAuth client scoped to `tag:ci` with write access to `auth_keys`. -- `BLOG_DEPLOY_SSH_KEY` — the private half of the ed25519 key whose public half is committed in `modules/deploy.nix`. +- `TS_OAUTH_CLIENT_ID` and `TS_OAUTH_SECRET` - from a Tailscale OAuth client scoped to `tag:ci` with write access to `auth_keys`. +- `BLOG_DEPLOY_SSH_KEY` - the private half of the ed25519 key whose public half is committed in `modules/deploy.nix`. -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. +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. -## What actually happens on a push +## Why not just use a flake input? -1. The workflow checks out the repo with `submodules: recursive`. -2. Nix is installed via `cachix/install-nix-action`. -3. The runner joins the Tailscale tailnet as an ephemeral `tag:ci` node. -4. SSH is configured with the deploy key. -5. The workflow polls until `ssh homelab true` succeeds (usually one attempt). -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. -7. `/var/www/ankarhem.dev` is atomically repointed to the new `public/` directory. -8. The Tailscale node exits and is removed from the tailnet. +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? -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. +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. -## Why this over the simple approach - -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. - -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. +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. ## The nix-config module system -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. +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. The modules use `flake.modules.nixos.` 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.