Containerize the NixOS way

How do you do, Cola Gangsters? It’s been a while! We’ll get back to the Nixification of our daily driver shortly because a lot of things have changed, and we need to update things for this modern age. Today, however, I want to talk about running containers in a NixOS way that is sure to thrill!

Let’s say you want to add n8n to your homelab. One way to go about that is to have Docker or Podman installed, grabbing a docker-compose.yml, changing what needs changing, and running sudo podman-compose up or the Docker equivalent. We don't use docker here.

What happens when you reboot your system, though? Do we need to go around and bring our containers up one by one? Or do we need to write a systemd service for every one of our containers and make sure we get all the dependencies right? Actually, we don't need either of those things. All we need is love compose2nix.

compose2nix will take the compose YAML file and generate a proper Nix module for you. From there, you just call the nix module from you configuration.nix or flake, sudo nixos-rebuild switch, and... that's about it. Let's take a look at how that would go with our n8n example.

First, you go here and grab the docker-compose.yml file. It looks like this:

version: '3.8'

volumes:
  db_storage:
  n8n_storage:

services:
  postgres:
    image: postgres:16
    restart: always
    environment:
      - POSTGRES_USER
      - POSTGRES_PASSWORD
      - POSTGRES_DB
      - POSTGRES_NON_ROOT_USER
      - POSTGRES_NON_ROOT_PASSWORD
    volumes:
      - db_storage:/var/lib/postgresql/data
      - ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: always
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
      - DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
    ports:
      - 5678:5678
    links:
      - postgres
    volumes:
      - n8n_storage:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

After you edit the file to get the env variables right, you just need to pass that YAML to compose2nix and watch the magic happens. You don't even need to install compose2nix: a nix shell or nix run will do the trick:

On the directory containing your docker-compose.yml, you can nix run github:aksiksi/compose2nix -project=n8n where n8n is just the name you want to give this project. Could be anything you want, and it will become part of the systemd service name. The default output is a docker-compose.nix file that you can call from your configuration.nix or from a flake. My configuration.nix looks like this, for example:

{ config, pkgs, lib, ... }:

{
  imports =
    [ # Include the results of the hardware scan.
        ./hardware-configuration.nix
        ./podman.nix
        ./nvim.nix
        ./shell.nix
        ./postgres.nix
        ./gitea.nix
        ./traefik.nix
        ./redis.nix
        ./n8n.nix
        ./node_exporter.nix
        ./vpn.nix
        ./lldap.nix
    ];

  # Bootloader.
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.loader.efi.efiSysMountPoint = "/boot/efi";

In this config, I just copied the docker-compose.nix output to /etc/nixos/, renamed it n8n.nix, and added it to the imports section of /etc/nixos/configuration.nix. You can also use the -inputs argument to specify the compose file name if you have something not called docker-compose.yml, and/or the -output argument to specify the path for the output file:

nix run github:aksiksi/compose2nix -- -project=test -output=name_I_like.nix Will generate a name_I_like.nix file in the current directory.

From here, my fellow Cola Gangster, it's just a matter of sudo nixos-rebuild switch, and enjoying the show.

Doing a sudo systemctl list-units *n8n* should yield (\*n8n\* if you're using zsh):

  UNIT                               LOAD   ACTIVE SUB     DESCRIPTION
  podman-n8n-n8n.service             loaded active running podman-n8n-n8n.service
  podman-network-n8n_default.service loaded active exited  podman-network-n8n_default.service
  podman-compose-n8n-root.target     loaded active active  Root target generated by compose2nix.

Legend: LOAD   → Reflects whether the unit definition was properly loaded.
        ACTIVE → The high-level unit activation state, i.e. generalization of SUB.
        SUB    → The low-level unit activation state, values depend on unit type.

3 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.

Now you can do all the nifty systemctl stuff: stop, restart, status, etc. Check out the project's Github page for info on how to update and auto-update your containers. See you on the next one, gangsters!