Deploying an Astro project on NixOS
Nix is an incredible project and has completely change the way I think about configuring linux and macOS environments. Recently, I moved my personal server from Ubuntu to NixOS to match my desktop environment. (dotfiles here!). In doing so, I realized I needed to move this blog over, too. I could simply deploy a docker container like I did before, but I think it would be interesting and informative to try and build a NixOS module around it. Hopefully you find it useful :)
The flake.nix
file.
Nix has a experimental feature called flakes, if you’ve been in the NixOS space long enough you’ve undoubtably heard of them. Flakes are a new way of writing your package configuration, you expose two objects, inputs
and outputs
. Your inputs
would be things your application depends on, for example, nixpkgs
(the package repository of Nix). Inputs can be a variety of different things, but typically are git repos. A flake.lock
file is generated automatically so any consumers of your flake get the exact revisions of each input to guarantee reproducability. The outputs
section of your flake can contain many things, from devShells
to entire nixosConfigurations
, but we are interested in packages
and nixosModules
today.
A example for a starter flake for a Astro project may look like this:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
};
outputs = {
systems,
nixpkgs,
...
} @ inputs: let
eachSystem = f:
nixpkgs.lib.genAttrs (import systems) (
system:
f nixpkgs.legacyPackages.${system}
);
in {
devShells = eachSystem (pkgs: {
default = pkgs.mkShell {
buildInputs = [
pkgs.nodejs
pkgs.nodePackages.pnpm
pkgs.nodePackages.typescript
pkgs.nodePackages.typescript-language-server
pkgs.nodePackages."@tailwindcss/language-server"
pkgs.nodePackages."@astrojs/language-server"
];
};
});
};
}
You can see for my inputs I am taking in nixpkgs
and nix-systems
, which I am using to generate a devShell for every system architechture supported by NixOS. The devShells
all import the following nix packages, nodejs
, pnpm
, typescript
, typescript-language-server
, tailwindcss-language-server
, astrojs-lanaguage-server
. Lets add add a package!
I use pnpm as my package manager of choice, and as such we have to add the pnpm2nix
flake to our inputs, which we can do with the following line.
pnpm2nix.url = "github:nzbr/pnpm2nix-nzbr";
Now, lets add the package spec:
{
outputs = {
systems,
nixpkgs,
self,
...
} @ inputs: let
eachSystem = f:
nixpkgs.lib.genAttrs (import systems) (
system:
f nixpkgs.legacyPackages.${system}
);
in {
# add packages :)
packages = eachSystem (pkgs: {
default = inputs.pnpm2nix.packages.${pkgs.system}.mkPnpmPackage {
name = "zm-blog";
src = ./.;
packageJSON = ./package.json;
pnpmLock = ./pnpm-lock.yaml;
};
});
# ...
};
}
Now, when we run nix build
, everything works as expected, great! But how can we see the outputs of our build? If we run the following command:
| nix eval --raw .#packages.x86_64-linux.default
/nix/store/ik5nmb60qrgib5knp4b538axwdxykc8z-zm-blog
Awesome, if we ls this path, we get exactly what we are expecting from the build output.
| ls /nix/store/ik5nmb60qrgib5knp4b538axwdxykc8z-zm-blog nix-shell-env
_astro fonts blog-placeholder-2.jpg blog-placeholder-5.jpg rss.xml
about index.html blog-placeholder-3.jpg blog-placeholder-about.jpg sitemap-0.xml
blog blog-placeholder-1.jpg blog-placeholder-4.jpg favicon.svg sitemap-index.xml
At this point, we could add this repo as a input to the flake configuring our server, add a new virtualHost
for the domain we want this to run on, point the root at this package and call it a day, but I want to take it one step further. I want to write a NixOS module to make the configuration server side even easier.
I added the following code to the outputs section of my flake.nix
.
{
# previous outputs
nixosModule = {
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.zmio.blog;
in {
options.zmio.blog = {
enable = mkEnableOption "Enables the Blog Site";
domain = mkOption rec {
type = type.str;
default = "zackmyers.io";
example = default;
description = "The domain name for the website";
};
ssl = mkOption rec {
type = type.bool;
default = true;
example = default;
description = "Whether to enable SSL on the domain or not";
};
};
config = mkIf cfg.enable {
services.nginx.virtualHosts.${cfg.domain} = {
forceSSL = cfg.ssl;
enableACME = cfg.ssl;
root = "${packages.${pkgs.system}.default}";
};
};
};
}
Woah, that’s a lot of code, let’s break it down.
Because Nix (the language) is mostly used for configuration, defining variables anywhere could be confusing, so you have to do it in a special scope, that being the let .. in
syntax. In this example, we are setting the variable cfg
to be equal to config.zmio.blog
, for convenience. Notice also the with lib;
, this allows us to call the values on lib
as a top-level var, ie, lib.mkOption
would become mkOption
.
The options.zmio.blog
object contains the options, their types and their defaults, and the config
section is the code that gets executed.
Enough code, lets deploy!
Deploying on the server
After adding the repo of my blog project to my server’s flake like this:
{
inputs = {
# all our previous definitions
blog.url = "github:zackartz/zmio";
};
# ...
nixosConfigurations.pluto = nixpkgs_stable.lib.nixosSystem {
specialArgs = {inherit inputs;};
modules = [
# previous modules
inputs.blog.nixosModule
];
};
# other configs
}
We can add the following to our server’s main nixosModule:
zmio.blog.enable = true;
And that should be it, after a rebuild it should be live!
Conclusion
NixOS allows for a truly unique way of deploying apps, and if you thought this was interesting, be sure to check out Nix! There’s tons of other cool stuff to check out!