OFFSET偏移
← All articlesOFFSET / FIELD NOTESNO. 071

Building and running container images with dockerTools

Manage application derivations, runtime closures, and container data state separately.

English translation · Code and commands follow the Chinese original.

IN THESE READING PATHSNixOS virtualization and deployment ↗
About 4 min readStart reading

Translated from nix.dev: Building and running Docker images

Nix's dockerTools can build Docker images from package closures. Containers do not have to run NixOS; image size, caching, and build time depend on the actual dependencies and layering strategy. The build environment requires Nix, while the loading and running examples require Docker.

Why Build Docker Images with Nix?#

Feature Nix dockerTools Dockerfile
Image Contents Organized around declarations and runtime closures Organized around the base image and individual instructions
Reproducibility Pinning nixpkgs, source code, and dependencies improves reproducibility; not every build is inherently bit-for-bit identical Pinning the base-image digest, dependencies, and build process also improves reproducibility
Layer Caching buildLayeredImage automatically organizes dependency layers, with a configurable layer count Layer structure is determined mainly by instructions and multistage-build design
Security Updates Update inputs, then rebuild, load, and deploy the image Update the base image/dependencies, then rebuild and deploy

Updating Nix inputs does not automatically patch running containers. A built image is a separate layer from running instances, data volumes, and network permissions. The import <nixpkgs> examples below depend on the current search path; real projects should use pinned nixpkgs.

Basic Configuration#

Create docker-image.nix:

{ pkgs ? import <nixpkgs> {} }:

pkgs.dockerTools.buildImage {
  name = "my-nixos-app";
  tag = "latest";
  
  copyToRoot = pkgs.buildEnv {
    name = "image-root";
    paths = [ 
      pkgs.bash
      pkgs.coreutils
      pkgs.cowsay
    ];
    pathsToLink = [ "/bin" ];
  };
  
  config = {
    Cmd = [ "/bin/cowsay" "Hello from NixOS!" ];
  };
}

Build the Image#

nix-build docker-image.nix

docker load < result

Run the Container#

docker run my-nixos-app:latest

Build an Image for a Web Service#

{ pkgs ? import <nixpkgs> {} }:

pkgs.dockerTools.buildImage {
  name = "nixos-web-server";
  tag = "latest";
  copyToRoot = pkgs.buildEnv {
    name = "image-root";
    paths = [ pkgs.nginx ];
    pathsToLink = [ "/bin" ];
  };
  extraCommands = "mkdir -p tmp";
  # 明确提供配置;包内二进制不会自动拥有可写 /var 和 /etc/nginx
  config = {
    Cmd = [ "${pkgs.nginx}/bin/nginx" "-c" "${pkgs.writeText "nginx.conf" ''
      daemon off;
      pid /tmp/nginx.pid;
      error_log /dev/stderr;
      events {}
      http {
        access_log /dev/stdout;
        client_body_temp_path /tmp/client_body;
        proxy_temp_path /tmp/proxy;
        fastcgi_temp_path /tmp/fastcgi;
        uwsgi_temp_path /tmp/uwsgi;
        scgi_temp_path /tmp/scgi;
        server { listen 80; location / { return 200 "Nix container\n"; } }
      }
    ''}" ];
    ExposedPorts."80/tcp" = {};
  };
}

ExposedPorts is only image metadata; it does not automatically publish a host port. Use docker run --rm -p 127.0.0.1:8080:80 nixos-web-server:latest for local verification.

Multistage Builds#

In Nix, build the application derivation first, then put its runtime outputs into the image. A compiler is not included in the runtime image merely because it was used during the build. This Go example requires source code and a replacement vendorHash; it cannot be copied and run unchanged.

{ pkgs ? import <nixpkgs> {} }:

let
  # 构建应用
  myApp = pkgs.buildGoModule {
    pname = "my-app";
    version = "1.0.0";
    src = ./.; # 含 go.mod、go.sum 及主程序
    vendorHash = pkgs.lib.fakeHash; # 用实际依赖 hash 替换;无依赖时按 builder 合约设 null
  };
