4.6 KiB
title, summary, date, authors, tags, content_meta
| title | summary | date | authors | tags | content_meta | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Building a Deterministic, Self-Hosted Build Pipeline on NixOS | Taking control of reliability and privacy by implementing a private, fully controlled CI/CD pipeline that eliminates external dependencies. | 2026-01-08 |
|
|
|
Building a Deterministic, Self-Hosted Build Pipeline on NixOS
In modern DevOps, the "path of least resistance" usually involves a mix of GitHub Actions, Docker Hub, and managed hosting. While efficient, this approach often treats the build environment as a "black box," introducing external dependencies and privacy trade-offs. To deepen my understanding of infrastructure and gain total control over my deployment lifecycle, I migrated my infrastructure to a fully self-hosted, declarative stack on NixOS.
The journey began with a migration from a standard Debian VPS on Digital Ocean. Using the nixos-consume script, I bootstrapped NixOS directly onto the live instance, replacing a mutable, manually-managed OS with a deterministic one. My entire server is now defined via Nix Flakes, allowing me to manage Nginx, Gitea, and my CI/CD runners from a single, version-controlled source of truth.
The Architecture: Gitea, Podman, and Nginx
At the center of this ecosystem is Gitea, which handles both source control and CI/CD via Gitea Actions. For the container runtime, I opted for Podman, integrated directly into my NixOS configuration. Nginx acts as the frontend, utilizing the ACME module for automated SSL certificate renewals.
One of the most significant technical challenges was the creation of a custom OCI image for my runners. Rather than pulling generic images from the internet, I used Nix’s dockerTools.buildLayeredImage to "bake" a specialized environment containing only the tools I need: Hugo, Go, Node.js, and Python’s uv.
# Snippet from runner-image.nix
runnerImage = pkgs.dockerTools.buildLayeredImage {
name = "localhost/site-builder";
tag = "latest";
contents = with pkgs; [ bashInteractive nodejs_20 hugo go uv rsync openssh cacert ];
fakeRootCommands = ''
mkdir -p etc/ssl/certs
ln -s ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt etc/ssl/certs/ca-certificates.crt
echo "root:x:0:0:root:/root:/bin/bash" > etc/passwd
'';
};
Solving the "Minimal Image" Hurdles
Building a container image from scratch in Nix results in an extremely minimal environment. This led to several interesting DevOps puzzles. For instance, rsync and ssh initially failed because the naked image lacked a /etc/passwd file, leaving the system unable to identify the current user. Similarly, the Go compiler and Git struggled with SSL verification because they expected certificates at hardcoded Linux paths.
I solved these by using fakeRootCommands to inject a standard user structure and symlink the Nix-provided certificates into /etc/ssl/certs. This ensured that the polyglot toolchain—Node.js for search indexing, Go for Hugo modules, and Python for resume rendering—all shared a consistent, secure environment.
The Deployment Pipeline
With the custom runner image loaded into Podman, the deployment pipeline is remarkably lean. Because the image is pre-baked with all dependencies, the Gitea Action doesn't spend time downloading packages or setting up environments. It simply clones the repo, builds the site, and uses rsync to move the static files to /var/www.
# deploy.yaml snippet
- name: Build with Hugo
run: hugo --minify --baseURL "https://ejs.cam/"
- name: Deploy via Rsync
run: |
ssh-keyscan -p "$REMOTE_PORT" "$REMOTE_HOST" >> ~/.ssh/known_hosts
rsync -avz --delete --exclude 'cv.pdf' ./public/ user@host:/var/www/
This setup protects specific files, like my CV (generated by a separate rendercv pipeline), while ensuring the rest of the site is updated atomically.
Conclusion and Future Outlook
By self-hosting this entire stack, I’ve achieved a build-and-deploy cycle that is both private and incredibly fast, with zero reliance on external service uptimes. The next step in this project is migrating my plaintext secrets into an encrypted management system like sops-nix. This will allow me to publish my entire NixOS configuration publicly, serving as a fully transparent and reproducible blueprint for declarative DevOps.
This project was more than just a hosting exercise; it was a deep dive into the "plumbing" of the modern web, proving that with the right tools, it’s possible to build professional-grade infrastructure that is both autonomous and easy to maintain.