OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 071

用 dockerTools 构建与运行容器镜像

把应用 derivation、运行时 closure 和容器数据状态分开管理。

所在专题NixOS 虚拟化与部署 ↗
约 11 分钟阅读开始阅读

翻译自 nix.dev: Building and running Docker images

Nix 的 dockerTools 可以从软件包 closure 构建 Docker 镜像。它不要求容器运行 NixOS;大小、缓存和构建时间取决于实际依赖与分层策略。构建环境需要 Nix,加载和运行示例需要 Docker。

为什么用 Nix 构建 Docker 镜像?#

特性 Nix dockerTools Dockerfile
镜像内容 根据声明与运行时 closure 组织内容 根据基础镜像和各条指令组织内容
可复现性 固定 nixpkgs、源代码和依赖可提高可复现性;不是所有构建天然位级相同 固定基础镜像 digest、依赖和构建过程也能提高可复现性
层缓存 buildLayeredImage 自动组织依赖层,可调层数 层结构主要由指令与多阶段构建设计决定
安全更新 更新输入并重新构建、加载和部署镜像 更新基础镜像/依赖并重新构建和部署

更新 Nix 输入不会自动修补已运行容器。构建好的镜像与运行实例、数据卷、网络权限是不同层。以下 import <nixpkgs> 示例依赖当前搜索路径;实际项目应使用锁定的 nixpkgs。

基础配置#

创建一个 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!" ];
  };
}

构建镜像#

nix-build docker-image.nix

docker load < result

运行容器#

docker run my-nixos-app:latest

构建运行 Web 服务的镜像#

{ 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 只是镜像元数据,不会自动发布宿主机端口。可用 docker run --rm -p 127.0.0.1:8080:80 nixos-web-server:latest 做本地验证。

多阶段构建#

在 Nix 中先构建应用 derivation,再把运行时产物放入镜像;编译器不会仅因为参与构建就被包含在运行镜像里。此 Go 例子需准备源代码并替换 vendorHash,不能直接复制运行。

{ 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" ];
  };
}

分层镜像优化#

使用 buildLayeredImage 可以创建更好的层缓存:

{ 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";
  };
}

镜像瘦身技巧#

{ 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" ];
  };
}

推送镜像到仓库#

# 加载镜像
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

实际应用示例#

数据库镜像

此例保留 PostgreSQL 14 的历史版本范围。数据库不仅需要二进制,还需要用户、数据目录所有权和首次初始化;镜像不应内置真实数据库或密码。先初始化独立命名卷,再运行服务。

{ 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" ];
  };
}

初始化与运行步骤(空卷、仅本地验证):

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

不同容器引擎对新卷初始化和所有权的行为可能不同;先检查 UID 999 对卷是否可写。上例未开放网络访问。启用 TCP、发布端口和设置认证前,需单独声明 listen_addresses 与 pg_hba.conf,并通过运行时 secret 注入凭据。数据库升级还需遵循对应版本的备份与迁移流程。

参考与延伸

  1. nix.dev 容器镜像教程 ↗
  2. Nixpkgs dockerTools 手册 ↗
  3. PostgreSQL initdb ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

NixOS 虚拟化与部署

  1. 01用 QEMU 验证 NixOS 配置
  2. 02构建可启动的 NixOS ISO
  3. 03用 dockerTools 构建与运行容器镜像
  4. 04通过 SSH 部署 NixOS
  5. 05Terraform 与 NixOS 的部署分工
  6. 06设计分层的桌面与开发环境
CONTINUE READING / 下一篇通过 SSH 部署 NixOS

区分本地构建、远端临时激活、持久切换和故障回滚。