Skip to main content

NixOS Docker Compose Autostarter

These nix config files are meant for enabling Docker on NixOS, configuring it, and then launching all docker compose files recursively in a specified directory. The script that launches the compose files also (likely inadvisably) opens ports in the firewall based on the ports specified in the compose files.

nixos-rebuild should tell you if a docker compose service fails to start when run, but you can also check the status of the service: `$ sudo systemctl status runDockerComposes.service`

Use the nix config files by importing them by adding the following to your "imports" section in your "configuration.nix":

imports =
    [
      ./hardware-configuration.nix
        ...
      ./services/utilities/docker.enable.nix # Change to the directory you put the docker nix config files.
        ...
  ];

docker.enable.nix

{ ... }:
{

  imports =
    [
      ./docker.config.nix
  ];

  virtualisation.docker.enable = true;

  # Replace "[USERNAME]" to add your user to the "docker" group to manage Docker without sudo
  users.users.[USERNAME].extraGroups = [ "docker" ];

  virtualisation.docker.storageDriver = "overlay2";

#  virtualisation.docker.registryMirrors = [ "https://example-mirror.com/" ];

  # Set to the directory you would like docker to store its files
  virtualisation.docker.daemon.settings = {
    data-root = "/zfszero/docker/var";
  };
}

Notes:

  • Configurable lines: 
    • `./docker.config.nix` 
    • `users.users.[USERNAME].extraGroups = [ "docker" ];`
    • `virtualisation.docker.registryMirrors = [ "https://example-mirror.com/" ];`
    • `data-root = "/zfszero/docker/var";`

docker.config.nix

{ pkgs, ... }:

let
  Compose_Directory = "/zfszero/docker/compose"; # Set to docker compose file root directory
  yq_Directory = "${Compose_Directory}/../.misc"; # Set to directory for yq binary file storage, used for yml/port parsing.
  bashPath = pkgs.bash;
  wgetPath = pkgs.wget;
  dockerPath = pkgs.docker;
  iptablesPath = pkgs.iptables;
  runDockerComposeScript = pkgs.writeScript "run-docker-composes.sh" ''
    #!${bashPath}/bin/bash
    error_file=$(mktemp)
    COMPOSE_DIR="${Compose_Directory}"
    COMPOSE_FILES=$(find "$COMPOSE_DIR" -type f -name "*docker-compose.yml")
    YQ_DIR="${yq_Directory}"
    mkdir -p "$YQ_DIR"
    if [ ! -f "$YQ_DIR/yq" ]; then
      ${wgetPath}/bin/wget https://github.com/mikefarah/yq/releases/download/v4.42.1/yq_linux_amd64 -O $YQ_DIR/yq && chmod +x $YQ_DIR/yq
    fi
    for DOCKERCOMPOSEFILE in $COMPOSE_FILES; do
      echo "Checking ports for $DOCKERCOMPOSEFILE"
      PORTS=$($YQ_DIR/yq e '.services[].ports[]?' "$DOCKERCOMPOSEFILE" | sed 's/"//g' | sed -r 's#([0-9]+):[0-9]+(/udp|/tcp)?#\1\2#' | sed s/-/:/ | sed -r 's#([0-9]+)-[0-9]+(/udp|/tcp)?#\1\2#')
      if [ ! -z "$PORTS" ]; then
        for PORT in $PORTS; do
          if [[ "$PORT" == *"/udp" ]]; then
            PSANSU=$(echo "$PORT" | sed 's#/udp##')
            echo "Opening UDP port $PSANSU for $DOCKERCOMPOSEFILE"
            ${iptablesPath}/bin/iptables -A INPUT -p udp --dport $PSANSU -j ACCEPT || echo "Failed to open UDP port $PSANSU for $DOCKERCOMPOSEFILE" >> "$error_file"
          else
            PSANST=$(echo "$PORT" | sed 's#/tcp##')
            echo "Opening TCP port $PSANST for $DOCKERCOMPOSEFILE"
            ${iptablesPath}/bin/iptables -A INPUT -p tcp --dport $PSANST -j ACCEPT || echo "Failed to open TCP port $PSANST for $DOCKERCOMPOSEFILE" >> "$error_file"
          fi
        done
      fi
      if ${dockerPath}/bin/docker compose -f "$DOCKERCOMPOSEFILE" up -d; then
        echo "Successfully started $DOCKERCOMPOSEFILE"
      else
        echo "Error starting $DOCKERCOMPOSEFILE"
        error_=1
        echo "Failed to start $DOCKERCOMPOSEFILE" >> "$error_file"
      fi
    done

    if [ -s "$error_file" ]; then
        echo "Errors encountered:"
        cat "$error_file"
        rm -f "$error_file"
        exit 1
    fi
  '';
in
{
  systemd.services.runDockerComposes = {

    description = "Launch docker compose files";
    after = [ "network.target" "docker.service" ]; # If docker compose files are on zfs storage, add "zfs.target" here
    requires = [ "docker.service" ]; # and here
    wants = [ "docker.service" ];
    wantedBy = [ "multi-user.target" ];
    script = "${runDockerComposeScript}";
    serviceConfig = {
      Type = "oneshot";
      RemainAfterExit = true;
      User = "root";
      # ExecStartPre = "-/run/current-system/sw/bin/sleep 10"; # Uncomment/adjust if you need to give time for other services to finish starting before this service 
      TimeoutStartSec = "8min"; # How long you want to give the service to start all docker compose files
    };
  };
}

Notes:

  • Configurable variables:
    • Compose_Directory
    • yq_Directory
    • ExecStartPre
    • TimeoutStartSec
  • yq binary downloaded and used rather than NixOS native package due to old version, differing syntax.
  • Intended compose file naming scheme:
    • [SERVICE NAME].docker-compose.yml (should work with just "docker-compose.yml")

To-do:

  • checksum verification for yq binary?
  • Add support for docker compose build