Skip to content

构建容器镜像完全实操:多语言应用 + 多架构(amd64/arm64)+ 无外网环境代理配置 ​

Docker 镜像构建的完整实操记录:先为 Java / Go / Node.js / Vue 四种典型应用写出各自的 Dockerfile(核心是多阶段构建),再用 docker buildx 一次构建 linux/amd64 + linux/arm64 双平台镜像并推送,最后附上无法直连 Docker Hub 时给 buildx 配 HTTP 代理的完整排坑过程。本文保留全部命令与演示输出,方便照着敲。


0. 准备 ​

  1. 安装 Docker。

  2. 克隆示例仓库到本地:

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/hello

1.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/hello

1.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 nodejs

1.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.1

2.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/archlinux/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 → Hubdocker CLI(宿主机进程)# syntax=docker/dockerfile:1 前端、oauth token、docker login宿主机 shell 环境变量
buildkitd 容器 → Hubdocker-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 --bootstrap

network=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 proxy

docker login 若直连不通,同样先 export 代理再登录。

3.5 步骤四:可选优化 —— 去掉前端拉取依赖 ​

Dockerfile 首行 # syntax=docker/dockerfile:1 会让 BuildKit 先去 Hub 拉前端镜像。删掉它改用内置前端,少一次外网依赖:

bash
sed -i '1{/^# syntax=docker\/dockerfile:1$/d}' Dockerfile

3.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 主命令 usagebuildx 插件缺失apt-get install docker-buildx,或下载官方二进制到 ~/.docker/cli-plugins/docker-buildx
Platforms 无 linux/arm64,构建报 exec format error缺 QEMU binfmtapt-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 无法直连 Hubexport 代理后再 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:7897

4.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. 经验总结 ​

  1. 多语言构建的心法:多阶段构建,把"依赖清单先拷进去装依赖"吃满缓存,运行阶段只留精简镜像。
  2. 多架构的关键两步:buildx builder + QEMU binfmt(arm64 缺一不可);Dockerfile 用 BUILDPLATFORM/TARGETOS/TARGETARCH 交叉编译。
  3. 无外网先分清通路再动手:CLI、buildkitd 容器、dockerd 三方各自拉镜像,各自配代理;builder 代理改完必须重建;shell 变量要小写大写都 export。
  4. 镜像站 vs 代理:局域网优先 HTTP 代理;daemon.json 的 mirror 覆盖不了 buildx 容器构建。
  5. 永久化三件套:export 只活一个终端 —— CLI 侧写 /etc/profile.d/proxy.sh,docker pull 写 daemon.json 的 proxies,buildx builder 的代理勿重建即永久。

用 VitePress + Meoo CLI 发布

Meoo By Meoo 秒悟