Docker
- debian/#install-using-the-repository
- rhel/#install-using-the-repository
- #daemon-configuration-file
- HTTP 代理环境变量的大小写说明
- docker-ce 火山引擎镜像
debian12 安装 docker
# Add Docker's official GPG key:
apt-get update
apt-get install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
debian13 安装 docker
相比 Debian12 的区别仅是使用 DEB822 格式的 sources.list 文件
# Add Docker's official GPG key:
apt-get update
apt-get install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
centos 9 安装 docker
dnf -y install dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
普通用户不需要sudo即可使用docker cli
sudo usermod -aG docker $USER
newgrp docker
docker ps
设置 docker daemon 代理
这个是用于 pull 镜像时的代理设置。
mkdir -p /etc/systemd/system/docker.service.d
touch /etc/systemd/system/docker.service.d/http-proxy.conf
if ! grep HTTP_PROXY /etc/systemd/system/docker.service.d/http-proxy.conf;
then
cat >> /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:3128/" "HTTPS_PROXY=http://127.0.0.1:3128/" "NO_PROXY=arloor.com,localhost,127.0.0.1,docker-registry.somecorporation.com"
EOF
fi
# Flush changes:
systemctl daemon-reload
#Restart Docker:
systemctl restart docker
#Verify that the configuration has been loaded:
systemctl show --property=Environment docker --no-pager
# 像这样:Environment=HTTP_PROXY=http://127.0.0.1:8081/ NO_PROXY=localhost,127.0.0.1,docker-registry.so
或者:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"proxies": {
"http-proxy": "http://127.0.0.1:3128",
"https-proxy": "http://127.0.0.1:3128",
"no-proxy": "*.arloor.com,.example.org,127.0.0.0/8,localhost,127.0.0.1,docker-registry.somecorporation.com"
}
}
EOF
systemctl restart docker
设置 docker CLI 的 HTTP 代理
这个是用于 build 和 run 时的代理设置。详见:Use a proxy server with the Docker CLI
全局设置
注意,这个配置文件中还有其他的配置,例如 docker registry 的账户密码,不建议直接覆盖。
mkdir -p ~/.docker
cat > ~/.docker/config.json <<EOF
{
"proxies": {
"default": {
"httpProxy": "http://127.0.0.1:3128",
"httpsProxy": "http://127.0.0.1:3128",
"noProxy": "*.arloor.com,.example.org,127.0.0.0/8,localhost,127.0.0.1,docker-registry.somecorporation.com"
}
}
}
EOF
这个配置会在 docker run 时自动给新容器注入 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 及对应小写环境变量;docker build 时也会自动注入对应的 build args。
注意:如果代理服务监听在宿主机的
127.0.0.1:3128,容器默认 bridge 网络下访问127.0.0.1会指向容器自身,不是宿主机。此时需要配合--network=host(Linux),或把代理地址改成容器能访问到的宿主机地址,例如host.docker.internal/ bridge 网关 IP。
单次设置
# build
docker build --build-arg HTTP_PROXY="${HTTP_PROXY:-}" --build-arg HTTPS_PROXY="${HTTPS_PROXY:-}" --network=host
# run
docker run --env HTTP_PROXY="http://proxy.example.com:3128" redis --network=host
Linux 上如果不用 host network,也可以显式增加宿主机名映射:
docker run \
--add-host=host.docker.internal:host-gateway \
--env HTTP_PROXY="http://host.docker.internal:3128" \
--env HTTPS_PROXY="http://host.docker.internal:3128" \
redis
也可以直接把代理地址写成默认 bridge 网关 IP(常见是 172.17.0.1):
{
"proxies": {
"default": {
"httpProxy": "http://172.17.0.1:3128",
"httpsProxy": "http://172.17.0.1:3128"
}
}
}
查看实际网关 IP:
docker network inspect bridge | grep Gateway
注意:代理服务本身也必须监听这个地址。如果代理只监听
127.0.0.1:3128,容器访问172.17.0.1:3128或host.docker.internal:3128仍然不通;代理需要监听0.0.0.0:3128,或至少监听 bridge 网关 IP,并用防火墙限制来源只允许 Docker 网段。
清理 Docker 构建缓存和无用镜像
查看 Docker 占用空间:
docker system df
清理悬空镜像:
docker image prune
清理构建缓存:
docker builder prune
如果使用 buildx:
docker buildx prune
清理所有未被容器使用的镜像和构建缓存:
docker system prune -a
注意:
docker system prune -a会删除所有未被容器使用的镜像,不只是中间镜像;如果加--volumes还会删除未使用的数据卷,需谨慎。
Docker Compose 使用
Compose V2 使用 docker compose 子命令;老版本的 docker-compose 是独立二进制。
compose.yml 示例
services:
app:
image: nginx:1.27-alpine
container_name: demo-nginx
depends_on:
redis:
condition: service_healthy
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
environment:
TZ: Asia/Shanghai
restart: unless-stopped
redis:
image: redis:7.4-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
volumes:
redis-data:
新版 Compose 规范不再强制写顶层
version,旧示例里的version: "3"可以省略。
常用命令
# 启动
docker compose up -d
# 查看服务状态
docker compose ps
# 查看日志
docker compose logs -f
docker compose logs -f app
# 进入容器
docker compose exec app sh
# 重启服务
docker compose restart app
# 拉取镜像
docker compose pull
# 重新构建并启动
docker compose up -d --build
# 强制重新创建容器,即使配置和镜像没有变化
docker compose up -d --force-recreate
# 重新创建指定服务,并同时重新创建它依赖的服务
docker compose up -d --force-recreate --always-recreate-deps app
# 只启动指定服务,不自动启动 depends_on 依赖
docker compose up -d --no-deps app
# 停止并删除容器、网络
docker compose down
# 同时删除匿名卷和 compose 文件中声明的 named volume
docker compose down -v
Compose 常用参数是
--force-recreate,不是--force-restart;docker compose restart只是重启已有容器,不会按新配置重新创建容器。
常用参数
# 指定项目名,影响容器、网络、卷名前缀
docker compose -p myapp up -d
# 指定 compose 文件
docker compose -f compose.yml -f compose.prod.yml up -d
# 展开并检查最终配置
docker compose config
环境变量
同目录下的 .env 默认用于 Compose 文件变量替换:
NGINX_PORT=8080
services:
app:
image: nginx:1.27-alpine
ports:
- "${NGINX_PORT}:80"
如果要把环境变量传进容器,用 environment 或 env_file:
services:
app:
image: nginx:1.27-alpine
env_file:
- app.env
.env主要给 Compose 自己做变量替换;env_file才是批量传给容器的环境变量文件。
depends_on
短语法只保证依赖服务先启动,不保证依赖服务已经可用:
services:
app:
depends_on:
- redis
如果需要等待依赖服务健康后再启动,用长语法加 healthcheck:
services:
app:
depends_on:
redis:
condition: service_healthy
redis:
image: redis:7.4-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
常见 condition:
| condition | 含义 |
|---|---|
service_started |
依赖服务已启动,短语法等价于这个 |
service_healthy |
依赖服务的 healthcheck 通过 |
service_completed_successfully |
依赖服务成功退出,常用于一次性初始化任务 |
Podman
删除 podman build 中途退出的中间镜像
https://github.com/containers/podman/issues/7889
podman ps --all --storage
buildah rm --all
podman rmi -a
设置 podman CLI 的 HTTP 代理设置
podman 的 build 和 run 都会默认使用 podman 进程的 http 代理环境变量,如果容器不需要的话,可以使用 --http-proxy=false 来关闭。
podman 保存密码
podman login quay.io -u arloor -p ${token}
cat /run/user/0/containers/auth.json #密码在此
cp /run/user/0/containers/auth.json ~/.config/podman_auth.json
if ! grep REGISTRY_AUTH_FILE ~/.zshrc;then
export REGISTRY_AUTH_FILE=~/.config/podman_auth.json
echo "export REGISTRY_AUTH_FILE=~/.config/podman_auth.json" >> ~/.zshrc
echo " add REGISTRY_AUTH_FILE to ~/.zshrc"
else
echo "REGISTRY_AUTH_FILE already exists in ~/.zshrc"
fi
why login registry auth.json always been cleared?:默认保存在 /run/user/0 下,在用户 log out 的时候会被清空。如果需要持久化保存,可以将其复制到其他位置,并设置环境变量 REGISTRY_AUTH_FILE 指向该文件或使用 --authfile 命令行参数。
Podman 使用 systemd 管理容器
podman generate systemd
参考 man podman-generate-systemd 或 podman-generate-systemd(1)
podman run -d \
--pull=newer \
--name redis \
--network host \
--replace \
--rm \
redis
podman generate systemd --new --name redis --env=HTTP_PROXY --env=HTTPS_PROXY| tee /lib/systemd/system/redis.service
systemctl daemon-reload
# systemctl enable redis --now
其中 podman run 的 --rm 和 podman generate systemd 的--new 含义都是停止容器时删除容器,下次全新创建。所以 –rm 和 –new 要么同时出现,要么同时缺失。
其中 --pull=newer 表示每次都 pull 镜像,这在使用 latest 镜像时很有用
qualet 运行
参考podman-systemd.unit.5或者 man podman-systemd.unit
cat > /tmp/proxy.container <<'EOF'
[Unit]
Wants=network-online.target
After=network-online.target
[Container]
Image=docker.io/arloor/rust_http_proxy:bpf
ContainerName=proxy
Pull=newer
PodmanArgs=--privileged
Volume=/tmp:/tmp
Volume=/root/.acme.sh:/root/.acme.sh
Volume=/usr/share/nginx/html:/usr/share/nginx/html
Network=host
Exec=-p 444 -p 443 -p 9443 \
-w /usr/share/nginx/html/blog \
-r arloor \
--never-ask-for-auth
[Service]
Environment=HTTPS_PROXY=http://127.0.0.1:3128
Environment=HTTP_PROXY=http://127.0.0.1:3128
Restart=on-failure
TimeoutStopSec=70
[Install]
WantedBy=default.target
EOF
QUADLET_UNIT_DIRS=/tmp /usr/libexec/podman/quadlet -dryrun #测试
mkdir -p /etc/containers/systemd;mv /tmp/proxy.container /etc/containers/systemd/proxy.container
rm -f /lib/systemd/system/proxy.service
systemctl daemon-reload
systemctl restart proxy
生成在 /run/systemd/generator/proxy.service®
Docker Hub pull-through cache
https://distribution.github.io/distribution/about/configuration/ 注意 Registry:2 已经不维护的,并且有明显 bug,推荐使用 Registry:3
- 启动
Registry:3
mkdir -p /etc/distribution
cat > /etc/distribution/config.yml <<EOF
version: 0.1
log:
level: info
http:
addr: :6666
debug:
addr: :3001
prometheus:
enabled: true
proxy:
remoteurl: https://registry-1.docker.io
username: ${your_docker_hub_username}
password: ${your_docker_hub_token}
ttl: 1h
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
EOF
docker stop registry&&docker rm registry
docker run -d --restart=always --network host --name registry \
-e http_proxy="http://localhost:3128" -e https_proxy="http://localhost:3128" \
-e HTTP_PROXY="http://localhost:3128" -e HTTPS_PROXY="http://localhost:3128" \
-v /etc/distribution/config.yml:/etc/distribution/config.yml \
registry:3 /etc/distribution/config.yml
docker logs -f registry
- 配置 docker daemon
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"registry-mirrors": ["http://ttl.arloor.com:6666"]
}
EOF
systemctl restart docker
- 配置 podman
mkdir -p $HOME/.config/containers
cat > $HOME/.config/containers/registries.conf<<EOF
unqualified-search-registries = ["docker.io"]
[[registry]]
location = "docker.io"
[[registry.mirror]]
location = "ttl.arloor.com:6666" # 你的镜像加速地址
insecure = true # 如果镜像站使用HTTP而非HTTPS,设为true
EOF
- 配置 K3s 的 containerd
K3s 会自己生成 containerd 配置,不要直接修改生成的 config.toml 或 hosts.toml。在每个可能运行 Pod 的 Server/Agent 节点配置 /etc/rancher/k3s/registries.yaml:
mkdir -p /etc/rancher/k3s
cat > /etc/rancher/k3s/registries.yaml <<'EOF'
mirrors:
docker.io:
endpoint:
- "http://ttl.arloor.com:6666"
EOF
# Server 节点
systemctl restart k3s
# Agent 节点执行 systemctl restart k3s-agent
之后 Pod 仍然使用原始镜像名,例如 docker.io/library/nginx:latest,containerd 会优先请求 pull-through cache。默认情况下,cache 不可用时会回退到 Docker Hub;如果必须强制经过 cache,可为 K3s 增加 --disable-default-registry-endpoint。详见 K3s Private Registry Configuration。
- 配置普通 K8s 的 containerd
containerd 1.5 之后推荐使用 certs.d/hosts.toml,旧的 registry.mirrors 配置已废弃。先在每个 K8s 节点创建 Docker Hub 的 host namespace:
mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'EOF'
server = "https://registry-1.docker.io"
[host."http://ttl.arloor.com:6666"]
capabilities = ["pull", "resolve"]
EOF
并确认 /etc/containerd/config.toml 已指向该目录。containerd 1.x 使用:
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
containerd 2.x 使用:
[plugins."io.containerd.cri.v1.images".registry]
config_path = "/etc/containerd/certs.d"
如果修改了 config_path,需要重启 containerd:
systemctl restart containerd
systemctl restart kubelet
可通过 CRI 验证拉取,并同时观察 Registry:3 日志:
crictl pull docker.io/library/alpine:latest
docker logs -f registry
ctr images pull 不一定读取 CRI 的 registry 配置,手工测试时需要指定 ctr images pull --hosts-dir /etc/containerd/certs.d ...。详见 containerd Registry Configuration。
本例只代理 docker.io,不会加速 registry.k8s.io、ghcr.io 等其他 registry。另外,cache 使用了 Docker Hub 账号凭据,不要将未鉴权的 HTTP 端口直接暴露到公网,至少应使用防火墙限制到集群节点网段。
Docker 构建参考
Dockerfile 规范和常见范式
通用规范
- 固定 Dockerfile 语法版本,启用 BuildKit 新能力。
- 基础镜像尽量固定到小版本,生产环境建议再加 digest(防止上游 tag 漂移)。
- 优先使用多阶段构建(builder/runtime 分离),减小最终镜像体积和攻击面。
- 优化缓存命中:先
COPY依赖描述文件,再安装依赖,最后COPY业务代码。 - 包管理器操作要“同层完成并清理”:
- Debian/Ubuntu:
apt-get update && apt-get install ... && rm -rf /var/lib/apt/lists/*
- Debian/Ubuntu:
- 默认使用非 root 用户运行应用。
ENTRYPOINT/CMD使用 JSON 格式(exec form),避免 shell 包裹导致信号处理异常。- 区分
ARG和ENV:ARG只在构建期有效,ENV会进入运行时环境。 - 敏感信息不要写进镜像层,使用 BuildKit secret mount。
- 配置
.dockerignore,避免把.git、日志、构建产物打进 build context。
指令详解(ARG/ENV/COPY/ADD/VOLUME/WORKDIR)
| 指令 | 作用 | 生效范围 / 时机 | 常见写法 | 注意事项(常见坑) |
|---|---|---|---|---|
ARG |
声明构建参数,用于镜像构建时动态传参 | 仅构建期有效;不会自动进入容器运行时环境 | ARG APP_VER=1.2.3FROM node:${NODE_VER}docker build --build-arg APP_VER=2.0 . |
1) FROM 前定义的 ARG 仅能用于 FROM 行;若后续指令还要用,需要在阶段内再声明一次。2) 不要传密钥,ARG 可能出现在构建历史/元数据中。 |
ENV |
设置环境变量,供后续构建步骤和容器运行时使用 | 当前阶段后续指令可见;并写入镜像配置,容器启动后默认存在 | ENV APP_ENV=prodENV PATH=/app/bin:$PATH |
1) 会进入最终镜像,避免放敏感信息。2) ENV 可覆盖同名 ARG 在后续步骤中的效果。 |
COPY |
将本地构建上下文文件(或其他阶段产物)复制进镜像 | 构建期执行,产生新层 | COPY . /appCOPY --from=build /out/app /usr/local/bin/appCOPY --chown=10001:10001 . /app |
1) 只复制 build context 内文件,不能 ../ 越界。2) 优先于 ADD 使用,行为更可预测。3) 先复制依赖文件再安装依赖,可提升缓存命中。 |
ADD |
类似 COPY,但支持额外能力(本地 tar 自动解包、URL 源) |
构建期执行,产生新层 | ADD app.tar.gz /opt/app/ADD https://example.com/a.tgz /tmp/ |
1) 除非你明确需要“自动解包/远程 URL”,否则用 COPY。2) URL 下载会引入额外不确定性,通常建议改为 RUN curl/wget 并做校验。 |
VOLUME |
声明容器运行时挂载点(数据卷) | 主要影响运行时;镜像中记录为元数据 | VOLUME ["/var/lib/mysql"]VOLUME /data |
1) 更推荐在编排层(Compose/K8s)声明挂载,Dockerfile 里慎用。2) 该路径在运行时会被卷接管,若依赖镜像内后续写入该目录,行为易混淆。 |
WORKDIR |
设置后续指令默认工作目录 | 对后续 RUN/CMD/ENTRYPOINT/COPY/ADD 生效 |
WORKDIR /appWORKDIR /app/bin |
1) 目录不存在会自动创建。2) 相对路径会基于上一个 WORKDIR 叠加,建议尽量用绝对路径降低歧义。 |
速记:可预测复制用
COPY,特殊场景(解包/URL)才用ADD;构建参数用ARG,运行时变量用ENV。
常用骨架(Debian 系)
# syntax=docker/dockerfile:1.7
FROM debian:12.9-slim
SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl tzdata \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
范式 1:Go 多阶段构建(推荐)
# syntax=docker/dockerfile:1.7
FROM golang:1.24-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]
范式 2:Node.js 生产镜像(依赖和运行时分层)
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
RUN useradd -r -u 10001 nodeapp && chown -R nodeapp:nodeapp /app
USER nodeapp
EXPOSE 3000
CMD ["node", "server.js"]
常用检查命令
# lint Dockerfile
docker run --rm -i hadolint/hadolint < Dockerfile
# 使用 BuildKit 构建
DOCKER_BUILDKIT=1 docker build -t myapp:dev .
# 构建时挂载 secret(示例:npm 私有源)
DOCKER_BUILDKIT=1 docker build \
--secret id=npmrc,src=$HOME/.npmrc \
-t myapp:dev .
BuildKit:原理、作用和使用
BuildKit 是 Docker 的镜像构建后端,docker buildx 是常用的客户端入口。它会把 Dockerfile 转成依赖图,按内容计算缓存并执行各阶段,最后把结果输出为本地镜像、文件或 registry 镜像。现在使用 docker buildx build 就是在使用 BuildKit,不需要再单独设置 DOCKER_BUILDKIT=1。
下面不展开内部的 LLB、Solver 和 Worker,直接以 tuke 为例,看 BuildKit 如何构建并发布一个同时包含 Go 服务、Node.js、Chromium 和 agent-browser 的生产镜像。
tuke 的完整 Dockerfile
# syntax=docker/dockerfile:1.7
FROM golang:1.26.5-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,id=tuke-go-mod,target=/go/pkg/mod,sharing=locked \
go mod download
COPY cmd ./cmd
COPY internal ./internal
RUN --mount=type=cache,id=tuke-go-mod,target=/go/pkg/mod,sharing=locked \
--mount=type=cache,id=tuke-go-build,target=/root/.cache/go-build,sharing=locked \
CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w" \
-o /out/tuke-agent \
./cmd/server
FROM node:24-trixie-slim AS runtime
ARG AGENT_BROWSER_VERSION=0.33.2
RUN --mount=type=cache,id=tuke-apt-lists,target=/var/lib/apt/lists,sharing=locked \
--mount=type=cache,id=tuke-apt-cache,target=/var/cache/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
bash \
bc \
ca-certificates \
chromium \
curl \
dnsutils \
file \
fonts-liberation \
fonts-noto-cjk \
fonts-noto-color-emoji \
git \
iproute2 \
iputils-ping \
jq \
less \
lsof \
nano \
netcat-openbsd \
openssh-client \
procps \
psmisc \
python-is-python3 \
python3 \
ripgrep \
rsync \
tree \
tzdata \
unzip \
vim-tiny \
wget \
zip \
&& install -d -o root -g root -m 0700 /data /data/sessions /workspace
RUN npm install --global --omit=dev --allow-scripts=agent-browser \
"agent-browser@${AGENT_BROWSER_VERSION}" \
&& npm cache clean --force
COPY --from=build /out/tuke-agent /usr/local/bin/tuke-agent
COPY .agents/skills /opt/tuke/skills
ENV TZ=Asia/Shanghai \
AGENT_BROWSER_CONTENT_BOUNDARIES=1 \
AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium \
AGENT_BROWSER_MAX_OUTPUT=50000 \
TUKE_ADDR=0.0.0.0:8080 \
TUKE_DATABASE_PATH=/data/sessions/tuke.db \
TUKE_BASH_WORKDIR=/workspace \
TUKE_SKILLS_DIR=/opt/tuke/skills
WORKDIR /workspace
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/tuke-agent"]
这个 Dockerfile 把编译与运行环境分成了 build、runtime 两个 stage,并同时使用普通层缓存、cache mount 和后面 deploy.sh 配置的 registry cache。
Dockerfile frontend
第一行固定 Dockerfile frontend 版本:
# syntax=docker/dockerfile:1.7
# syntax=docker/dockerfile:1.7 让 Dockerfile 可以使用 RUN --mount=type=cache 等 BuildKit 语法。BuildKit 构建开始时会先解析并拉取这个 frontend,所以网络异常时,甚至可能在拉取第一个 FROM 之前就看到 docker/dockerfile:1.7 的报错。
Go 依赖与编译缓存
Go stage 没有一开始就 COPY . .,而是先复制 go.mod、go.sum,执行依赖下载后才复制业务源码。这里叠加使用了两种缓存:
COPY go.mod go.sum产生的是普通构建层缓存。业务源码变化不会让go mod download失效;只有依赖文件变化才需要重新执行。type=cache给命令挂载可复用目录。tuke-go-mod保存下载的模块,tuke-go-build保存 Go 编译缓存;即使对应的RUN必须重新执行,也不必从零下载和编译。
两个 RUN 使用相同的 id=tuke-go-mod,因此看到的是同一份模块缓存。sharing=locked 会在并发构建写入同一缓存时加锁,避免多个 Go 进程同时修改目录。cache mount 属于 builder 的构建状态,不会因为挂载到了 RUN 中就被写进最终镜像。
apt 缓存与运行时镜像
runtime stage 基于 Node.js 镜像,需要安装 Chromium、字体和常用命令行工具,下载量远大于最终的 Go 二进制。Debian 基础镜像默认会清理 apt 缓存,所以先删除 docker-clean 配置,再把 package lists 和 deb 缓存分别挂载到命名 cache mount。它们主要用于加速同一个 builder 后续需要重新执行的 apt-get,不用于填充最终镜像层。
AGENT_BROWSER_VERSION 使用 ARG,只参与构建。需要升级时修改默认值,或在手工构建时传入:
--build-arg AGENT_BROWSER_VERSION=0.34.0
最终仅通过 COPY --from=build 取出 /out/tuke-agent,Go 编译器、源码和中间文件都留在 build stage,不会进入发布镜像。runtime stage 还复制内置 skills,设置服务、数据库、工作目录和 Chromium 环境变量,最后以 tuke-agent 作为入口。.dockerignore 排除了 .git、.env、数据库和本地 data,既缩小 build context,也避免把本地数据或密钥误送给 builder。
缓存失效范围大致如下:
| 发生变化的内容 | 需要重新执行的主要步骤 |
|---|---|
go.mod / go.sum |
下载 Go 依赖、编译服务及其后续层 |
cmd/ / internal/ |
编译服务及其后续层,依赖下载层仍可复用 |
AGENT_BROWSER_VERSION |
安装 agent-browser 及其后续层 |
apt 软件包列表或对应 RUN |
安装系统软件及其后续层 |
.agents/skills |
复制 skills;此前的大部分层仍可复用 |
这就是安排 Dockerfile 指令顺序的意义:越稳定、越昂贵的步骤尽量靠前,频繁变化的源码和 skills 靠后。
tuke 的完整 deploy.sh
#!/usr/bin/env bash
# 构建并推送 docker.io/arloor/app:tuke,然后滚动重启 k3s Deployment。
# 在 WSL debian 中执行;仓库需位于 WSL 原生文件系统(不要用 /mnt 路径)。
set -euo pipefail
cd "$(dirname "$0")"
IMAGE=docker.io/arloor/app:tuke
BUILDER=cache-builder
DEPLOYMENT=tuke-agent
# 必须用 docker-container driver 的 builder(默认 docker driver 不支持 registry 缓存导出);
# host 网络 + 继承系统 HTTP 代理环境变量。不存在则创建。
if ! docker buildx inspect "$BUILDER" >/dev/null 2>&1; then
docker buildx create \
--name "$BUILDER" \
--driver docker-container \
--driver-opt network=host \
--driver-opt "env.HTTP_PROXY=${HTTP_PROXY:-}" \
--driver-opt "env.HTTPS_PROXY=${HTTPS_PROXY:-${HTTP_PROXY:-}}" \
--driver-opt "\"env.NO_PROXY=${NO_PROXY:-localhost}\"" \
--buildkitd-flags '--allow-insecure-entitlement=network.host'
fi
docker buildx build . \
--builder "$BUILDER" \
--file Dockerfile \
--tag "$IMAGE" \
--network host \
--cache-from "type=registry,ref=${IMAGE}-buildcache" \
--cache-to "type=registry,ref=${IMAGE}-buildcache,mode=max" \
--push
kubectl rollout restart "deployment/$DEPLOYMENT"
kubectl rollout status "deployment/$DEPLOYMENT"
创建独立 builder
tuke 不直接使用 Docker 自带的默认 builder,而是创建名为 cache-builder 的独立 docker-container builder。这样可以使用独立的 BuildKit daemon,并稳定支持 registry cache 导入和导出。builder 的数据保存在 Docker volume 中,因此脚本运行结束后,本地构建状态仍然存在。
这里有两个容易混淆的网络设置:
--driver-opt network=host设置 BuildKit daemon 容器的网络。它拉取 Dockerfile frontend、基础镜像以及 registry cache 时使用这个网络。- 后面
docker buildx build --network host设置 Dockerfile 中RUN步骤的网络,例如go mod download、apt-get和npm install。
--allow-insecure-entitlement=network.host 只是允许构建请求使用 host network,并不代表关闭 HTTPS 或证书校验。
NO_PROXY 那一行的两层引号是有意为之。Bash 会去掉最外层引号,把里面的 " 作为字面量双引号传给 buildx;buildx 又会把每个 --driver-opt 按 CSV 解析,因此这对字面量引号可以保护 NO_PROXY=localhost,127.0.0.1,.example.com 中的逗号,使整串内容保持为一个 option。CSV 解析结束后,引号本身不会进入 BuildKit 容器。若只写成 --driver-opt "env.NO_PROXY=${NO_PROXY:-localhost}",默认值 localhost 没问题,但带逗号的常见 NO_PROXY 会被 buildx 拆成多个字段并报 expecting k=v。
代理变量在执行 docker buildx create 时展开并写入 builder 配置,并不是每次运行 deploy.sh 都动态刷新。脚本发现同名 builder 已存在就不会重新创建;如果宿主机代理地址变了,应先用 docker buildx inspect cache-builder 检查现有配置。
registry cache 与镜像推送
构建命令会在 Docker Hub 使用两个 tag:
| 地址 | 用途 |
|---|---|
docker.io/arloor/app:tuke |
生产镜像 |
docker.io/arloor/app:tuke-buildcache |
BuildKit registry cache |
--cache-from 在构建前拉取远程缓存,换机器或重建 builder 后仍有机会命中;--cache-to ...,mode=max 会把中间 stage 在内的构建缓存导出到 registry,而不只保存最终镜像相关缓存。第一次执行时 tuke-buildcache 还不存在,出现 not found 警告是正常的,成功推送一次后就有了。
registry cache 与 RUN --mount=type=cache 不应视为同一件事:前者让不同机器共享可复用的构建记录和层,后者给正在执行的包管理器提供持久目录。本机 builder 上的 cache mount 在某个步骤必须重跑时尤其有用;远程层缓存命中时则可以直接跳过整个步骤。
--push 会直接把结果推到 Docker Hub,而不是自动加载到本机 Docker image store。公开镜像拉取可以匿名获取短期 Bearer token;推送 arloor/app 前需执行一次:
docker login docker.io
Buildx 会使用 ~/.docker/config.json 或 credential store 中的登录凭据,自动向 auth.docker.io 换取 registry Bearer token,不需要在 deploy.sh 中保存或手工传 OAuth token。
镜像推送成功后,脚本才会用 kubectl rollout restart 滚动重启 tuke-agent Deployment,并用 kubectl rollout status 等待更新完成。脚本开头的 set -euo pipefail 保证构建、推送或滚动更新任一步失败都会立即退出。例如 Docker Hub 超时或推送失败时,不会继续重启仍引用旧镜像的 Deployment。
实际执行与排查
常用检查命令:
# builder 类型、网络、代理、BuildKit 版本和运行状态
docker buildx inspect cache-builder
# BuildKit 容器及其日志
docker ps --filter name=buildx_buildkit_cache-builder
docker logs buildx_buildkit_cache-builder0
# 本地 BuildKit 缓存占用
docker buildx du --builder cache-builder
# 展开构建日志,排查具体是哪一步没有命中缓存或网络失败
BUILDKIT_PROGRESS=plain ./deploy.sh
脚本只判断同名 builder 是否存在,不会校验它是不是 docker-container driver,也不会自动更新代理和网络配置。如果 builder 配置已经不符合预期,可以删除后让脚本重建:
docker buildx rm cache-builder
./deploy.sh
删除 builder 会同时丢失它的本地构建状态;已经推送到 docker.io/arloor/app:tuke-buildcache 的 registry cache 不受影响,下次构建仍可通过 --cache-from 拉取。
BuildKit 常见用法速查
上面的 tuke 已经实际使用了语法版本、cache mount 和 registry cache。下面把这些能力连同 secret mount、SSH mount 和多平台构建整理成可直接套用的最小示例。使用 docker buildx build 时天然启用 BuildKit;只有在仍使用旧式 docker build 且环境没有默认启用 BuildKit 时,才需要前缀 DOCKER_BUILDKIT=1。
指定 Dockerfile 语法版本
建议在 Dockerfile 第一行固定 frontend 版本,以便稳定使用 cache、secret 和 SSH mount 等语法:
# syntax=docker/dockerfile:1.7
这个指令不是普通注释。BuildKit 会解析并拉取对应的 docker/dockerfile frontend;它必须位于所有 Dockerfile 指令之前。
cache mount:缓存包管理器下载
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
cache mount 把指定目录保存在 builder 的缓存状态中。当这个 RUN 因依赖或源码变化而重新执行时,pip 可以复用已经下载的包。挂载目录中的内容不会因为这条指令自动写入镜像层,因此它适合 Maven、Gradle、npm、Go、apt、pip 等工具的下载缓存,不适合保存最终运行时必须存在的文件。
多个构建可能同时写同一缓存时,可以显式命名并加锁:
RUN --mount=type=cache,id=my-pip-cache,target=/root/.cache/pip,sharing=locked \
pip install -r requirements.txt
secret mount:临时使用密钥
先从客户端把本地文件作为 secret 交给 BuildKit:
docker buildx build \
--secret id=npmrc,src="$HOME/.npmrc" \
-t myapp:dev \
.
再在 Dockerfile 的单条 RUN 中挂载:
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
secret 只在这条 RUN 执行期间可见,不会被 COPY 进构建上下文,也不会作为 ARG、ENV 或普通文件留在镜像层。不要为了图省事改成 --build-arg TOKEN=...;构建参数和环境变量不适合承载密钥。
需要注意,secret 的内容变化不会自动使构建缓存失效。如果命令输出依赖 secret 的具体值,需要额外改变一个非敏感的 cache-bust ARG,或显式让对应步骤重新执行。
SSH mount:访问私有 Git 仓库
先确保宿主机的 SSH agent 已经加载所需私钥,然后把 agent socket 转发给构建:
eval "$(ssh-agent -s)"
ssh-add "$HOME/.ssh/id_ed25519"
docker buildx build \
--ssh default="$SSH_AUTH_SOCK" \
-t myapp:dev \
.
Dockerfile 中只有声明了 SSH mount 的 RUN 能使用该 agent:
# syntax=docker/dockerfile:1.7
RUN --mount=type=ssh \
git clone git@github.com:your/private-repo.git
SSH 私钥本身不会复制进镜像。实际使用时还应预先配置目标服务器的 known_hosts,不要用永久关闭 host key 校验来绕过验证。
导出和导入 registry cache
本地 builder 缓存只属于当前 BuildKit 实例。CI runner 经常是临时机器,此时可以把缓存作为独立对象推送到 OCI registry:
docker buildx build \
--cache-from type=registry,ref=registry.example.com/ns/myapp:buildcache \
--cache-to type=registry,ref=registry.example.com/ns/myapp:buildcache,mode=max \
--tag registry.example.com/ns/myapp:latest \
--push \
.
--cache-from 导入已有缓存;--cache-to 导出本次的新缓存。mode=max 会连中间 stage 的缓存一起导出,适合多阶段构建,但缓存体积也比默认的 mode=min 更大。缓存应使用单独的 ref,不要让 buildcache 和生产镜像使用同一个 tag。
多平台构建
支持目标架构的 builder 可以在一次构建中生成多平台镜像索引:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.example.com/ns/myapp:latest \
--push \
.
多平台构建需要 builder 具备对应的原生节点或 QEMU 模拟能力。--push 会把各平台镜像和镜像索引一起推到 registry,拉取时容器运行时会自动选择当前机器的架构。使用经典 Docker image store 时,不能把这种多平台结果整体 --load 到本机,因此发布场景通常直接使用 --push。