{ config, pkgs, lib, ... }: with lib; { options.services.dockerStacks = mkOption { type = types.attrsOf (types.submodule { options = { path = mkOption { type = types.str; }; envFile = mkOption { type = types.nullOr types.path; default = null; }; ports = mkOption { type = types.listOf types.int; default = [ ]; }; }; }); default = {}; }; config = { virtualisation.docker.enable = true; virtualisation.docker.daemon.settings.dns = [ "1.1.1.1" "8.8.8.8" ]; networking.firewall.allowedTCPPorts = flatten (mapAttrsToList (name: value: value.ports) config.services.dockerStacks); systemd.services = mapAttrs' (name: value: nameValuePair "${name}_stack" { description = "Docker Compose stack: ${name}"; # Added 'docker.socket' to both after and wants to ensure the API is reachable after = [ "network.target" "docker.service" "docker.socket" "agenix.service" ]; wants = [ "docker.socket" "agenix.service" ]; requires = [ "docker.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; WorkingDirectory = value.path; User = "root"; # This line forces the service to wait until the docker socket is actually responsive ExecStartPre = "${pkgs.bash}/bin/bash -c 'while [ ! -S /var/run/docker.sock ]; do sleep 1; done'"; ExecStart = "${pkgs.docker-compose}/bin/docker-compose up -d --remove-orphans"; ExecStop = "${pkgs.docker-compose}/bin/docker-compose down"; RemainAfterExit = true; # Ensure the environment file is passed correctly EnvironmentFile = mkIf (value.envFile != null) [ value.envFile ]; }; }) config.services.dockerStacks; }; }