一、为什么我们的Docker镜像像个“大胖子”?
如果你用过Elixir和Docker,可能遇到过这样的情况:一个简单的Web应用,构建出来的镜像动不动就超过1GB,上传到仓库慢,部署到服务器也慢。每次代码有一点点改动,重新构建镜像,又得等上半天。这感觉就像每次出门都要把整个家都背上,非常累赘。
这背后的原因,主要在于我们传统的构建方式。通常,我们会把编译环境(比如Erlang/OTP、Elixir、Node.js等)和最终要运行的应用代码,全都塞进一个镜像里。编译过程会产生很多中间文件、依赖缓存,这些“垃圾”如果没有被清理,也会留在镜像里,导致镜像虚胖。
我们的目标,就是给这个“大胖子”制定一个高效的减肥和锻炼计划,让它变得苗条、敏捷。
二、核心减肥计划:多阶段构建
Docker有一个非常棒的功能,叫做“多阶段构建”。你可以把它想象成一个现代化的汽车工厂:
- 第一阶段(构建车间):这里设备齐全,有各种重型工具(编译器、构建工具),负责把原材料(源代码)加工成成品(可执行程序)。
- 第二阶段(成品仓库):这里环境干净整洁,只存放从“构建车间”运过来的最终成品,以及运行它所需的最基本环境。那些笨重的工具和加工废料都被留在了第一阶段,不会带过来。
这样做的好处显而易见:最终的镜像只包含运行应用必不可少的东西,体积大大缩小。
下面,我们来看一个为Elixir Phoenix应用优化的多阶段Dockerfile示例。
技术栈声明:本文所有示例均基于 Elixir/Phoenix 技术栈。
# 第一阶段:构建阶段 - 这个镜像会比较大,但最终我们不会用它
FROM hexpm/elixir:1.15.7-erlang-26.2.2-alpine-3.19.1 AS builder
# 设置构建环境变量
ENV MIX_ENV=prod
# 安装编译依赖(Alpine Linux使用`apk`包管理器)
RUN apk add --no-cache build-base npm git python3
# 在镜像中创建工作目录
WORKDIR /app
# 首先拷贝依赖定义文件,这步可以利用Docker的缓存层
# 只要mix.exs和mix.lock没变,就不会重新下载依赖
COPY mix.exs mix.lock ./
RUN mix do deps.get --only $MIX_ENV, deps.compile
# 拷贝配置文件和静态资源相关依赖
COPY config config
# 假设前端使用esbuild,拷贝其配置文件
COPY assets assets
RUN mix do assets.deploy
# 拷贝所有应用源代码并编译
COPY lib lib
COPY priv priv
RUN mix do compile, release
# 第二阶段:运行阶段 - 这个镜像才是我们最终要的,非常小巧
FROM alpine:3.19.1 AS runner
# 安装运行Erlang应用所需的最小依赖:BEAM和OpenSSL
RUN apk add --no-cache openssl ncurses-libs libstdc++
# 创建一个非root用户来运行应用,增强安全性
RUN adduser -S -D -H -h /app appuser
USER appuser
WORKDIR /app
# 从`builder`阶段,只复制我们需要的最终产物:构建好的release
COPY --from=builder --chown=appuser:appuser /app/_build/prod/rel/my_app ./
# 设置环境变量,并暴露应用端口(Phoenix默认是4000)
ENV MIX_ENV=prod
ENV PORT=4000
EXPOSE 4000
# 使用release启动命令
CMD ["./bin/my_app", "start"]
这个Dockerfile的妙处在于:
- 分而治之:
builder阶段负责所有脏活累活,runner阶段保持纯净。 - 利用缓存:把变化频率低的文件(如
mix.exs)放在前面拷贝,能充分利用Docker构建缓存,加速后续构建。 - 最小化运行时:运行阶段基于极简的
alpine系统,只安装Erlang运行时必需的几个库,而不是整个Erlang/OTP。 - 安全考虑:创建了专门的
appuser用户来运行应用,而不是默认的root。
三、让部署流程“自动化”起来
镜像瘦身之后,我们还需要一个流畅的部署流程。这里,我们可以结合GitLab CI/CD(其他如GitHub Actions、Jenkins原理类似)来实现自动化。核心思想是:代码一推送到特定分支,就自动触发构建、测试、推送镜像和部署。
下面是一个.gitlab-ci.yml文件的简化示例:
# 定义构建镜像的步骤
stages:
- build
- deploy
# 缓存MIX和NPM依赖,可以极大加快后续构建速度
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- _build/
- deps/
- assets/node_modules/
# 第一步:构建并推送Docker镜像
build-image:
stage: build
image: docker:latest
services:
- docker:dind
variables:
# 为镜像打上git commit的标签,便于追踪
DOCKER_IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $DOCKER_IMAGE_TAG .
- docker push $DOCKER_IMAGE_TAG
only:
- main # 只在推送到main分支时触发
# 第二步:部署到服务器
deploy-to-server:
stage: deploy
image: alpine:latest
script:
# 使用SSH连接到你的服务器,执行部署脚本
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh -o StrictHostKeyChecking=no user@your-server-ip "
# 拉取最新的镜像
docker pull $DOCKER_IMAGE_TAG &&
# 停止旧容器并启动新容器
docker stop my_app_container || true &&
docker rm my_app_container || true &&
docker run -d --name my_app_container -p 4000:4000 --restart always $DOCKER_IMAGE_TAG
"
only:
- main
needs:
- build-image
这个流程实现了:
- 持续集成:代码变更自动触发构建和测试。
- 持续交付:自动将可用的镜像推送到仓库。
- 持续部署:自动将新镜像部署到生产或测试服务器。
四、高级技巧与避坑指南
优化之路永无止境。这里还有一些进阶技巧和需要注意的坑:
.dockerignore文件是你的好朋友:在项目根目录创建这个文件,告诉Docker哪些文件不需要拷贝进镜像(比如测试文件、日志、本地IDE配置等),能进一步减少构建上下文大小和镜像层。# .dockerignore 示例 .git _build deps assets/node_modules *.log .env Dockerfile docker-compose.yml谨慎选择基础镜像:
alpine镜像很小,但某些C库可能不兼容。如果你的依赖(如某些NIF)需要更完整的环境,可以考虑使用debian-slim或elixir:slim作为运行阶段基础镜像,在体积和兼容性间取得平衡。Release配置是关键:Elixir 1.9+的
Mix Release功能强大。确保你的config/runtime.exs文件配置正确,特别是关于SECRET_KEY_BASE、数据库URL等敏感信息,应该通过环境变量注入,而不是写在代码或编译配置中。健康检查:在Dockerfile或
docker-compose.yml中为容器添加健康检查指令,让编排工具(如Docker、K8s)能判断应用是否真的准备好了。# 可以在Dockerfile末尾添加 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1考虑分布式部署:如果你的应用是分布式的(多个节点组成集群),构建流程本身不变,但部署时需要额外步骤来设置节点名、Cookie和网络发现(如通过环境变量或K8s服务)。这超出了本文范围,但它是Elixir应用上生产的重要一环。
五、总结:从臃肿到敏捷的蜕变
通过以上步骤,我们可以将Elixir应用的Docker镜像构建与部署流程彻底优化:
- 应用场景:这套流程非常适合基于Elixir/Phoenix的Web API、实时应用的后端服务,尤其适合需要频繁迭代、持续交付的团队。
- 技术优缺点:
- 优点:镜像体积显著减小(从GB级降到MB级),构建速度因缓存而加快,部署流程自动化且可靠,安全性更高(非root用户运行)。
- 缺点:多阶段构建的Dockerfile稍微复杂;依赖Alpine镜像可能遇到罕见的C库兼容性问题;自动化部署需要前期搭建CI/CD环境。
- 注意事项:务必处理好敏感配置(使用环境变量);写好
.dockerignore;根据实际需求选择基础镜像;为容器添加健康检查。 - 文章总结:优化Docker镜像的本质是“职责分离”和“最小化原则”。多阶段构建负责分离构建与运行环境,而CI/CD负责自动化流程。对于Elixir开发者而言,结合
Mix Release和轻量级基础镜像,能打造出极其高效、安全的容器化应用。这不仅节省了存储和带宽,更重要的是,它提升了整个开发、测试、部署的生命周期效率,让团队能更快速、更自信地交付价值。
评论