强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 陷阱

  1. Erlang 节点名必须在容器网络内可解析
  2. EPMD 端口 4369 需要暴露
  3. 集群通信端口范围需要配置
  4. 容器重启后 cookie 必须保持一致
  5. 时钟同步对分布式系统很重要

💡 最佳实践

  1. 使用多阶段构建减小镜像体积
  2. 利用 Docker 缓存层加速构建
  3. 使用 Alpine Linux 作为基础镜像
  4. 以非 root 用户运行容器
  5. 配置健康检查

21.8 扩展阅读


上一章:20 - 性能优化 下一章:22 - NIF 与 C 集成