搞容器我们必须要关注两个点:变量的优先级、两个阶段。
必懂基础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 解析优先级
优先级从高到低:
-
🥇 命令行显式设置 (最高)
- 在执行命令前临时设置:
MY_VAR=value docker compose up - 使用
--env-file标志:docker compose --env-file .env.prod up - 注意:这会覆盖所有文件中的定义。
- 在执行命令前临时设置:
-
🥈 当前 Shell 环境变量
- 你在终端中
export MY_VAR=value过的变量。 - 或者在当前终端会话中直接设置的变量。
- 你在终端中
-
🥉
env_file指定的文件- 在
docker-compose.yml的services.<name>.env_file中列出的文件。 - 重要:如果有多个文件,列表后面的覆盖前面的。
- 这是你之前问题的核心:
build.args能读到这里的值,因为此时 Compose 已经加载了这些文件。
- 在
-
🏅 默认
.env文件- 位于
docker-compose.yml同级目录下的.env文件。 - 这是 Compose 默认自动加载的,无需配置。
- 位于
-
📉 Compose 文件中的默认值语法
- 使用
${VAR:-default_value}语法。如果以上都没找到,则使用default_value。
- 使用
-
❌ 空值
- 如果以上都没有,变量为空字符串。
💡 关键点:build.args 获取变量的值可以来自env_file和environment
🐳 第二阶段:容器运行时环境变量优先级
优先级从高到低:
-
🥇
docker run或docker compose run的-e参数 (最高)- 命令行强制覆盖:
docker run -e MY_VAR=override ... - 这在调试时非常有用,可以临时覆盖所有配置。
- 命令行强制覆盖:
-
🥈
docker-compose.yml中的environment字段env_file: `` - .env (其中 MY_VAR=file_value) `` environment: ``- MY_VAR=compose_value # 最终容器里是 compose_value- 在 YAML 中显式定义的
environment: - MY_VAR=value。 - 关键规则:这里的定义会覆盖
env_file中同名的变量。 - 例如:
- 在 YAML 中显式定义的
-
🥉
env_file指定的文件- 通过
env_file加载进容器的变量。 - 如果变量名没有在
environment中重新定义,则保留文件中的值。
- 通过
-
🏅 Dockerfile 中的
ENV指令- 在镜像构建时通过
ENV MY_VAR=image_default设置的默认值。 - 如果运行时没有通过上述方式覆盖,则使用此值。
- 在镜像构建时通过
-
❌ 无定义
- 如果以上都没有,该变量在容器内不存在。
实践解释
我有一个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是展示用。