3 changed files with 303 additions and 3 deletions
@ -0,0 +1,301 @@
@@ -0,0 +1,301 @@
|
||||
--- |
||||
title: Deploying with NixOS - Site Edition |
||||
date: 2022-04-06 |
||||
--- |
||||
|
||||
Today, I managed to make this site deployable via NixOS, as well as simplifying |
||||
the whole way I manage my NixOS servers. In this post, I want to walk you |
||||
through what I did and how I did it. |
||||
|
||||
## Original flake |
||||
|
||||
My [original][origflake] NixOS flakes were... a mess, to put it lightly. They were an |
||||
amalgamation of other people's flakes, and it led to things like my note-taking |
||||
laptop (which should have xournalpp on it and not much else) somehow running |
||||
libvirtd and Postgresql. Since I first wrote them, I've wanted to modularize my |
||||
home-manager configuration and allow it to be managed outside of the system |
||||
configuration. |
||||
|
||||
## Modularizing |
||||
|
||||
I'm pretty sure that's not a word, but it is now. The first step was to break my |
||||
`home-manager` configuration out of my NixOS system configuration. This would |
||||
let me switch around my home without needing to use `sudo`, but more |
||||
importantly, it would let me include modules properly based on the system |
||||
hostname. |
||||
|
||||
To do this, I ~~stole~~ wrote a function based on one by my friend Ellie, `hmConfig`. |
||||
It's similar to `mkSystem` in that it generates a proper configuration, except |
||||
that this one is for the specific user. You can see my function |
||||
[here][hmConfig], but it's nothing too complex. It sets my home directory, |
||||
username, and home state version. It then imports a baseline `./home/home.nix`, |
||||
which sets up some other things like my shell. |
||||
|
||||
The big thing is one I copied from `mkSystem`, the `++ extraImports`. This is |
||||
used further down in the flake in `homeConfigurations`, where I define the |
||||
specific modules that should be included in this home-manager configuration. For |
||||
example, my laptop (cesium), needs things like my mail setup, TeX, my X11 |
||||
configuration, and mpd. The servers I run, however, don't need anything special. |
||||
This lets each system be narrowed down to exactly what it needs. |
||||
|
||||
## Deploy-RS |
||||
|
||||
Two days ago, I converted two servers (`kronos` and `magnesium`) to NixOS. I |
||||
quickly realized how much of a pain updating these was going to be, as they got |
||||
out of sync with my flake. I could've set up a crontab to automatically apply |
||||
the latest flake every so often, but that would be too simple (and result in too |
||||
much waiting). |
||||
|
||||
Instead, I found a project called [serokell/deploy-rs][deployrs]. This let me |
||||
make a two-line script in my flake repository: |
||||
|
||||
``` |
||||
#!/usr/bin/env sh |
||||
|
||||
set -e |
||||
nix run github:serokell/deploy-rs |
||||
``` |
||||
|
||||
Every time I make a change to my flake, I can run this script and have it |
||||
automatically conform every managed system. It sets up my services, and all the |
||||
stuff I expect from one of my servers. When more servers come into the NixOS |
||||
fold, I have to do a few things. |
||||
|
||||
First, I need to apply a basic flake. I tend to use the `kronos` host, since |
||||
that's got nothing special attached to it. Quickly rewriting the installed |
||||
`configuration.nix` to enable flakes, I can run something like: |
||||
``` |
||||
nixos-rebuild switch --flake "git+https://git.carathe.dev/muirrum/nix#kronos" |
||||
``` |
||||
|
||||
This will install the kronos flake (which has the unfortunate side effect of |
||||
making some interesting network decisions and setting the hostname to `kronos`). |
||||
In the future, I'll probably write a generic `server` configuration that doesn't |
||||
do anything special but enable SSH and create my user. |
||||
|
||||
After that, I need to add the server to my deployment configuration. Near the |
||||
bottom of my `flake.nix` is a [`deploy`][mydeploy] output, that currently has two nodes |
||||
(the two servers I'm managing this way). It sets up the user that should be used |
||||
to SSH in as, and the nodes. I can conform this to a different configuration at |
||||
this time. |
||||
|
||||
Now that this is done, I can move on to automatically configuring my services. |
||||
|
||||
## Service flakes |
||||
|
||||
I decided to start with my site, because it's fairly simple as a service. All it |
||||
needs to do is parse some markdown and serve it. Should be pretty simple, right? |
||||
|
||||
Haha. This took two to three days of working out bugs in my site flake. The |
||||
first thing I had to do was configure some options for it and create a system |
||||
module. In my `flake.nix`, I wrote the following: |
||||
|
||||
``` |
||||
nixosModules.site = { config, lib, ... }: { |
||||
options = { |
||||
cara.services.carasite.enable = lib.mkEnableOption "enable cara's site"; |
||||
cara.services.carasite.domain = lib.mkOption { |
||||
type = lib.types.str; |
||||
default = "devcara.com"; |
||||
}; |
||||
cara.services.carasite.port = lib.mkOption { |
||||
type = lib.types.port; |
||||
default = 3000; |
||||
}; |
||||
}; |
||||
}; |
||||
``` |
||||
|
||||
This defines a few options that I can use in my generated configuration. Whether |
||||
or not the site should be enabled (that's the `mkEnableOption` call), and the |
||||
domain and port to use for Nginx. |
||||
|
||||
I prefixed my options with "cara" just to avoid collisions with proper `nixpkgs` |
||||
modules (not that `carasite` is ever going to end up in `nixpkgs`, but you never |
||||
know). |
||||
|
||||
Next up was writing the "implementation", which takes the options and generates |
||||
the way that the system should look to make the service work. My basic one looks |
||||
like this: |
||||
|
||||
``` |
||||
nixosModules.site = { config, lib, ... }: { |
||||
... |
||||
|
||||
config = lib.mkIf config.cara.services.carasite.enable { |
||||
users.groups.cara-site = { |
||||
... |
||||
}; |
||||
users.users.cara-site = { |
||||
... |
||||
}; |
||||
|
||||
systemd.services.cara-site = { |
||||
... |
||||
}; |
||||
|
||||
networking.firewall.allowedTCPPorts = [ ... ]; |
||||
|
||||
services.nginx = { |
||||
... |
||||
}; |
||||
}; |
||||
}; |
||||
``` |
||||
|
||||
I'll go into more details on what each of those sections do in a bit. Basically, |
||||
this takes the options I'm going to set in my per-host configuration and turn it |
||||
into a workable service deployment. |
||||
|
||||
### Implementation Details |
||||
|
||||
#### Users & Groups |
||||
Each of these attribute sets configures a different aspect of what makes this |
||||
site run. First up are the user and group management bits, they create a service |
||||
user and group that the site will run as. |
||||
|
||||
``` |
||||
users.groups.cara-site = { |
||||
members = [ "cara-site" ]; |
||||
} |
||||
|
||||
users.users.cara-site = { |
||||
createHome = true; |
||||
isSystemUser = true; |
||||
home = "/var/lib/cara-site"; |
||||
group = "cara-site"; |
||||
}; |
||||
``` |
||||
Pretty standard stuff, and described more thoroughly in the [NixOS manual][manual-users]. |
||||
|
||||
#### Systemd |
||||
|
||||
This is definitely the place where I had the most trouble. I was running into a |
||||
few errors related to the way I had set up my `serviceConfig`, but that was |
||||
fixed by properly setting the `WorkingDirectory`. |
||||
|
||||
``` |
||||
systemd.services.cara-site = { |
||||
wantedBy = [ "multi-user.target" ]; |
||||
|
||||
environment = { |
||||
PORT = "${toString (config.cara.services.carasite.port)}"; |
||||
}; |
||||
|
||||
serviceConfig = { |
||||
User = "cara-site"; |
||||
Group = "cara-site"; |
||||
Restart = "always"; |
||||
WorkingDirectory = "${defaultPackage}"; |
||||
ExecStart = "${defaultPackage}/bin/carasite"; |
||||
}; |
||||
}; |
||||
``` |
||||
|
||||
In the end, it's a pretty simple systemd service. It runs in the store directory |
||||
where the built site is kept, and runs as the user we configured just a moment |
||||
ago. |
||||
|
||||
Here we see one of the frustrations of the Nix options system. For some reason, |
||||
the `lib.types.port` type can't be automatically made into a string, as an |
||||
integer. Even though the whole purpose of ports is to be made into strings and |
||||
then put into configuration files somehow, you still need the whole `${toString(...)}` |
||||
crap. |
||||
|
||||
#### Nginx |
||||
Nginx was fairly simple. All it took was configuring the ACME settings somewhere |
||||
else in my system flake and opening the right firewall ports. |
||||
|
||||
``` |
||||
networking.firewall.allowedTCPPorts = [ 443 80 ]; |
||||
|
||||
services.nginx = { |
||||
enable = true; |
||||
recommendedProxySettings = true; |
||||
recommendedTlsSettings = true; |
||||
|
||||
virtualHosts."${config.cara.services.carasite.domain}" = { |
||||
forceSSL = true; |
||||
enableACME = true; |
||||
|
||||
locations."/" = { |
||||
proxyPass = "http://127.0.0.1:${toString (config.cara.services.carasite.port)}"; |
||||
}; |
||||
}; |
||||
}; |
||||
``` |
||||
|
||||
In the future, I should probably move the `127.0.0.1` to something that lets me |
||||
configure the bind host, but it's working for now. |
||||
|
||||
## Deploying |
||||
|
||||
Now that the flake is all done, it comes time to deploy it to a server. `kronos` |
||||
is my static site host, so that's where I'll be putting this. |
||||
|
||||
First, I added it to my system flake's `inputs`, like this: |
||||
``` |
||||
inputs = { |
||||
... |
||||
|
||||
carasite = { |
||||
url = "git+https://git.carathe.dev/muirrum/site"; |
||||
inputs.nixpkgs.follows = "nixpkgs"; |
||||
}; |
||||
}; |
||||
``` |
||||
|
||||
This tells my flake to include `carasite` as a dependency and make its version |
||||
of `nixpkgs` follow the system flake's version. |
||||
|
||||
Next, I added it to my `mkSystem` function: |
||||
``` |
||||
mkSystem = conf: |
||||
nixpkgs.lib.nixosSystem rec { |
||||
modules = [ |
||||
... |
||||
|
||||
carasite.nixosModules.${system}.site |
||||
]; |
||||
... |
||||
}; |
||||
``` |
||||
|
||||
Note that this won't enable it for all systems, it still needs to be enabled |
||||
with the `cara.services.carasite.enable` option we defined in the site flake. |
||||
|
||||
Then, I went into my `hosts/kronos/default.nix` file, and added the |
||||
configuration snippet: |
||||
``` |
||||
cara.services.carasite = { |
||||
enable = true; |
||||
|
||||
domain = "devcara.com"; |
||||
port = 3030; |
||||
}; |
||||
``` |
||||
|
||||
This tells the flake to enable the site only on the `kronos` server, and also |
||||
explicitly sets the defaults just in case. |
||||
|
||||
Now, the only thing left to do is run the deploy script and watch the magic |
||||
work! |
||||
|
||||
## Conclusion |
||||
|
||||
*Wow*. This method of deploying software is so much easier and more predictable |
||||
than any other I've tried. I used to have to manually SSH in and update the site |
||||
whenever something changed, and now I can do it from my laptop. |
||||
|
||||
The added convienence of being able to apply the same basic settings to all my |
||||
servers without needing to use something like ansible is also helpful. |
||||
|
||||
I think a few of my Discord bots are going to be moved over next. Those should |
||||
present some added challenges in that they all require PostgreSQL to function |
||||
properly. |
||||
|
||||
[origflake]: https://git.carathe.dev/muirrum/nix/src/tag/pre-modules-home |
||||
[hmConfig]: https://git.carathe.dev/muirrum/nix/src/branch/master/flake.nix#L27 |
||||
[deployrs]: https://github.com/serokell/deploy-rs |
||||
[mydeploy]: https://git.carathe.dev/muirrum/nix/src/branch/master/flake.nix#L95 |
||||
[manual-users]: https://nixos.org/manual/nixos/unstable/index.html#sec-user-management |
Loading…
Reference in new issue