Erlang/OTP 完全指南 / 21 - Docker 容器化
第 21 章:Docker 容器化
本章学习如何使用 Docker 和 Docker Compose 部署 Erlang/OTP 应用,包括多阶段构建最佳实践。
21.1 基础 Dockerfile
21.1.1 简单 Dockerfile
# Dockerfile
FROM erlang:27-alpine
WORKDIR /app
# 复制依赖文件(利用 Docker 缓存层)
COPY rebar.config rebar.lock ./
# 下载依赖
RUN rebar3 compile
# 复制源代码
COPY . .
# 构建 Release
RUN rebar3 as prod release
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["_build/prod/rel/myapp/bin/myapp", "foreground"]
21.2 多阶段构建(推荐)
21.2.1 Builder + Runtime
# ==== 构建阶段 ====
FROM erlang:27-alpine AS builder
WORKDIR /build
# 复制依赖文件
COPY rebar.config rebar.lock ./
# 下载依赖(缓存层)
RUN rebar3 compile
# 复制源代码
COPY . .
# 构建 Release
RUN rebar3 as prod release
# ==== 运行阶段 ====
FROM alpine:3.19 AS runtime
# 安装最小依赖
RUN apk add --no-cache \
libstdc++ \
openssl \
ncurses-libs \
libintl \
bash
WORKDIR /opt/myapp
# 从构建阶段复制 Release
COPY --from=builder /build/_build/prod/rel/myapp .
# 创建非 root 用户
RUN addgroup -g 1000 myapp && \
adduser -u 1000 -G myapp -s /bin/bash -D myapp && \
chown -R myapp:myapp /opt/myapp
USER myapp
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD /opt/myapp/bin/myapp pid || exit 1
# 启动命令
ENTRYPOINT ["/opt/myapp/bin/myapp"]
CMD ["foreground"]
21.2.2 完整的生产 Dockerfile
# ==== 依赖层(缓存) ====
FROM erlang:27-alpine AS deps
WORKDIR /build
COPY rebar.config rebar.lock ./
RUN mkdir -p config && \
rebar3 compile
# ==== 构建层 ====
FROM deps AS builder
COPY . .
RUN rebar3 as prod release && \
rebar3 as prod tar
# ==== 运行时 ====
FROM alpine:3.19 AS runtime
ARG APP_VERSION=0.1.0
RUN apk add --no-cache \
bash \
libstdc++ \
openssl \
ncurses-libs
WORKDIR /opt/myapp
# 从 tar 包安装(更干净)
COPY --from=builder /build/_build/prod/rel/myapp/myapp-*.tar.gz /tmp/
RUN tar xzf /tmp/myapp-*.tar.gz -C /opt/myapp && \
rm /tmp/myapp-*.tar.gz
# 配置文件
COPY config/vm.args /opt/myapp/releases/${APP_VERSION}/vm.args
COPY config/sys.config /opt/myapp/releases/${APP_VERSION}/sys.config
# 权限
RUN addgroup -g 1000 myapp && \
adduser -u 1000 -G myapp -s /bin/bash -D myapp && \
chown -R myapp:myapp /opt/myapp
USER myapp
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD /opt/myapp/bin/myapp pid || exit 1
CMD ["foreground"]
21.3 Docker Compose
21.3.1 开发环境
# docker-compose.yml
version: '3.8'
services:
myapp:
build:
context: .
dockerfile: Dockerfile
target: builder
ports:
- "8080:8080"
volumes:
- .:/app
- rebar_cache:/app/_build
environment:
- ERL_FLAGS=+pc unicode
command: rebar3 shell --apps myapp
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
rebar_cache:
pgdata:
21.3.2 生产环境
# docker-compose.prod.yml
version: '3.8'
services:
myapp:
image: myapp:latest
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- MYAPP_DB_HOST=postgres
- MYAPP_DB_PORT=5432
deploy:
replicas: 3
resources:
limits:
cpus: '2'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 10s
timeout: 5s
retries: 5
secrets:
- db_password
volumes:
pgdata:
secrets:
db_password:
file: ./secrets/db_password.txt
21.4 Erlang 节点配置
21.4.1 VM 参数
%% config/vm.args(生产环境)
-name myapp@${HOSTNAME}
-setcookie ${ERLANG_COOKIE}
## 性能
+P 1048576
+A 128
+sbt s
+sbwt very_long
## 禁用 Ctrl+C(容器中不需要)
+B
## 核心转储
+Mea max
21.4.2 环境变量配置
%% config/sys.config
[
{myapp, [
{port, ${MYAPP_PORT}},
{db_host, "${MYAPP_DB_HOST}"},
{db_port, ${MYAPP_DB_PORT}}
]}
].
%% 在应用中读取环境变量
%% src/myapp_config.erl
-module(myapp_config).
-export([get_env/2, get_port/0]).
get_env(Key, Default) ->
case os:getenv(Key) of
false -> Default;
Value -> Value
end.
get_port() ->
case os:getenv("MYAPP_PORT") of
false -> application:get_env(myapp, port, 8080);
Value -> list_to_integer(Value)
end.
21.5 .dockerignore
# .dockerignore
_build/
.git/
.gitignore
*.beam
*.plt
erl_crash.dump
log/
tmp/
test/
doc/
README.md
21.6 实战:完整部署流程
21.6.1 构建镜像
# 构建镜像
docker build -t myapp:latest .
# 带版本号
docker build -t myapp:0.1.0 .
# 测试运行
docker run -it --rm -p 8080:8080 myapp:latest
21.6.2 推送到镜像仓库
# 标记镜像
docker tag myapp:latest registry.example.com/myapp:latest
docker tag myapp:latest registry.example.com/myapp:0.1.0
# 推送
docker push registry.example.com/myapp:latest
docker push registry.example.com/myapp:0.1.0
21.6.3 Kubernetes 部署
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: registry.example.com/myapp:0.1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "500m"
limits:
memory: "512Mi"
cpu: "2000m"
livenessProbe:
exec:
command: ["/opt/myapp/bin/myapp", "pid"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: MYAPP_DB_HOST
valueFrom:
configMapKeyRef:
name: myapp-config
key: db_host
21.7 注意事项
⚠️ Docker 陷阱
- Erlang 节点名必须在容器网络内可解析
- EPMD 端口 4369 需要暴露
- 集群通信端口范围需要配置
- 容器重启后 cookie 必须保持一致
- 时钟同步对分布式系统很重要
💡 最佳实践
- 使用多阶段构建减小镜像体积
- 利用 Docker 缓存层加速构建
- 使用 Alpine Linux 作为基础镜像
- 以非 root 用户运行容器
- 配置健康检查
21.8 扩展阅读
上一章:20 - 性能优化 下一章:22 - NIF 与 C 集成