Appearance
构建容器镜像完全实操:多语言应用 + 多架构(amd64/arm64)+ 无外网环境代理配置
Docker 镜像构建的完整实操记录:先为 Java / Go / Node.js / Vue 四种典型应用写出各自的 Dockerfile(核心是多阶段构建),再用
docker buildx一次构建linux/amd64+linux/arm64双平台镜像并推送,最后附上无法直连 Docker Hub 时给 buildx 配 HTTP 代理的完整排坑过程。本文保留全部命令与演示输出,方便照着敲。
0. 准备
安装 Docker。
克隆示例仓库到本地:
bash
git clone https://github.com/lyzhang1999/gitops.git下文涉及的应用示例都在 gitops/docker/13/ 下,按语言分目录:
gitops/docker/13/
├── sprint-boot/ # Java / Spring Boot(Maven)
├── golang/ # Go(echo)
├── node/ # Node.js(Express)
├── vue/example/ # Vue 前端(Vite)
└── multi-arch/ # 多架构示例(Go + gin)1. 为不同语言的应用构建镜像
1.1 Java(Spring Boot + Maven)
示例应用:gitops/docker/13/sprint-boot。关注三个文件:src/、Dockerfile、.dockerignore。DemoApplication.java 是一个典型的 Spring Boot + Maven 应用,提供 /hello 接口。
方式一:启动 Jar 包(多阶段构建)
dockerfile
# syntax=docker/dockerfile:1
FROM eclipse-temurin:17-jdk-jammy as builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install
FROM eclipse-temurin:17-jre-jammy
WORKDIR /opt/app
EXPOSE 8080
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
CMD ["java", "-jar", "/opt/app/*.jar"]两个 FROM 就是多阶段构建:第一阶段(builder)用带 JDK 的镜像编译出 Jar;第二阶段用精简的 JRE 镜像运行。第一阶段的所有产物 *.jar 通过 COPY --from=builder 复制到运行阶段 —— 最终镜像只带运行时,不带编译工具链,体积小得多。
bash
docker build -t spring-boot .
docker run --publish 8080:8080 spring-boot
curl localhost:8080/hello方式二:使用 Spring Boot 插件直接运行(适合开发)
dockerfile
# syntax=docker/dockerfile:1
FROM eclipse-temurin:17-jdk-jammy
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:resolve
COPY src ./src
CMD ["./mvnw", "spring-boot:run"]mvnw dependency:resolve 负责安装依赖,mvnw spring-boot:run 启动应用。用 -f 指定不同的 Dockerfile 文件名:
bash
docker build -t spring-boot . -f Dockerfile-Boot
docker run --publish 8080:8080 spring-boot
curl localhost:8080/hello1.2 Go(编译成静态二进制)
Go 应用编译后是单个可执行文件,镜像里只放二进制即可。示例:gitops/docker/13/golang,main.go 用 echo 框架提供 /hello,返回 "Hello World Golang"。
dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.17 as builder
WORKDIR /opt/app
COPY go.* ./
RUN go mod download
COPY . .
ARG LD_FLAGS='-linkmode external -w -extldflags "-static"'
RUN go build -ldflags "$LD_FLAGS" -o example
FROM alpine as runner
WORKDIR /opt/app
COPY --from=builder /opt/app/example /opt/app/example
EXPOSE 8080
CMD ["/opt/app/example"]几个点:
COPY go.* ./只先拷go.mod/go.sum,再go mod download—— 文件不变时可命中缓存,不必每次重下依赖(和前面的dependency:go-offline同理)。go build加静态链接参数,产出不依赖 glibc 的二进制;- 运行阶段用极小的
alpine,从 builder 阶段把example拷过来即可。
bash
docker build -t golang .
docker run --publish 8080:8080 golang
curl localhost:8080/hello1.3 Node.js(Express)
示例:gitops/docker/13/node,app.js 用 Express 提供 /hello。.dockerignore 忽略 node_modules,避免把本机依赖打进构建上下文。
dockerfile
# syntax=docker/dockerfile:1
FROM node:16.17.0-bullseye AS build
RUN sed -i "s@http://\(deb\|security\).debian.org@https://mirrors.aliyun.com@g" /etc/apt/sources.list
RUN apt-get update && apt-get install -y dumb-init
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
FROM node:16.17.0-bullseye-slim
ENV NODE_ENV production
COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
USER node
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app/node_modules /usr/src/app/node_modules
COPY --chown=node:node . /usr/src/app
CMD ["dumb-init", "node", "app.js"]可借鉴的实践:
dumb-init作 PID 1:正确处理容器里的信号转发与僵尸进程回收;USER node+--chown=node:node:以非 root 运行,降低风险;- 只拷
package*.json→npm ci(锁定版本)→ 再拷源码,最大化利用层缓存。
bash
docker build -t nodejs .
docker run --publish 3000:3000 nodejs1.4 Vue 前端(静态站点两种托管方式)
前端构建完是纯静态文件(dist/),关键是"用什么服务器托管"。示例:gitops/docker/13/vue/example。
方式一:http-server
dockerfile
# syntax=docker/dockerfile:1
FROM node:lts-alpine
RUN npm install -g http-server
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 8080
CMD [ "http-server", "dist" ]bash
docker build -t vue .
docker run --publish 8080:8080 vue方式二:Nginx(生产更常见)
dockerfile
# syntax=docker/dockerfile:1
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]第一阶段和上面几乎一样:装依赖 → npm run build 产出 dist/;第二阶段把 dist 复制进 Nginx 的默认网页目录 /usr/share/nginx/html。注意容器内端口从 8080 换成了 80,所以运行时要做映射:
bash
docker build -t vue-nginx -f Dockerfile-Nginx .
docker run --publish 8080:80 vue-nginx小结:上面四种语言,套路高度一致 —— 多阶段构建 + 先把依赖清单拷进去装依赖(吃缓存)+ 只把运行产物拷进精简运行镜像。这就是"构建容器镜像"的核心心法。
2. 构建多平台(多架构)镜像:buildx
2.1 为什么需要 buildx
docker build 默认只构建本机平台(如 linux/amd64)的镜像。想把镜像"一次构建、到处运行",覆盖 linux/arm64 等其它架构,就要用 docker buildx(Docker 的 CLI 插件)。
bash
# 安装插件(Ubuntu/Debian;若 docker 官方源没配,可下载二进制到 ~/.docker/cli-plugins/)
apt-get install -y docker-buildx
docker buildx version # 例如 buildx 0.30.12.2 创建并初始化 builder
bash
docker buildx create --name builder # 创建构建器
docker buildx use builder # 设为默认
docker buildx inspect --bootstrap # 初始化(启动 BuildKit 容器)初始化后能看到 builder 信息。注意 Platforms 一行:
Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386⚠️ 没有 linux/arm64!宿主是 amd64 机器,而 docker-container 驱动做跨架构构建时,需要用 QEMU(binfmt) 模拟其它 CPU 架构,否则构建 arm64 时一执行 RUN 就报 exec format error。
2.3 注册 QEMU 模拟器(arm64 关键)
二选一:
bash
# 方式 A:官方 binfmt 镜像(需要能拉 docker.io 镜像)
docker run --privileged --rm tonistiigi/binfmt --install all
# 方式 B:系统包(不依赖外网拉镜像,推荐受限网络环境)
apt-get install -y qemu-user-static binfmt-support再次检查,Platforms 应出现 arm64:
bash
docker buildx inspect --bootstrap | grep arm64
# Platforms: linux/amd64, ..., linux/arm64, linux/riscv64, linux/ppc64, ...2.4 multi-arch 的 Dockerfile:内置变量 + 交叉编译
示例:gitops/docker/13/multi-arch。main.go 是一个 Go + gin 应用,访问 / 会返回 runtime 包的 os / arch / version —— 部署后访问它,就能直观看到当前跑在哪个平台上。
多平台构建时,Dockerfile 里可用几个内置变量:
| 变量 | 含义 | 示例值 |
|---|---|---|
BUILDPLATFORM | 构建平台的 os/arch | linux/amd64 |
TARGETOS | 目标系统 | linux |
TARGETARCH | 目标 CPU 架构 | amd64 / arm64 |
示例应用的 Dockerfile:
dockerfile
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.18 as build
ARG TARGETOS TARGETARCH
WORKDIR /opt/app
COPY go.* ./
RUN go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /opt/app/example .
FROM ubuntu:latest
WORKDIR /opt/app
COPY --from=build /opt/app/example ./example
CMD ["/opt/app/example"]逐行解读:
- 第 2 行
FROM --platform=$BUILDPLATFORM golang:1.18 as build:强制用构建平台的基础镜像做编译阶段(否则默认拉本机架构镜像,在其它架构上可能拉不到或跑不了)。 - 第 3 行
ARG TARGETOS TARGETARCH:声明两个内置变量,构建时 Docker 自动填充。 - 第 5~6 行
COPY go.* ./+go mod download:先装依赖、吃缓存。 - 第 8~9 行:
--mount=type=cache,target=/root/.cache/go-build挂载 Go 构建缓存加速;GOOS=$TARGETOS GOARCH=$TARGETARCH go build做交叉编译,为每个目标平台产出对应二进制。 - 第二阶段把编译产物拷进精简运行镜像。
2.5 一次构建、双平台、直接推送
bash
docker buildx build --platform linux/amd64,linux/arm64 \
-t robotzsj/multi-arch:latest --push .--platform指定两个目标平台;-t指定镜像名与标签;--push构建完成后直接推到 Docker Hub(需要先docker login);- 末尾
.是构建上下文路径,不能省略。
docker-container驱动的构建结果默认只留在缓存里,不带--push/--load是看不到镜像的,构建完成会提示WARNING: No output specified...。
推送成功后,到 Docker Hub 镜像详情页能看到该镜像同时兼容 linux/amd64 和 linux/arm64。
3. 无外网环境实战排坑:给 buildx 配 HTTP 代理
构建双架构镜像时最常卡住的一步不是 Dockerfile,而是拉不到镜像。下面是在"无法直连 Docker Hub"的机器上(只能通过局域网代理 http://192.168.0.140:7897 访问外网)的完整排坑记录。
3.1 先搞懂:buildx 有三条"网络通路"
拉镜像的动作发生在不同进程里,要给对地方配代理:
| 通路 | 由谁发起 | 拉取的内容 | 在哪配代理 |
|---|---|---|---|
| CLI → Hub | docker CLI(宿主机进程) | # syntax=docker/dockerfile:1 前端、oauth token、docker login | 宿主机 shell 环境变量 |
| buildkitd 容器 → Hub | docker-container 构建容器 | FROM 基础镜像、构建中的拉取 | 创建 builder 时 --driver-opt env.*_proxy |
| dockerd → Hub | 守护进程 | 普通 docker pull / build | /etc/docker/daemon.json 的 registry-mirrors / proxies |
最容易踩的坑:
docker-container驱动把构建放进独立 BuildKit 容器,daemon.json的 registry-mirror 对它不生效;- 反过来,Dockerfile 首行
# syntax=docker/dockerfile:1要拉"前端"镜像、以及 oauth token 的获取,发生在 CLI 侧,BuildKit 容器里的代理对它也无效。
所以典型症状是:容器里配了代理,构建还是 30 秒超时 —— 因为卡住的那一步其实走的是宿主机 shell。两侧要分开配。
3.2 步骤一:验证代理可用
bash
curl -x http://192.168.0.140:7897 -sI https://registry-1.docker.io/v2/ | head -3
# HTTP/1.1 200 Connection established + HTTP/2 401 => 代理通(401 只是未认证,正常)3.3 步骤二:重建带代理的 builder(管 buildkitd 容器)
bash
docker buildx rm builder
docker buildx create --name builder --driver docker-container \
--driver-opt network=host \
--driver-opt "env.http_proxy=http://192.168.0.140:7897" \
--driver-opt "env.https_proxy=http://192.168.0.140:7897" \
--driver-opt "env.no_proxy=localhost,127.0.0.1" \
--use
docker buildx inspect --bootstrapnetwork=host 让 BuildKit 容器直接用宿主机网络,避免访问不到局域网代理。改代理后必须 rm + create 重建,光改不重建不生效。
验证代理真的进了容器(而不是想当然):
bash
docker inspect $(docker ps -q --filter name=buildx_buildkit) \
--format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i proxy
# 能看到 http_proxy / https_proxy 才算配进去3.4 步骤三:宿主机 shell 也要挂代理(管 CLI 侧)
bash
export http_proxy=http://192.168.0.140:7897 https_proxy=http://192.168.0.140:7897
export HTTP_PROXY=http://192.168.0.140:7897 HTTPS_PROXY=http://192.168.0.140:7897
env | grep -i proxydocker login 若直连不通,同样先 export 代理再登录。
3.5 步骤四:可选优化 —— 去掉前端拉取依赖
Dockerfile 首行 # syntax=docker/dockerfile:1 会让 BuildKit 先去 Hub 拉前端镜像。删掉它改用内置前端,少一次外网依赖:
bash
sed -i '1{/^# syntax=docker\/dockerfile:1$/d}' Dockerfile3.6 完整命令链
bash
# ① 代理可达性验证
curl -x http://192.168.0.140:7897 -sI https://registry-1.docker.io/v2/ | head -3
# ② 重建带代理的 builder(rm + create)
docker buildx rm builder
docker buildx create --name builder --driver docker-container \
--driver-opt network=host \
--driver-opt "env.http_proxy=http://192.168.0.140:7897" \
--driver-opt "env.https_proxy=http://192.168.0.140:7897" \
--driver-opt "env.no_proxy=localhost,127.0.0.1" \
--use
docker buildx inspect --bootstrap # Platforms 需含 linux/arm64(缺则先装 qemu-user-static)
# ③ 宿主机 shell 挂代理(CLI 侧)
export http_proxy=http://192.168.0.140:7897 https_proxy=http://192.168.0.140:7897
export HTTP_PROXY=http://192.168.0.140:7897 HTTPS_PROXY=http://192.168.0.140:7897
# ④ 本地验证构建
docker buildx build --platform linux/amd64,linux/arm64 -t robotzsj/multi-arch:test .
# ⑤ 登录并正式推送
docker login
docker buildx build --platform linux/amd64,linux/arm64 \
-t robotzsj/multi-arch:latest --push .3.7 排查速查表
| 报错现象 | 原因 | 处理 |
|---|---|---|
unknown flag: --name / 显示 docker 主命令 usage | buildx 插件缺失 | apt-get install docker-buildx,或下载官方二进制到 ~/.docker/cli-plugins/docker-buildx |
Platforms 无 linux/arm64,构建报 exec format error | 缺 QEMU binfmt | apt-get install -y qemu-user-static binfmt-support |
拉镜像 EOF / connection reset by peer | 直连 Hub 被断 | 按 3.3 / 3.4 配代理(镜像站只对 dockerd 有效) |
failed to resolve ... docker/dockerfile:1 + 30s 超时 | 前端拉取走 CLI 侧,未配 shell 代理 | shell export 代理后重建 |
错误信息里出现 Docker 自己的 IP/IPv6(2a03:2880:...) | 请求仍在直连,代理没配上 | rm + create 重建 builder,并用 3.3 的 inspect 命令核实 |
WARNING: No output specified with docker-container driver | 构建结果只在缓存 | 加 --push 或 --load |
docker login 失败 | CLI 无法直连 Hub | export 代理后再 login |
鉴别技巧:报错里能看到代理 IP = 走了代理;看到 Docker 自己的 IP(registry-1.docker.io / IPv6 2a03:2880...)= 还在直连,据此判断是哪一侧没配上。
4. 让代理配置永久生效(收尾)
前面 export 的代理只对当前终端有效,一关终端就没了。要让机器重启、新开终端都自动带上代理,分三层固化:
4.1 环境变量写入 /etc/profile.d(管 CLI / curl / docker login)
bash
cat > /etc/profile.d/proxy.sh <<'EOF'
export http_proxy=http://192.168.0.140:7897
export https_proxy=http://192.168.0.140:7897
export HTTP_PROXY=http://192.168.0.140:7897
export HTTPS_PROXY=http://192.168.0.140:7897
export no_proxy=localhost,127.0.0.1,192.168.0.0/16
export NO_PROXY=localhost,127.0.0.1,192.168.0.0/16
EOF
chmod +x /etc/profile.d/proxy.sh
source /etc/profile.d/proxy.sh # 当前会话立即生效新开的终端登录时会自动加载,docker login、CLI 侧拉取都不需要再手动 export。192.168.0.0/16 放进 no_proxy,内网请求不走代理。
4.2 dockerd 守护进程代理(管 docker pull)
docker pull 由守护进程发起,不读 shell 变量,要写进 /etc/docker/daemon.json 的 proxies。注意:如果文件里已有 registry-mirrors 等内容,要合并,别整文件覆盖:
json
{
"registry-mirrors": ["https://docker.1ms.run", "https://docker.m.daocloud.io"],
"proxies": {
"http-proxy": "http://192.168.0.140:7897",
"https-proxy": "http://192.168.0.140:7897",
"no-proxy": "localhost,127.0.0.1"
}
}bash
systemctl restart docker
docker info | grep -i -A2 proxy # 应显示 HTTP Proxy: http://192.168.0.140:78974.3 buildx builder:本就永久,勿删重建
builder 创建时用 --driver-opt env.*_proxy 注入的代理存在 builder 定义里,重启机器、重开终端都还在。唯一的坑:重建(rm + create)builder 会丢掉代理,需要重新注入。
4.4 最终验证(新开一个终端)
bash
env | grep -i proxy # 变量已自动带上
docker pull alpine:latest # 走 daemon 代理,应能成功
docker buildx build --platform linux/amd64,linux/arm64 \
-t robotzsj/multi-arch:test . # 双平台构建复验撤销:删掉 /etc/profile.d/proxy.sh,并从 daemon.json 去掉 proxies 后 systemctl restart docker 即可。
5. 经验总结
- 多语言构建的心法:多阶段构建,把"依赖清单先拷进去装依赖"吃满缓存,运行阶段只留精简镜像。
- 多架构的关键两步:buildx builder + QEMU binfmt(arm64 缺一不可);Dockerfile 用
BUILDPLATFORM/TARGETOS/TARGETARCH交叉编译。 - 无外网先分清通路再动手:CLI、buildkitd 容器、dockerd 三方各自拉镜像,各自配代理;builder 代理改完必须重建;shell 变量要小写大写都 export。
- 镜像站 vs 代理:局域网优先 HTTP 代理;
daemon.json的 mirror 覆盖不了 buildx 容器构建。 - 永久化三件套:
export只活一个终端 —— CLI 侧写/etc/profile.d/proxy.sh,docker pull写daemon.json的proxies,buildx builder 的代理勿重建即永久。