diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 0f3c9d0c5627..dc571602581b 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -880,6 +880,7 @@
./virtualisation/container-config.nix
./virtualisation/containers.nix
./virtualisation/docker.nix
+ ./virtualisation/docker-containers.nix
./virtualisation/ecs-agent.nix
./virtualisation/libvirtd.nix
./virtualisation/lxc.nix
diff --git a/nixos/modules/virtualisation/docker-containers.nix b/nixos/modules/virtualisation/docker-containers.nix
new file mode 100644
index 000000000000..7cf871cc3bac
--- /dev/null
+++ b/nixos/modules/virtualisation/docker-containers.nix
@@ -0,0 +1,233 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+ cfg = config.docker-containers;
+
+ dockerContainer =
+ { name, config, ... }: {
+
+ options = {
+
+ image = mkOption {
+ type = types.str;
+ description = "Docker image to run.";
+ example = "library/hello-world";
+ };
+
+ cmd = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = "Commandline arguments to pass to the image's entrypoint.";
+ example = literalExample ''
+ ["--port=9000"]
+ '';
+ };
+
+ entrypoint = mkOption {
+ type = with types; nullOr str;
+ description = "Overwrite the default entrypoint of the image.";
+ default = null;
+ example = "/bin/my-app";
+ };
+
+ environment = mkOption {
+ type = with types; attrsOf str;
+ default = {};
+ description = "Environment variables to set for this container.";
+ example = literalExample ''
+ {
+ DATABASE_HOST = "db.example.com";
+ DATABASE_PORT = "3306";
+ }
+ '';
+ };
+
+ log-driver = mkOption {
+ type = types.str;
+ default = "none";
+ description = ''
+ Logging driver for the container. The default of
+ "none" means that the container's logs will be
+ handled as part of the systemd unit. Setting this to
+ "journald" will result in duplicate logging, but
+ the container's logs will be visible to the docker
+ logs command.
+
+ For more details and a full list of logging drivers, refer to the
+
+ Docker engine documentation
+ '';
+ };
+
+ ports = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = ''
+ Network ports to publish from the container to the outer host.
+
+
+ Valid formats:
+
+
+
+
+ <ip>:<hostPort>:<containerPort>
+
+
+
+
+ <ip>::<containerPort>
+
+
+
+
+ <hostPort>:<containerPort>
+
+
+
+
+ <containerPort>
+
+
+
+
+ Both hostPort and
+ containerPort can be specified as a range of
+ ports. When specifying ranges for both, the number of container
+ ports in the range must match the number of host ports in the
+ range. Example: 1234-1236:1234-1236/tcp
+
+
+ When specifying a range for hostPort only, the
+ containerPort must not be a
+ range. In this case, the container port is published somewhere
+ within the specified hostPort range. Example:
+ 1234-1236:1234/tcp
+
+
+ Refer to the
+
+ Docker engine documentation for full details.
+ '';
+ example = literalExample ''
+ [
+ "8080:9000"
+ ]
+ '';
+ };
+
+ user = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = ''
+ Override the username or UID (and optionally groupname or GID) used
+ in the container.
+ '';
+ example = "nobody:nogroup";
+ };
+
+ volumes = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = ''
+ List of volumes to attach to this container.
+
+ Note that this is a list of "src:dst" strings to
+ allow for src to refer to
+ /nix/store paths, which would difficult with an
+ attribute set. There are also a variety of mount options available
+ as a third field; please refer to the
+
+ docker engine documentation for details.
+ '';
+ example = literalExample ''
+ [
+ "volume_name:/path/inside/container"
+ "/path/on/host:/path/inside/container"
+ ]
+ '';
+ };
+
+ workdir = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "Override the default working directory for the container.";
+ example = "/var/lib/hello_world";
+ };
+
+ extraDockerOptions = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = "Extra options for docker run.";
+ example = literalExample ''
+ ["--network=host"]
+ '';
+ };
+ };
+ };
+
+ mkService = name: container: {
+ wantedBy = [ "multi-user.target" ];
+ after = [ "docker.service" "docker.socket" ];
+ requires = [ "docker.service" "docker.socket" ];
+ serviceConfig = {
+ ExecStart = concatStringsSep " \\\n " ([
+ "${pkgs.docker}/bin/docker run"
+ "--rm"
+ "--name=%n"
+ "--log-driver=${container.log-driver}"
+ ] ++ optional (! isNull container.entrypoint)
+ "--entrypoint=${escapeShellArg container.entrypoint}"
+ ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment)
+ ++ map (p: "-p ${escapeShellArg p}") container.ports
+ ++ optional (! isNull container.user) "-u ${escapeShellArg container.user}"
+ ++ map (v: "-v ${escapeShellArg v}") container.volumes
+ ++ optional (! isNull container.workdir) "-w ${escapeShellArg container.workdir}"
+ ++ map escapeShellArg container.extraDockerOptions
+ ++ [container.image]
+ ++ map escapeShellArg container.cmd
+ );
+ ExecStartPre = "-${pkgs.docker}/bin/docker rm -f %n";
+ ExecStop = "${pkgs.docker}/bin/docker stop %n";
+ ExecStopPost = "-${pkgs.docker}/bin/docker rm -f %n";
+
+ ### There is no generalized way of supporting `reload` for docker
+ ### containers. Some containers may respond well to SIGHUP sent to their
+ ### init process, but it is not guaranteed; some apps have other reload
+ ### mechanisms, some don't have a reload signal at all, and some docker
+ ### images just have broken signal handling. The best compromise in this
+ ### case is probably to leave ExecReload undefined, so `systemctl reload`
+ ### will at least result in an error instead of potentially undefined
+ ### behaviour.
+ ###
+ ### Advanced users can still override this part of the unit to implement
+ ### a custom reload handler, since the result of all this is a normal
+ ### systemd service from the perspective of the NixOS module system.
+ ###
+ # ExecReload = ...;
+ ###
+
+ TimeoutStartSec = 0;
+ TimeoutStopSec = 120;
+ Restart = "always";
+ };
+ };
+
+in {
+
+ options.docker-containers = mkOption {
+ default = {};
+ type = types.attrsOf (types.submodule dockerContainer);
+ description = "Docker containers to run as systemd services.";
+ };
+
+ config = mkIf (cfg != []) {
+
+ systemd.services = mapAttrs' (n: v: nameValuePair "docker-${n}" (mkService n v)) cfg;
+
+ virtualisation.docker.enable = true;
+
+ };
+
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 69510c1420fa..a5acf78a8839 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -59,6 +59,7 @@ in
dhparams = handleTest ./dhparams.nix {};
dnscrypt-proxy = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy.nix {};
docker = handleTestOn ["x86_64-linux"] ./docker.nix {};
+ docker-containers = handleTestOn ["x86_64-linux"] ./docker-containers.nix {};
docker-edge = handleTestOn ["x86_64-linux"] ./docker-edge.nix {};
docker-preloader = handleTestOn ["x86_64-linux"] ./docker-preloader.nix {};
docker-registry = handleTest ./docker-registry.nix {};
diff --git a/nixos/tests/docker-containers.nix b/nixos/tests/docker-containers.nix
new file mode 100644
index 000000000000..972552735202
--- /dev/null
+++ b/nixos/tests/docker-containers.nix
@@ -0,0 +1,29 @@
+# Test Docker containers as systemd units
+
+import ./make-test.nix ({ pkgs, lib, ... }: {
+ name = "docker-containers";
+ meta = {
+ maintainers = with lib.maintainers; [ benley ];
+ };
+
+ nodes = {
+ docker = { pkgs, ... }:
+ {
+ virtualisation.docker.enable = true;
+
+ virtualisation.dockerPreloader.images = [ pkgs.dockerTools.examples.nginx ];
+
+ docker-containers.nginx = {
+ image = "nginx-container";
+ ports = ["8181:80"];
+ };
+ };
+ };
+
+ testScript = ''
+ startAll;
+ $docker->waitForUnit("docker-nginx.service");
+ $docker->waitForOpenPort(8181);
+ $docker->waitUntilSucceeds("curl http://localhost:8181|grep Hello");
+ '';
+})