| name | oci-stacks |
| description | Reference for creating and configuring OCI container stacks using the mix.nix oci-stacks NixOS module. Use when writing virtualisation.oci-stacks definitions, Docker container orchestration in NixOS, or working with container networking/systemd integration. |
OCI Container Stacks — Reference
The oci-stacks module abstracts Docker container orchestration into simple stack
definitions. It auto-generates Docker networks, systemd services with restart
policies, dependency wiring, and a root systemd target per stack.
Source: modules/nixos/oci-stacks.nix
Import: imports = [ inputs.mix-nix.nixosModules.oci-stacks ];
What It Generates (per stack)
| Generated | Purpose |
|---|
virtualisation.oci-containers.containers.* | Passes containers through to NixOS module |
systemd.services.docker-network-<name> | Creates/destroys the Docker network |
systemd.services.docker-<container> | Container services with restart policies |
systemd.targets.docker-compose-<name>-root | Root target for stack orchestration |
Each container service automatically gets:
partOf / wantedBy the root target
after / requires the network service (when network is enabled)
ExecStartPre script that verifies the network exists before start
- Default restart policies from
lib.infra.containers.serviceDefaults
Stack Definition Schema
virtualisation.oci-stacks.<stackName> = {
containers = {
"<container-name>" = {
image = "registry/image:tag";
# All standard oci-containers options:
# ports, volumes, environment, cmd, entrypoint,
# dependsOn, labels, user, log-driver, extraOptions, etc.
};
};
network = <string | attrset>; # default: stack name
description = "Human-readable stack description";
};
Network Configuration Patterns
Default (stack name as network)
Omit network — defaults to stack name with bridge driver.
virtualisation.oci-stacks.myapp = {
containers.myapp = {
image = "myapp:latest";
extraOptions = [ "--network=myapp" "--network-alias=myapp" ];
};
};
String shorthand
network = "my-custom-network";
Full submodule
network = {
name = "mynet"; # default: stack name
driver = "bridge"; # default: "bridge"
subnet = "10.1.1.0/24"; # optional
gateway = "10.1.1.1"; # optional
script = null; # optional: fully custom creation script
external = [ ]; # optional: external network soft dependencies
};
Custom network creation script
For advanced options (bridge name, ip-range) not covered by the submodule:
network = {
name = name;
script = ''
docker network inspect ${name} || docker network create ${name} \
--driver=bridge \
--opt com.docker.network.bridge.name=br-${name} \
--subnet=10.1.1.0/24 \
--ip-range=10.1.1.0/24 \
--gateway=10.1.1.1
'';
};
External network dependencies
When containers join a network owned by another stack:
network = {
name = name;
external = [ "pangolin" ]; # Adds wants (not requires) on docker-network-pangolin.service
};
# Then in container: extraOptions = [ "--network=${name}" "--network=pangolin" ];
Disabled network (host networking)
network.enable = false;
# Container uses: extraOptions = [ "--network=host" ];
Common Container Patterns
Network alias (inter-container DNS)
extraOptions = [ "--network=${name}" "--network-alias=database" ];
Health checks
extraOptions = [
"--network=${name}"
''--health-cmd=["mongosh", "--eval", "db.runCommand('ping').ok", "localhost:27017/test", "--quiet"]''
"--health-interval=10s"
"--health-timeout=10s"
"--health-retries=5"
"--health-start-period=10s"
];
Localhost-bound ports (native service integration)
Expose container ports only to the host so NixOS native services can reach them:
extraOptions = [ "--network=${name}" "-p" "127.0.0.1:27017:27017" ];
Static IP / MAC address
extraOptions = [ "--network=${name}" "--ip=10.1.1.10" "--mac-address=02:42:68:28:01:10" ];
Multiple networks
extraOptions = [ "--network=${name}" "--network-alias=${name}" "--network=pangolin" ];
Shared network namespace (container:X)
extraOptions = [ "--network=container:gerbil" ];
GPU passthrough (AMD ROCm)
extraOptions = [
"--device=/dev/dri:/dev/dri:rwm"
"--device=/dev/kfd:/dev/kfd:rwm"
"--group-add=video"
"--shm-size=17g"
"--memory=34g"
"--network=${name}"
];
Privileged / host-network container
virtualisation.oci-stacks.myagent = {
network.enable = false;
containers.myagent = {
image = "myagent:latest";
user = "root:root";
log-driver = "journald";
extraOptions = [ "--privileged" "--cap-add=NET_ADMIN" "--network=host" ];
};
description = "Agent running in host network mode";
};
Init / one-shot container
"${name}-init" = {
image = "minio/mc";
dependsOn = [ "${name}-minio" ];
entrypoint = "/bin/sh";
cmd = [ "-c" "mc config host add ...; mc mb --ignore-existing ...; exit 0;" ];
log-driver = "journald";
extraOptions = [ "--network=${name}" ];
};
Prevent restart loop for init containers:
systemd.services."docker-${name}-init".serviceConfig = {
Restart = lib.mkForce "on-failure";
};
Extra Systemd Customization
Container services are named docker-<containerName>. Extend them in the same file:
Extra dependencies
systemd.services."docker-pangolin" = {
after = [ "pangolin-config-sync.service" ];
requires = [ "pangolin-config-sync.service" ];
};
Soft dependency on another container
systemd.services."docker-rathole".wants = [ "docker-gerbil.service" ];
Custom restart timing
systemd.services."docker-rathole".serviceConfig.RestartSec = lib.mkOverride 90 "10s";
Pre-create storage directories
systemd.tmpfiles.rules = [
"d /store/myapp 0750 1000 mygroup -"
"d /store/myapp/db 0750 1000 mygroup -"
];
Full Example: Multi-Container Stack
{ secrets, ... }:
let
name = "myapp";
store = "/store/${name}";
env = secrets.service.myapp;
in
{
virtualisation.oci-stacks.${name} = {
containers = {
"${name}-web" = {
image = "myapp/web:latest";
environment = env;
volumes = [ "${store}/data:/app/data:rw" ];
dependsOn = [ "${name}-db" ];
log-driver = "journald";
extraOptions = [
"--network=${name}"
"--network-alias=web"
"--expose=8080"
];
};
"${name}-db" = {
image = "docker.io/postgres:16";
environment = {
POSTGRES_PASSWORD = env.DB_PASSWORD;
POSTGRES_DB = name;
};
volumes = [ "${store}/db:/var/lib/postgresql/data:rw" ];
log-driver = "journald";
extraOptions = [
"--network=${name}"
"--network-alias=database"
''--health-cmd=["pg_isready", "-U", "postgres"]''
"--health-interval=10s"
"--health-retries=5"
];
};
};
description = "My application stack";
};
systemd.tmpfiles.rules = [
"d ${store} 0750 1000 1000 -"
"d ${store}/data 0750 1000 1000 -"
"d ${store}/db 0750 999 999 -"
];
}
Conventions
- Stack name variable:
let name = "stackName"; in — reference ${name} throughout
- Storage paths:
store = "/store/${name}";
- Log driver: Prefer
"journald" (integrates with systemd journal)
- Network aliases:
--network-alias=<service-role> for inter-container DNS
- Always include:
--network=${name} in extraOptions (unless host/shared networking)
- Secrets: Pass via module arguments (
{ secrets, ... }:) — never hardcode
- Container naming: Prefix with stack name:
"${name}-db", "${name}-web"
- Volumes: Use
:rw or :ro suffix explicitly