Containers and images

Nix gives you two different container stories, and they solve different problems. Declarative NixOS containers run a slice of NixOS next to your host, managed by the same config. OCI images package an application into a portable artifact you can ship to any Docker or Podman runtime. Pick the first when you want isolated services on a NixOS machine you control. Pick the second when you need an image that runs somewhere else.

Declarative NixOS containers

A NixOS container is a lightweight system container declared right in configuration.nix. Each one is a small NixOS instance with its own services, built and activated alongside the host.

{
  containers.web = {
    autoStart = true;
    config = { config, pkgs, ... }: {
      services.nginx.enable = true;
      services.nginx.virtualHosts."localhost".root = "/var/www";
      networking.firewall.allowedTCPPorts = [ 80 ];
      system.stateVersion = "24.11";
    };
  };
}

By default a container shares the host network. To give it an isolated network with a known address, use privateNetwork with a host and local address.

{
  containers.web = {
    autoStart = true;
    privateNetwork = true;
    hostAddress = "192.168.100.1";
    localAddress = "192.168.100.2";
    config = { pkgs, ... }: {
      services.nginx.enable = true;
      system.stateVersion = "24.11";
    };
  };
}

After a rebuild, manage containers with the nixos-container command.

# list containers and their status
nixos-container list
 
# start, stop, and open a root shell
sudo nixos-container start web
sudo nixos-container stop web
sudo nixos-container root-login web

These containers rebuild with the host. When you run nixos-rebuild switch --flake .#nixos-host, the container's config is evaluated and updated too.

Building OCI images with dockerTools

When you need an image that runs on a Docker or Podman host anywhere, build one from Nix with dockerTools. buildLayeredImage produces an image where each store path becomes its own layer, so rebuilds reuse unchanged layers.

# in a flake's outputs, with pkgs for x86_64-linux
{
  packages.x86_64-linux.image = pkgs.dockerTools.buildLayeredImage {
    name = "hello-web";
    tag = "latest";
    contents = [ pkgs.python3 ];
    config = {
      Cmd = [ "${pkgs.python3}/bin/python3" "-m" "http.server" "8080" ];
      ExposedPorts."8080/tcp" = { };
    };
  };
}

Build the image and load it into a local Docker or Podman daemon.

nix build .#image
docker load < result
docker run -p 8080:8080 hello-web:latest

The output is a normal OCI image. Nothing about it depends on Nix at runtime, so it runs on any host with a container engine.

Running other people's containers

If you just want to run an existing image on a NixOS host, declare it with virtualisation.oci-containers. Choose podman or docker as the backend.

{
  virtualisation.podman.enable = true;
 
  virtualisation.oci-containers = {
    backend = "podman";
    containers.grafana = {
      image = "grafana/grafana:latest";
      ports = [ "3000:3000" ];
      volumes = [ "grafana-data:/var/lib/grafana" ];
    };
  };
}

NixOS turns each entry into a systemd service, so the container starts on boot and restarts on failure like any other service.

Advanced

buildLayeredImage shares layers across images that use the same store paths, so a fleet of images built from one nixpkgs deduplicates most of their size on a registry. For very large images, streamLayeredImage writes the tarball to stdout instead of into the store, which avoids keeping a second copy on disk.

nix build .#image   # uses buildLayeredImage, image lands in the store
# streamLayeredImage variant pipes straight to the daemon
nix run .#streamImage | docker load

Next

For packaging the application that goes inside an image, see Writing packages. For complete host configs see Examples, for shipping to a server see Deploying to remote machines, and for the flake outputs that expose an image see Flakes.