概述

在微服务架构中,前端应用通常需要代理多个后端服务的 API 请求。传统的做法是为每个环境(开发、测试、生产)构建不同的镜像,这会导致镜像管理复杂、部署效率低下。本文将介绍如何通过 Nginx 配置参数化,结合 Docker 环境变量传递,实现一个镜像适配多个环境的动态代理配置方案。

为什么需要 Nginx 配置参数化?

传统方式的痛点

  1. 多环境配置差异:不同环境的后端服务地址不同,需要为每个环境维护不同的配置文件
  2. 镜像管理复杂:每个环境都需要构建独立的镜像,镜像版本管理困难
  3. 部署效率低:环境变更需要重新构建镜像,无法快速响应
  4. 配置硬编码:后端服务地址写死在配置文件中,缺乏灵活性

参数化的优势

  • 一次构建,多处部署:同一个镜像可以在不同环境使用
  • 配置动态化:通过环境变量动态配置后端服务地址
  • 运维友好:无需重新构建镜像即可调整代理配置
  • 符合 DevOps 最佳实践:配置与代码分离

实现方案

方案架构

Docker Run (传递环境变量)
    ↓
Entrypoint Script (替换占位符)
    ↓
Nginx 配置文件 (使用环境变量值)
    ↓
Nginx 服务启动

