全栈工程师:运维篇(一):docker 终极理解

3 阅读4分钟

搞容器我们必须要关注两个点:变量的优先级、两个阶段。

必懂基础1:Docker 双阶段环境变量逻辑

要解决传递问题,首先要明确两个阶段的核心区别,这是后续配置的基础:

1. 构建阶段(Build Time)

  • 触发指令:docker build 或 docker compose build
  • 关联 Dockerfile 指令:ARG(用于接收构建时参数)、ENV(将构建时参数固化到镜像)
  • 变量来源:只能通过 --build-arg 参数传递,或在 docker-compose.yml 的 build.args 部分指定(.env 文件需通过 args 引用才能生效)
  • 核心特点:变量会被“烘焙”进镜像层(ARG 仅在构建期间有效,构建结束后消失;ENV 会永久保留在镜像中,可以被后续替换。
  • 为什么运行时能从 Compose 的 environment 拿变量,还要在 Dockerfile 写 ENV? 完全不是多余,而是应对两种高频需求: 1. 构建和运行都需要同一个变量,避免重复配置。比如你构建镜像时,需要用 VERSION=1.0 下载对应版本的依赖(这一步必须用 ARG 接收变量),而容器运行时,应用也需要知道自己的版本号(比如接口返回版本信息)。这时用 ENV APP_VERSION=$VERSION,就能把构建时的版本号“存”到镜像里,运行时应用直接读取,不用再在 Compose 的 environment 再写一次 APP_VERSION=1.0,减少重复工作。 2. 给镜像设“兜底默认值”,避免应用启动失败。比如你做一个通用镜像,不确定用户运行时会不会配置 environment 变量。这时在 Dockerfile 写 ENV PORT=8080,如果用户运行时没配置端口,应用就会用8080启动,不会因为没拿到端口变量而崩溃;如果用户配置了 environment: - PORT=9090,也能直接覆盖这个默认值,不影响自定义配置。 简单说:environment 是“运行时临时给的变量”,ENV 是“构建时存到镜像里的变量”,前者灵活、不写进镜像,后者能复用、能兜底,两者不冲突,按需使用就好。

2. 运行阶段(Runtime)

  • 触发指令:docker run 或 docker compose up
  • 关联配置:docker-compose.yml 的 environment 字段、.env 文件(默认仅作用于运行时)
  • 变量来源:容器启动时注入,无需提前写入镜像
  • 核心特点:变量仅存在于运行中的容器内存,不改变镜像本身,适合传递敏感信息

必懂基础2:Docker Compose 环境变量优先级

🚀 第一阶段:Docker Compose 解析优先级

优先级从高到低

  1. 🥇 命令行显式设置 (最高)

    1. 在执行命令前临时设置:MY_VAR=value docker compose up
    2. 使用 --env-file 标志:docker compose --env-file .env.prod up
    3. 注意:这会覆盖所有文件中的定义。
  2. 🥈 当前 Shell 环境变量

    1. 你在终端中 export MY_VAR=value 过的变量。
    2. 或者在当前终端会话中直接设置的变量。
  3. 🥉 env_file 指定的文件

    1. 在 docker-compose.yml 的 services.<name>.env_file 中列出的文件。
    2. 重要:如果有多个文件,列表后面的覆盖前面的
    3. 这是你之前问题的核心:build.args 能读到这里的值,因为此时 Compose 已经加载了这些文件。
  4. 🏅 默认 .env 文件

    1. 位于 docker-compose.yml 同级目录下的 .env 文件。
    2. 这是 Compose 默认自动加载的,无需配置。
  5. 📉 Compose 文件中的默认值语法

    1. 使用 ${VAR:-default_value} 语法。如果以上都没找到,则使用 default_value
  6. ❌ 空值

    1. 如果以上都没有,变量为空字符串。

💡 关键点build.args 获取变量的值可以来自env_file和environment

🐳 第二阶段:容器运行时环境变量优先级

优先级从高到低

  1. 🥇 docker run 或 docker compose run 的 -e 参数 (最高)

    1. 命令行强制覆盖:docker run -e MY_VAR=override ...
    2. 这在调试时非常有用,可以临时覆盖所有配置。
  2. 🥈 docker-compose.yml 中的 environment 字段 env_file: `` - .env (其中 MY_VAR=file_value) `` environment: ``- MY_VAR=compose_value # 最终容器里是 compose_value

    1. 在 YAML 中显式定义的 environment: - MY_VAR=value
    2. 关键规则:这里的定义会覆盖 env_file 中同名的变量。
    3. 例如:
  3. 🥉 env_file 指定的文件

    1. 通过 env_file 加载进容器的变量。
    2. 如果变量名没有在 environment 中重新定义,则保留文件中的值。
  4. 🏅 Dockerfile 中的 ENV 指令

    1. 在镜像构建时通过 ENV MY_VAR=image_default 设置的默认值。
    2. 如果运行时没有通过上述方式覆盖,则使用此值。
  5. ❌ 无定义

    1. 如果以上都没有,该变量在容器内不存在。

实践解释

我有一个monorepo,直接拿他来解释

项目结构

├── apps/
│   ├── b2b-api/          # Elysia 后端 API
│   ├── b2badmin/         # 后台管理系统 (Next.js)
│   └───web/
        ├──.env.1panel
|       └──Dockerfile         # 客户端前台 (Next.js)
|__ docker-compose-{环境}.yml   
#Dockerfile
  # ── Stage 1: Prune(修剪) monorepo ──────────────────────────────────────────
  
FROM oven/bun:1.3.10 AS pruner
WORKDIR /app

# Copy lockfile + package.jsons first, install turbo
COPY package.json bun.lock turbo.json  ./
# 详细模式安装可以看到安装细节
RUN bun install --verbose

# Copy everything else, then prune
COPY . .
# turbo 提供的精简命令,可以把monorepo其中一个应用打包成docker
RUN bun run turbo prune web --docker 

# ── Stage 2: Install & Build ─────────────────────────────────────────
FROM oven/bun:1.3.10 AS builder
WORKDIR /app

# Copy pruned lockfile + package.jsons (cache layer)
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/bun.lock ./bun.lock
#按照bun.lock安装和启动 详细模式
RUN bun install --frozen-lockfile --verbose 

# Then copy full source
COPY --from=pruner /app/out/full/ .

# Build Next.js (standalone mode) Nextjs的环境变量NEXT_PUBLIC变量需要使用ARG 这是构建时必须确定的,NEXT_PUBLIC_* 需要
ARG NEXT_PUBLIC_API_URL

ENV NODE_ENV=production
#运行时通过 environment 或 env_file 覆盖
ENV DATABASE_URL=$DATABASE_URL
ENV DOMAIN=$DOMAIN
RUN cd apps/web && bun run build

# ── Stage 3: Production ─────────────────────────────────────────────
FROM oven/bun:1.3.10-slim AS runner
WORKDIR /app 

ENV NODE_ENV=production
ENV HOSTNAME="0.0.0.0"
ENV PORT=9001

# Copy Next.js standalone output + static assets
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static

EXPOSE 9001

CMD ["bun", "apps/web/server.js"]
#docker-compose-{环境}.yml

# 1生产环境 Docker Compose 配置
# 用于本地构建 Web 前端镜像

services:
  # Web Frontend
  web:
    image: XXXX/tradeflow-web:latest
    container_name: tradeflow_web
    env_file:
      - ./apps/web/.env.1panel
    build:
      context: .
      dockerfile: apps/web/Dockerfile
      args:
        - NODE_ENV=production
        - NEXT_PUBLIC_API_URL
    restart: always
    # 暴露端口用于测试
    ports:
      - "7011:7011"
    networks:
      - tradeflow_network
      #1panel 平台为了互相通信
      - 1panel_network 
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:7011 || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

networks:
  tradeflow_network:
    external: true
  1panel_network:
    external: true
    name: 1panel-network

易错点:

1.我的DockerFile文件这样子写,然后我在这个compose里面传入了新值,是改变不了已经构建产物已经写入的NEXT_PUBLIC_API_URL的。因为 NEXT_PUBLIC_* 是 Next.js 构建时注入的变量,一旦构建完成,产物中已固化该值,运行时无法通过 environment 覆盖

ARG NEXT_PUBLIC_API_URL 
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

总结

  • ARG 用于构建时,来源可以是docker-compose 文件的build.arg声明,来自env-file,比如next的前端变量就要使用arg ,因为这个是写死在构建文件里面的。
  • docker-compose文件中environment的优先级大于env_file。 arg 用于构建时,env可覆盖,配合arg是展示用。