in
pkgs.dockerTools.buildLayeredImage {
  name = "my-go-app";
  tag = "latest";
  
  contents = [ myApp pkgs.cacert ];
  
  config = {
    Cmd = [ "/bin/my-app" ];
  };
}

Optimizing Layered Images#

Use buildLayeredImage for better layer caching:

{ pkgs ? import <nixpkgs> {} }:

pkgs.dockerTools.buildLayeredImage {
  name = "layered-app";
  tag = "latest";
  
  # 基础层
  contents = with pkgs; [
    bash
    coreutils
    curl
  ];
  
  # 应用层
  extraCommands = ''
    mkdir -p /app
    cp ${./my-app} /app/
  '';
  
  config = {
    Cmd = [ "/app/my-app" ];
    WorkingDir = "/app";
  };
}

Tips for Reducing Image Size#

{ pkgs ? import <nixpkgs> {} }:

pkgs.dockerTools.buildImage {
  name = "minimal-app";
  tag = "latest";
  
  # 使用 pkgsStatic 静态链接
  copyToRoot = pkgs.buildEnv {
    name = "image-root";
    paths = [ pkgs.pkgsStatic.hello ];
    pathsToLink = [ "/bin" ];
  };
  
  # 移除不必要的包
  runAsRoot = ''
    #!${pkgs.runtimeShell}
    rm -rf /share/doc /share/man
  '';
  
  config = {
    Cmd = [ "/bin/hello" ];
  };
}

Push the Image to a Registry#

# 加载镜像
docker load < result

# 打标签
docker tag my-nixos-app:latest registry.example.com/my-app:v1.0

# 推送
docker push registry.example.com/my-app:v1.0

Practical Examples#

Database Images

This example retains the historical PostgreSQL 14 version scope. A database needs more than binaries: it also needs a user, data-directory ownership, and initial setup. Images should not contain a real database or password. Initialize a separate named volume first, then run the service.

{ pkgs ? import <nixpkgs> {} }:

pkgs.dockerTools.buildLayeredImage {
  name = "nixos-postgres";
  tag = "14";
  
  contents = [ pkgs.postgresql_14 ];
  
  extraCommands = ''
    mkdir -p etc var/lib/postgresql/data tmp
    echo 'postgres:x:999:999:PostgreSQL:/var/lib/postgresql:/bin/false' > etc/passwd
    echo 'postgres:x:999:' > etc/group
  '';
  fakeRootCommands = ''
    chown -R 999:999 var/lib/postgresql
  '';
  
  config = {
    User = "999:999";
    Cmd = [ "/bin/postgres" "-D" "/var/lib/postgresql/data" ];
    Volumes."/var/lib/postgresql/data" = {};
    ExposedPorts = {
      "5432/tcp" = {};
    };
    Env = [ "PGDATA=/var/lib/postgresql/data" ];
  };
}

Initialization and startup steps (empty volume, local verification only):

docker load < result
docker volume create pg-example-data
docker run --rm --mount source=pg-example-data,target=/var/lib/postgresql/data \
  nixos-postgres:14 /bin/initdb -D /var/lib/postgresql/data
docker run --rm --mount source=pg-example-data,target=/var/lib/postgresql/data \
  nixos-postgres:14

Container engines may differ in how they initialize and assign ownership to new volumes. First check that UID 999 can write to the volume. The example above does not enable network access. Before enabling TCP, publishing ports, and configuring authentication, explicitly configure listen_addresses and pg_hba.conf, and inject credentials through runtime secrets. Database upgrades also require the backup and migration procedures for the relevant versions.

References & further reading

  1. nix.dev Container Image Tutorial ↗
  2. Nixpkgs dockerTools Manual ↗
  3. PostgreSQL initdb ↗

Adapted from technical notes. Examples use generic names.

Continue along a reading path

NixOS virtualization and deployment

  1. 01Validating NixOS configuration with QEMU
  2. 02Building a bootable NixOS ISO
  3. 03Building and running container images with dockerTools
  4. 04Deploying NixOS over SSH
  5. 05Dividing deployment responsibilities between Terraform and NixOS
  6. 06Designing layered desktop and development environments
CONTINUE READING / NEXT ARTICLEDeploying NixOS over SSH

Distinguish local builds, temporary remote activation, persistent switching, and failure rollback.