核心思路

  1. 配置模板化:在 Nginx 配置文件中使用占位符(如 ${DATA_URL}
  2. 启动时替换:通过 entrypoint 脚本在容器启动时替换占位符
  3. 环境变量传递:通过 Docker 环境变量传递实际的后端服务地址

详细实现步骤

1. Nginx 配置文件模板

conf.d/default.conf 中使用环境变量占位符:

server {
    listen 80;
    server_name localhost;
    
    # 日志配置
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;

    # 缓存静态资源
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # 前端静态资源
    location = /standard_front {
        return 301 /standard_front/;
    }
    
    location ^~ /standard_front/ {
        alias /usr/share/nginx/html/;
        index index.html;
        include /etc/nginx/mime.types;
        default_type application/octet-stream;
    }
    
    # API 代理配置 - 使用环境变量占位符
    location /unify/data {
        proxy_pass http://${DATA_URL}:8096;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /database {
        proxy_pass http://${DATABASE_URL}:8097;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /approve {
        proxy_pass http://${APPROVE_URL}:8098;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /framework {
        proxy_pass http://${FRAMEWORK_URL}:8079;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /info {
        proxy_pass http://${INFO_URL}:8100;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /datafilling {
        proxy_pass http://${DATAFILLING_URL}:8092;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /api/cockpit {
        proxy_pass http://${COCKPIT_URL}:8096;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /warning {
        proxy_pass http://${WARNING_URL}:8093;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    location /fastdfs {
        proxy_pass http://${OBS_URL}/;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_buffer_size 64k;
        proxy_buffers   32 32k;
        proxy_busy_buffers_size 128k;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
    }
    
    # 错误页面
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

关键点

  • 使用 ${变量名} 作为占位符
  • 占位符会被 entrypoint 脚本替换为实际的环境变量值
  • 支持多个后端服务的代理配置

2. Entrypoint 启动脚本

创建 entrypoint.sh 脚本,在容器启动时替换配置中的占位符:

#!/bin/sh 
# 修复:仅保留ash支持的set选项(-e 报错立即退出,-u 未定义变量报错) 

# 设置错误处理选项
set -e
set -u

# 定义默认值(统一管理,便于维护) 
DEFAULT_URL="localhost"

# 环境变量赋值,使用默认值如果未定义
DATA_URL=${DATA_URL:-$DEFAULT_URL}
DATABASE_URL=${DATABASE_URL:-$DEFAULT_URL}
COCKPIT_URL=${COCKPIT_URL:-$DEFAULT_URL}
WARNING_URL=${WARNING_URL:-$DEFAULT_URL}
FRAMEWORK_URL=${FRAMEWORK_URL:-$DEFAULT_URL}
APPROVE_URL=${APPROVE_URL:-$DEFAULT_URL}
DATAFILLING_URL=${DATAFILLING_URL:-$DEFAULT_URL}
INFO_URL=${INFO_URL:-$DEFAULT_URL}
OBS_URL=${OBS_URL:-"mqc-obs-prd.obsv3.xm-gmgf-1.itg.com.cn"}

# 优化:合并多次sed调用为一次(减少文件IO,提升效率) 
# 用|作为分隔符,避免URL中的/冲突;转义$和{确保精准匹配 
sed -i \
  -e "s|\\\${DATA_URL}|${DATA_URL}|g" \
  -e "s|\\\${DATABASE_URL}|${DATABASE_URL}|g" \
  -e "s|\\\${COCKPIT_URL}|${COCKPIT_URL}|g" \
  -e "s|\\\${WARNING_URL}|${WARNING_URL}|g" \
  -e "s|\\\${FRAMEWORK_URL}|${FRAMEWORK_URL}|g" \
  -e "s|\\\${APPROVE_URL}|${APPROVE_URL}|g" \
  -e "s|\\\${DATAFILLING_URL}|${DATAFILLING_URL}|g" \
  -e "s|\\\${OBS_URL}|${OBS_URL}|g" \
  -e "s|\\\${INFO_URL}|${INFO_URL}|g" \
  /etc/nginx/conf.d/default.conf 

# 可选:校验Nginx配置语法(提前发现错误) 
if ! nginx -t; then 
  echo "Error: Nginx config syntax error!" 
  exit 1 
fi 

# 前台启动Nginx(确保容器不退出) 
exec nginx -g "daemon off;"

脚本说明

  1. 错误处理set -eset -u 确保脚本在出错时立即退出
  2. 默认值设置:使用 ${VAR:-default} 语法为环境变量设置默认值
  3. 批量替换:使用 sed -i 一次性替换所有占位符
    • 使用 | 作为分隔符,避免 URL 中的 / 冲突
    • 转义 ${ 确保精确匹配占位符
  4. 配置校验:使用 nginx -t 验证配置语法
  5. 前台启动:使用 exec nginx -g "daemon off;" 确保容器不退出

3. Dockerfile 配置

# ============================================
# Nginx 前端应用 Dockerfile
# 基于 nginx:latest,支持代理配置
# ============================================

FROM cr.kylinos.cn/basekylin/nginx-aarch64:1.16.1-v10sp1

LABEL maintainer="your-email@example.com"
LABEL description="Nginx Frontend Application with Configurable Proxy"

# 创建应用目录
RUN mkdir -p /usr/share/nginx/html
RUN mkdir -p /apply

# 复制前端应用文件(默认index.html)
COPY html/ /usr/share/nginx/html

# 复制 nginx 配置文件
COPY nginx.conf /etc/nginx/nginx.conf
COPY mime.types /etc/nginx/mime.types
COPY conf.d/ /etc/nginx/conf.d/

# 复制并设置 entrypoint 脚本权限
COPY entrypoint.sh /apply/entrypoint.sh
RUN chmod +x /apply/entrypoint.sh

# 暴露端口(HTTP和HTTPS)
EXPOSE 80 443

ENTRYPOINT ["sh","/apply/entrypoint.sh"]

关键点

  • 复制包含占位符的配置文件模板
  • 复制 entrypoint 脚本并设置执行权限
  • 使用 entrypoint 脚本作为容器入口点

Docker 运行方式

方式一:直接使用 -e 参数传递环境变量

docker run -d \
  --name frontend \
  -p 8011:80 \
  -e DATA_URL=10.2.3.1 \
  -e DATABASE_URL=192.168.173.8 \
  -e APPROVE_URL=192.168.173.9 \
  -e FRAMEWORK_URL=192.168.173.10 \
  -e INFO_URL=192.168.173.11 \
  -e DATAFILLING_URL=192.168.173.12 \
  -e COCKPIT_URL=10.2.3.1 \
  -e WARNING_URL=192.168.173.13 \
  -e OBS_URL=mqc-obs-prd.obsv3.xm-gmgf-1.itg.com.cn \
  your-image:tag

方式二:使用环境变量文件 --env-file

创建环境变量文件 prod.env

# prod.env
DATA_URL=10.2.3.1
DATABASE_URL=192.168.173.8
APPROVE_URL=192.168.173.9
FRAMEWORK_URL=192.168.173.10
INFO_URL=192.168.173.11
DATAFILLING_URL=192.168.173.12
COCKPIT_URL=10.2.3.1
WARNING_URL=192.168.173.13
OBS_URL=mqc-obs-prd.obsv3.xm-gmgf-1.itg.com.cn

使用环境变量文件启动:

docker run -d \
  --name frontend \
  -p 8011:80 \
  --env-file prod.env \
  your-image:tag

方式三:使用启动脚本

创建 start-docker.sh 脚本:

#!/bin/bash
# ============================================
# Nginx 前端 Docker 容器启动脚本
# ============================================

echo "======================================"
echo "  Docker 容器启动脚本"
echo "======================================"
echo ""

# 固定容器名称
CONTAINER_NAME="frontend"

# 应用端口
APP_PORT=${APP_PORT:-"8011"}

# 后端服务地址配置(可修改默认值)
DATA_URL=${DATA_URL:-"localhost"}
DATABASE_URL=${DATABASE_URL:-"localhost"}
APPROVE_URL=${APPROVE_URL:-"localhost"}
FRAMEWORK_URL=${FRAMEWORK_URL:-"localhost"}
INFO_URL=${INFO_URL:-"localhost"}
DATAFILLING_URL=${DATAFILLING_URL:-"localhost"}
COCKPIT_URL=${COCKPIT_URL:-"localhost"}
WARNING_URL=${WARNING_URL:-"localhost"}
OBS_URL=${OBS_URL:-"mqc-obs-prd.obsv3.xm-gmgf-1.itg.com.cn"}

echo ""
echo "======================================"
echo "  配置信息"
echo "======================================"
echo "  容器名: $CONTAINER_NAME"
echo "  应用端口: $APP_PORT"
echo "  数据服务: $DATA_URL:8096"
echo "  数据库服务: $DATABASE_URL:8097"
echo "  审批服务: $APPROVE_URL:8098"
echo "  框架服务: $FRAMEWORK_URL:8079"
echo "  信息服务: $INFO_URL:8100"
echo "  数据填充服务: $DATAFILLING_URL:8092"
echo "  驾驶舱服务: $COCKPIT_URL:8096"
echo "  预警服务: $WARNING_URL:8093"
echo "  对象存储: $OBS_URL"
echo ""

# 确认是否继续
read -p "是否启动容器? (y/n): " CONFIRM

if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
    echo "已取消操作"
    exit 0
fi

echo ""
echo "清理旧容器..."
docker stop $CONTAINER_NAME 2>/dev/null
docker rm $CONTAINER_NAME 2>/dev/null

echo "启动容器中..."
docker run -d \
  --name $CONTAINER_NAME \
  -p ${APP_PORT}:80 \
  -e DATA_URL=$DATA_URL \
  -e DATABASE_URL=$DATABASE_URL \
  -e APPROVE_URL=$APPROVE_URL \
  -e FRAMEWORK_URL=$FRAMEWORK_URL \
  -e INFO_URL=$INFO_URL \
  -e DATAFILLING_URL=$DATAFILLING_URL \
  -e COCKPIT_URL=$COCKPIT_URL \
  -e WARNING_URL=$WARNING_URL \
  -e OBS_URL=$OBS_URL \
  crpi-77ngqnae8dl9woor.cn-hangzhou.personal.cr.aliyuncs.com/xmgm_123/frontend:v1

if [ $? -eq 0 ]; then
    echo ""
    echo "======================================"
    echo "  容器启动成功!"
    echo "======================================"
    echo ""
    echo "访问地址: http://localhost:$APP_PORT"
    echo ""
    echo "常用命令:"
    echo "  查看日志: docker logs -f $CONTAINER_NAME"
    echo "  进入容器: docker exec -it $CONTAINER_NAME sh"
    echo "  停止容器: docker stop $CONTAINER_NAME"
    echo "  删除容器: docker rm $CONTAINER_NAME"
    echo "  查看配置: docker exec $CONTAINER_NAME cat /etc/nginx/conf.d/default.conf"
else
    echo ""
    echo "错误: 容器启动失败!"
    echo "请检查镜像名称是否正确"
fi

echo ""

使用脚本启动:

# 方式1:使用默认值
./start-docker.sh

# 方式2:通过环境变量覆盖默认值
DATA_URL=10.2.3.1 DATABASE_URL=192.168.173.8 ./start-docker.sh

配置参数说明

环境变量 说明 默认值 对应后端服务端口
DATA_URL 数据服务地址 localhost 8096
DATABASE_URL 数据库服务地址 localhost 8097
APPROVE_URL 审批服务地址 localhost 8098
FRAMEWORK_URL 框架服务地址 localhost 8079
INFO_URL 信息服务地址 localhost 8100
DATAFILLING_URL 数据填充服务地址 localhost 8092
COCKPIT_URL 驾驶舱服务地址 localhost 8096
WARNING_URL 预警服务地址 localhost 8093
OBS_URL 对象存储服务地址 mqc-obs-prd.obsv3.xm-gmgf-1.itg.com.cn -

最佳实践

1. 占位符命名规范

  • 使用大写字母和下划线DATA_URLDATABASE_URL
  • 使用有意义的名称:避免使用 URL1URL2 等无意义名称
  • 保持一致性:同一类型的配置使用统一的前缀或后缀

2. Entrypoint 脚本优化

使用数组批量处理(可选优化)
#!/bin/sh
set -e
set -u

# 定义默认值
DEFAULT_URL="localhost"

# 定义需要替换的变量数组
VARS="DATA_URL DATABASE_URL COCKPIT_URL WARNING_URL FRAMEWORK_URL APPROVE_URL DATAFILLING_URL INFO_URL OBS_URL"

# 设置默认值并替换
for VAR in $VARS; do
    case $VAR in
        OBS_URL)
            eval "${VAR}=\${${VAR}:-mqc-obs-prd.obsv3.xm-gmgf-1.itg.com.cn}"
            ;;
        *)
            eval "${VAR}=\${${VAR}:-$DEFAULT_URL}"
            ;;
    esac
    # 替换占位符
    sed -i "s|\\\${${VAR}}|$(eval echo \$$VAR)|g" /etc/nginx/conf.d/default.conf
done

# 校验配置
nginx -t || exit 1

# 启动 Nginx
exec nginx -g "daemon off;"

3. 安全性建议

  • 敏感信息管理:不要将包含敏感信息的 .env 文件提交到代码仓库
  • 文件权限.env 文件应设置适当的文件权限(如 chmod 600
  • 默认值安全:生产环境的默认值应该是安全的,避免使用测试环境的地址

4. 配置验证

在 entrypoint 脚本中添加配置验证:

# 验证必需的环境变量
if [ -z "$DATA_URL" ] || [ "$DATA_URL" = "localhost" ]; then
    echo "Warning: DATA_URL is not set or using default value"
fi

# 验证 Nginx 配置语法
if ! nginx -t; then
    echo "Error: Nginx config syntax error!"
    exit 1
fi

5. 日志和调试

在 entrypoint 脚本中添加调试信息:

# 显示替换后的配置(仅用于调试,生产环境可注释)
if [ "${DEBUG:-false}" = "true" ]; then
    echo "=== Nginx Configuration ==="
    cat /etc/nginx/conf.d/default.conf
    echo "==========================="
fi

验证配置

1. 查看容器环境变量

# 查看容器的所有环境变量
docker exec frontend env

# 查看特定环境变量
docker exec frontend env | grep DATA_URL

2. 查看替换后的配置文件

# 查看替换后的 Nginx 配置
docker exec frontend cat /etc/nginx/conf.d/default.conf

# 检查特定代理配置
docker exec frontend grep -A 5 "location /database" /etc/nginx/conf.d/default.conf

3. 测试代理配置

# 测试代理是否正常工作
curl http://localhost:8011/database/health

# 查看 Nginx 访问日志
docker exec frontend tail -f /var/log/nginx/access.log

# 查看 Nginx 错误日志
docker exec frontend tail -f /var/log/nginx/error.log

4. 验证配置语法

# 在容器内验证配置语法
docker exec frontend nginx -t

常见问题

Q1: 环境变量没有生效?

A: 检查以下几点:

  1. 环境变量名称是否正确(区分大小写)
  2. docker run 命令中是否正确使用 -e 参数
  3. entrypoint 脚本中的 sed 替换命令是否正确
  4. 查看容器日志:docker logs frontend

Q2: Nginx 配置语法错误?

A:

  1. 检查 entrypoint 脚本中的 sed 替换是否正确
  2. 确保占位符格式正确:${VAR_NAME}
  3. 使用 nginx -t 验证配置语法
  4. 查看错误日志:docker logs frontend

Q3: 代理请求失败?

A:

  1. 检查后端服务地址是否正确
  2. 检查后端服务是否可访问(网络连通性)
  3. 查看 Nginx 错误日志:docker exec frontend tail -f /var/log/nginx/error.log
  4. 检查代理配置中的端口是否正确

Q4: 如何为不同环境设置不同的配置?

A: 可以创建多个环境变量文件:

  • dev.env - 开发环境
  • test.env - 测试环境
  • prod.env - 生产环境

启动时指定对应的文件:

docker run -d --env-file prod.env your-image:tag

Q5: 如何在 Docker Compose 中使用?

A: 在 docker-compose.yml 中使用 environmentenv_file

version: '3.8'
services:
  frontend:
    image: your-image:tag
    ports:
      - "8011:80"
    environment:
      - DATA_URL=10.2.3.1
      - DATABASE_URL=192.168.173.8
      - APPROVE_URL=192.168.173.9
    # 或者使用 env_file
    env_file:
      - prod.env

Q6: 如何动态更新配置而不重启容器?

A: Nginx 配置参数化是在容器启动时进行的,如果需要更新配置:

  1. 停止并删除旧容器
  2. 使用新的环境变量启动新容器
  3. 或者使用 docker exec 进入容器手动修改配置并重载:nginx -s reload

方案对比

方案一:配置模板 + Entrypoint 替换(本文方案)

优点

  • ✅ 实现简单,易于理解
  • ✅ 支持所有 Nginx 配置项
  • ✅ 配置校验在启动时完成

缺点

  • ⚠️ 需要维护 entrypoint 脚本
  • ⚠️ 配置变更需要重启容器

方案二:使用 envsubst 工具

优点

  • ✅ 更标准的做法
  • ✅ 支持更复杂的模板语法

缺点

  • ⚠️ 需要安装 envsubst(通常包含在 gettext 包中)
  • ⚠️ 需要确保镜像中包含该工具

示例

#!/bin/sh
envsubst < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
nginx -g "daemon off;"

方案三:使用 Nginx Lua 模块

优点

  • ✅ 真正的动态配置
  • ✅ 无需重启容器

缺点

  • ⚠️ 需要安装 Nginx Lua 模块
  • ⚠️ 配置复杂度较高
  • ⚠️ 性能开销略大

总结

通过 Nginx 配置参数化结合 Docker 环境变量传递,我们可以实现:

  1. 配置与代码分离:配置不进入镜像,提高灵活性
  2. 一次构建,多处部署:同一个镜像可以在不同环境使用
  3. 运维友好:无需重新构建即可调整代理配置
  4. 符合最佳实践:遵循 12-Factor App 原则

这种方式特别适合微服务架构中的前端应用,能够显著提高部署效率和可维护性。

参考资料

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