描述在 Docker 容器内传递环境变量给 grunt 的最佳实践

解读

面试官想知道你是否理解“容器一次构建、到处运行”的原则,以及 Grunt 这类“配置即代码”工具在容器化场景下的安全、可维护、可扩展做法。国内大厂 CI/CD 流水线普遍强制「镜像不可变 + 配置外部化」,答“写死在 Gruntfile” 或 “npm script 硬编码” 会被直接判负。核心考点:

  1. 如何把宿主机/编排平台变量无损注入容器进程
  2. 如何让 Grunt 任务动态感知这些变量,而不需要重新 build 镜像
  3. 如何兼顾多环境(dev/test/stage/prod)与密钥安全(ak/sk、内网域名)

知识点

  • Docker 环境变量优先级:RUN < ENV < docker run -e < docker-compose environment < K8s env / ConfigMap / Secret
  • Grunt 运行时变量读取:grunt.option、process.env、--gruntfile 参数、.env 文件、grunt-template 渲染
  • 12-Factor App 配置原则:配置与代码严格分离
  • 国内镜像合规:阿里云 ACR、腾讯云 TCR 要求镜像内不含明文密钥
  • 多架构构建:Node 官方镜像已支持 linux/amd64 & linux/arm64,Grunt 插件需做 cpu 架构判断
  • 缓存优化:package-lock.json 单独 COPY + npm ci --only=production 降低层缓存失效概率
  • 健康检查:grunt-contrib-watch 属于长驻进程,需配 HEALTHCHECK 指令避免 K8s 误判 Pod 死锁

答案

  1. 镜像构建阶段绝不写死变量,仅声明默认值:

    FROM node:18-alpine
    WORKDIR /app
    COPY package* ./
    RUN npm ci --omit=dev
    COPY . .
    # 仅声明默认占位,方便本地调试
    ENV NODE_ENV=dev API_PREFIX=http://localhost:3000
    ENTRYPOINT ["npx", "grunt", "--gruntfile", "Gruntfile.js"]
    
  2. 运行阶段通过 -e 或编排文件注入真实值:

    docker run --rm \
      -e NODE_ENV=production \
      -e API_PREFIX=https://api.xxx.com \
      -e CDN_DOMAIN=cdn.xxx.com \
      myapp:latest
    
  3. Gruntfile 内使用 process.env 动态取值,并给出缺省回退,保证本地裸跑不报错:

    module.exports = function(grunt) {
      const env = process.env.NODE_ENV || 'dev';
      const apiPrefix = process.env.API_PREFIX || 'http://localhost:3000';
      const cdnDomain = process.env.CDN_DOMAIN || '';
    
      grunt.initConfig({
        replace: {
          dist: {
            options: {
              patterns: [{
                match: /__API_PREFIX__/g,
                replacement: apiPrefix
              }, {
                match: /__CDN_DOMAIN__/g,
                replacement: cdnDomain
              }]
            },
            files: [{'src':'dist/**/*.js','dest':'dist/'}]
          }
        },
        uglify: { dist: { files: {'dist/app.min.js': 'dist/app.js'} } },
        // 其余任务…
      });
    
      grunt.registerTask('default', ['replace', 'uglify']);
    };
    
  4. 敏感信息(数据库密码、私钥)使用 Docker SecretK8s Secret,以文件方式挂载,然后在 Gruntfile 中 fs.readFileSync('/run/secrets/db_pass','utf8').trim() 读取,避免 ps 可见。

  5. 多环境差异化配置统一走 ConfigMap + Secret,同一份镜像在不同命名空间(dev/prod)只需改编排变量即可,无需重新打标签,符合国内金融、政企项目「 immutable delivery 」审计要求。

  6. 若团队习惯 .env 文件,可在 ENTRYPOINT 里加 env-cmd -f /config/.env 先导入再启 grunt,但须把 .env 挂载为 read-only 卷,防止容器内误写。

  7. 本地开发保持「零差异」:
    用 docker-compose.override.yml 把源码目录挂成卷,grunt-contrib-watch 监听文件变化,Livereload 端口 35729 通过 ports: "35729:35729" 暴露给宿主机浏览器,调试体验与裸机一致。

拓展思考

  • 灰度场景:结合 K8s Downward API 把 Pod 名称、IP 作为环境变量注入,Grunt 任务可在打包时写入 window.__BUILD_POD_ID__,方便 Sentry 报错时精准定位到具体实例。
  • 构建加速:把不常变的 grunt 插件预先在 builder 阶段 全局安装,再 COPY --from=builder /usr/local/lib/node_modules /usr/local/lib/node_modules,减少重复下载,提高阿里云龙构建的缓存命中率。
  • 安全左移:在 CI 阶段用 grunt-contrib-jshint + eslint 扫描,若发现 process.env 被直接拼接到 html 属性,则中断流水线,防止 XSS;国内银行项目已把这条写进基线检查
  • 边缘部署:在 ARM 边缘节点运行 Grunt 镜像时,注意 phantomjs-prebuilt 等旧插件不提供 arm64 二进制,需改用 puppeteer-core 并指定 executablePath: '/usr/bin/chromium-browser',否则任务会报 spawn ENOENT