
i built a webapp for a college project a while back. it uses sqlite, server-side templates, nothing fancy. and to deploy it i was using docker and jenkins but it was kinda heavy on the system as i was also building the docker image on the same system instead of pulling it from docker hub (i just wanted to test something). i wanted something lighter
so i started looking into nix. not nixos, just nix the package manager. the idea was simple: build the binary on the server, no go toolchain required, reproducible every time. and with a self-hosted github actions runner and systemd for service management, i thought i could get push-to-deploy working without too much complexity
the setup
the app lives on a linux server with nix installed in multi-user mode, a github actions self-hosted runner registered to the repo, and a systemd service for the app. as for the deploy flow:
- push to
master - github actions copies the repo to a release directory on the server
- a deploy script runs on the server: builds with nix, copies the output to a
currentsymlink, restarts systemd - done
the nix build
the flake pins nixpkgs to nixos-unstable, overrides go to go_1_24, and builds the module with buildGoModule including sqlite headers. running nix build . on the server produces a result/ symlink with the compiled binary. no go installation needed.
{
description = "nix flake for [redacted] server";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [
(final: prev: {
go = prev.go_1_24;
})
];
};
in {
packages.${system}.default = pkgs.buildGoModule {
pname = "server";
version = "0.1.0";
src = self;
vendorHash = "sha256-nm2ByXQ+0b+re3JiNukBYpKK/+VvcTxMiqwukcu/uuM=";
buildInputs = [ pkgs.sqlite ];
ldflags = [ "-w" "-s" ];
};
};
}now any machine with nix can build the exact same binary
the github actions workflow
the workflow runs on a self-hosted runner. it triggers on push to master, copies the repo to a release directory on the server, then runs the deploy script
name: Deploy to Production (Nix btw)
on:
push:
branches: ["master"]
jobs:
deploy:
runs-on: self-hosted
environment:
name: production
url: https://[redacted].bynisarg.in
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Copy repo to releases
run: |
mkdir -p /var/www/[redacted]/releases/${GITHUB_SHA}
rsync -av \
--exclude='.git' \
--exclude='.github' \
--exclude='.gitignore' \
./ /var/www/[redacted]/releases/${GITHUB_SHA}
cp -r . /var/www/[redacted]/releases/${GITHUB_SHA}
- name: Run deploy.sh on server
run: |
bash /var/www/[redacted]/releases/${GITHUB_SHA}/deploy.sh ${GITHUB_SHA}the runner just pulls the code. the server does the build

the deploy script
this script handles the actual deployment and it runs inside the release directory on the server:
#!/usr/bin/env bash
set -e
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
SHA="$1"
RELEASES_DIR="/var/www/[redacted]/releases"
CURRENT_DIR="/var/www/[redacted]/current"
RELEASE_DIR="$RELEASES_DIR/$SHA"
echo "Deploying commit $SHA"
cd "$RELEASE_DIR"
echo "Activating release..."
rm -rf "$CURRENT_DIR"
mkdir -p $CURRENT_DIR/views
if [ -d "views" ]; then
cp -r views $CURRENT_DIR/views
echo "Copied views/"
fi
echo "Building with Nix..."
nix build .
echo "Copy Nix output into a current release directory"
cp -r result $CURRENT_DIR/result
echo "Restarting systemd service..."
sudo systemctl restart [redacted]
echo "Cleaning old releases (keeping last 5)..."
cd "$RELEASES_DIR"
ls -1 | sort | head -n -"5" | xargs -I {} rm -rf "{}" || true
echo "Deploy complete for commit $SHA!"the binary ends up at current/result/bin/server, which is what systemd points to
systemd service
the service file is simple:
[Unit]
Description=[redacted] Server
After=network.target
[Service]
User=igris
WorkingDirectory=/var/www/[redacted]/current
ExecStart=/var/www/[redacted]/current/result/bin/server
Environment="[redacted]_DB_PATH=/var/www/[redacted]/data/[redacted].db"
Environment="[redacted]_PORT=:3001"
Restart=always
[Install]
WantedBy=multi-user.targetenable it and it starts on every reboot. systemctl restart [redacted].service pulls in a new deploy

what i learned
a few things that caught me off guard along the way:
- nix needs multi-user mode on the server, otherwise the daemon profile path won’t exist
- the
vendorHashin the flake was confusing at first but after figuring out it’s just dependency verifier(?) it made sense - github runners need to be re-created for every repo on personal accounts, so using a github org runner was easier (what i did)
- the commit hash as build output dir idea was from gipitty. the cleanup logic is simple as well: sort releases, keep the last 5.
- the rollback story is nice too. each deploy lives in its own directory named by commit sha. if something breaks, i just update the
currentsymlink to point to an older release and restart the service
conclusion
i’m not 100% sure if this is a good idea to deploy an app using nix but it was a fun way to do things and move away from traditional devops practices like most people advise. and the server was happy as well as the build wasn’t eating up too much of it’s resources and didn’t keep crashing either (it did crash a few times tho). if you are reading this and know how to actually deploy with nix, i would love to hear from you! comments are always welcome!
comments