用于生产 ERP 部署的 Docker:完整操作指南
在 Docker 容器中运行 ERP 系统的组织报告称,与传统的裸机部署相比,部署周期缩短了 73%,与环境相关的事件减少了 45%。 Docker 将 ERP 部署从需要多天、容易出错的流程转变为任何团队成员都可以执行的可重复、版本控制的操作。
本指南涵盖了在生产 Docker 环境中运行企业 ERP 系统的整个生命周期(包括 Odoo、自定义 NestJS 后端和 Next.js 前端)。
要点
- 多阶段 Docker 构建将 ERP 容器镜像大小减少 60-80%,提高部署速度
- Docker Compose 将 ERP、数据库、反向代理和缓存服务编排为单个可部署单元
- 命名卷和绑定挂载确保容器重启和升级期间的数据持久性
- 健康检查和重启策略提供从瞬时故障中自动恢复
Docker 化 ERP 堆栈的架构
生产 ERP 部署通常涉及五个或更多互连服务。 Docker Compose 以声明方式定义这些服务,确保跨环境的部署一致。
服务拓扑
标准 Docker 化 ERP 堆栈:
- 应用程序服务器:ERP运行时(Odoo、NestJS或类似)
- 数据库:具有持久卷存储的 PostgreSQL
- 反向代理:Nginx处理SSL终止、静态文件和请求路由
- 缓存层:Redis用于会话存储、作业队列和应用程序缓存
- 后台工作人员:用于电子邮件、报告和集成的异步作业处理器
可选服务包括备份容器(cron 上的 pg_dump)、监控 sidecar(Prometheus 导出器)和日志传送器(Fluent Bit)。
ERP 应用程序的多阶段构建
多阶段构建对于生产 Docker 镜像至关重要。它们将构建时依赖性与运行时分离,生成精益、安全的映像。
NestJS 后端构建
# Stage 1: Install dependencies and build
FROM node:20-alpine AS builder
WORKDIR /app
# Install pnpm
RUN corepack enable
# Copy workspace configuration
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/ ./packages/
COPY apps/api/package.json ./apps/api/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY apps/api/ ./apps/api/
RUN pnpm --filter @ecosire/db build
RUN pnpm --filter @ecosire/types build
RUN pnpm --filter @ecosire/validators build
RUN pnpm --filter @ecosire/api build
# Stage 2: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
COPY --from=builder --chown=appuser:appgroup /app/apps/api/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/apps/api/package.json ./
USER appuser
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
CMD ["node", "dist/main.js"]
Next.js 前端构建
FROM node:20-alpine AS builder
WORKDIR /app
RUN corepack enable
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/ ./packages/
COPY apps/web/package.json ./apps/web/
RUN pnpm install --frozen-lockfile
COPY apps/web/ ./apps/web/
RUN pnpm --filter @ecosire/web build
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
COPY --from=builder --chown=appuser:appgroup /app/apps/web/.next/standalone ./
COPY --from=builder --chown=appuser:appgroup /app/apps/web/.next/static ./.next/static
COPY --from=builder --chown=appuser:appgroup /app/apps/web/public ./public
USER appuser
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "server.js"]
图像大小比较
| 构建类型 | 图像尺寸 | 构建时间 |
|---|---|---|
| 单阶段(全节点镜像) | 1.8 GB | 1.8 GB 4 分钟 |
| 单级(阿尔卑斯) | 650 MB | 3.5 分钟 |
| 多级(高山) | 180 MB | 5 分钟 |
| 多阶段 + 精简依赖 | 120 MB | 5.5 分钟 |
5.5 分钟的构建时间是可以接受的,因为它发生在 CI 中,而不是在开发人员机器上。
用于生产的 Docker Compose
version: "3.8"
services:
api:
build:
context: .
dockerfile: apps/api/Dockerfile
environment:
- DATABASE_URL=postgresql://app:${DB_PASSWORD}@db:5432/ecosire
- REDIS_URL=redis://redis:6379
- NODE_ENV=production
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- backend
- frontend
web:
build:
context: .
dockerfile: apps/web/Dockerfile
environment:
- API_URL=http://api:3001
- NODE_ENV=production
depends_on:
- api
restart: unless-stopped
networks:
- frontend
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ecosire
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d ecosire"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- backend
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./infrastructure/nginx/production.conf:/etc/nginx/conf.d/default.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on:
- web
- api
restart: unless-stopped
networks:
- frontend
volumes:
postgres-data:
redis-data:
networks:
frontend:
backend:
网络隔离
上面的配置使用两个网络:
- 前端:Nginx、Web 和 API(两者的 Nginx 代理)
- 后端:API、数据库和 Redis
无法从 Nginx 容器或外部网络访问数据库和 Redis。这种网络分段是一项关键的安全实践。
卷管理和数据持久化
卷是 Docker 化 ERP 部署中最关键的部分。丢失卷就丢失数据。
卷类型
| 类型 | 使用案例 | 坚持 | 性能 |
|---|---|---|---|
| 命名卷 | 数据库、Redis | 容器移除后仍然存在 | 本机文件系统速度 |
| 绑定坐骑 | 配置文件、日志 | 与主机文件系统绑定 | 本机文件系统速度 |
| tmpfs 安装 | 临时文件、机密 | 仅内存,重启后丢失 | 内存速度 |
Docker Volume 的备份策略
#!/bin/bash
# backup-volumes.sh - Run via cron every 6 hours
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/backups"
# Stop the application briefly for consistent backup
docker compose stop api web
# Backup PostgreSQL
docker compose exec -T db pg_dump -U app ecosire | gzip > "$BACKUP_DIR/db_$TIMESTAMP.sql.gz"
# Backup Redis
docker compose exec -T redis redis-cli -a "$REDIS_PASSWORD" BGSAVE
sleep 5
docker cp $(docker compose ps -q redis):/data/dump.rdb "$BACKUP_DIR/redis_$TIMESTAMP.rdb"
# Restart services
docker compose start api web
# Upload to S3
aws s3 sync "$BACKUP_DIR" "s3://company-backups/docker-volumes/" --exclude "*.tmp"
# Retain 30 days locally
find "$BACKUP_DIR" -name "*.gz" -mtime +30 -delete
find "$BACKUP_DIR" -name "*.rdb" -mtime +30 -delete
健康检查和重启策略
生产容器必须自我报告其运行状况并自动从故障中恢复。
应用程序健康检查端点
// health.controller.ts
@Controller('health')
export class HealthController {
constructor(
private readonly db: DatabaseService,
private readonly redis: RedisService,
) {}
@Get()
@Public()
async check() {
const checks = {
database: await this.checkDatabase(),
redis: await this.checkRedis(),
uptime: process.uptime(),
memory: process.memoryUsage(),
};
const healthy = checks.database && checks.redis;
return { status: healthy ? 'ok' : 'degraded', checks };
}
private async checkDatabase(): Promise<boolean> {
try {
await this.db.execute('SELECT 1');
return true;
} catch {
return false;
}
}
private async checkRedis(): Promise<boolean> {
try {
await this.redis.ping();
return true;
} catch {
return false;
}
}
}
重启策略选择
| 政策 | 行为 | 使用案例 |
|---|---|---|
| 代码0 | 永不重启 | 开发、一次性任务 |
| 代码0 | 仅在非零退出时重新启动 | 工人,批量作业 |
| 代码0 | 始终重新启动(包括 docker 守护进程重新启动) | 生产服务 |
| 代码0 | 与 always 类似,但尊重手动停止 | 大多数制作服务 |
使用 unless-stopped 进行生产服务。这可确保容器在服务器重新启动或 Docker 守护程序重新启动后重新启动,但在维护期间遵循手动 docker compose stop 命令。
部署工作流程
使用 Docker Compose 滚动更新
#!/bin/bash
# deploy.sh - Zero-downtime deployment
set -e
echo "Pulling latest code..."
git pull origin main
echo "Building new images..."
docker compose build --no-cache api web
echo "Rolling update - API first..."
docker compose up -d --no-deps api
sleep 10
# Verify API health
if ! curl -sf http://localhost:3001/health > /dev/null; then
echo "API health check failed, rolling back..."
docker compose up -d --no-deps api
exit 1
fi
echo "Rolling update - Web..."
docker compose up -d --no-deps web
sleep 5
# Verify Web health
if ! curl -sf http://localhost:3000 > /dev/null; then
echo "Web health check failed, rolling back..."
docker compose up -d --no-deps web
exit 1
fi
echo "Deployment complete!"
docker compose ps
数据库迁移安全
切勿在应用程序启动时运行迁移。相反,将它们作为单独的步骤运行:
# Run migrations before deploying new containers
docker compose run --rm api npx drizzle-kit push
# Then deploy the new version
docker compose up -d
此模式可确保如果迁移失败,旧版本将继续运行而不受影响。
日志记录和调试
集中日志记录
# Add to docker-compose.yml
services:
api:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
labels: "service"
labels:
service: "ecosire-api"
常用调试命令
# View logs for a specific service
docker compose logs -f api --tail 100
# Execute a shell inside a running container
docker compose exec api sh
# View resource usage
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
# Inspect container networking
docker compose exec api ping db
# View container environment variables
docker compose exec api env | sort
常见问题
我们如何在 Docker 中处理数据库迁移?
在部署新的应用程序容器之前,将迁移作为单独的步骤运行。使用 docker compose run --rm api npx drizzle-kit push (或 ORM 的迁移命令)作为预部署步骤。切勿在容器启动命令中嵌入迁移执行——失败的迁移不应阻止当前版本继续运行。
Docker 的性能开销是多少?
在 Linux 上,Docker 的性能开销可以忽略不计——对于 CPU 密集型工作负载来说通常低于 2%,而对于 I/O 密集型工作负载则没有可测量的差异。在 macOS 和 Windows 上,Docker 在虚拟机内运行,增加了 5-15% 的开销。对于生产(应该是 Linux),Docker 并不是一个有意义的性能问题。
我们如何在 Docker 中管理机密?
切勿将机密放入 Dockerfile 或 docker-compose.yml 文件中。使用从版本控制、Docker 密钥(对于 Swarm 模式)或外部密钥管理器(AWS Secrets Manager、HashiCorp Vault)中排除的环境变量文件 (.env)。对于 Docker Compose,项目根目录下的 .env 文件是最简单的方法。
我们应该使用 Docker Swarm 还是 Kubernetes?
对于大多数中小型企业 ERP 部署,Docker Compose 就足够了。 Docker Swarm 以最小的复杂性开销添加了多主机编排。当您需要自动扩展、复杂的网络策略或服务网格功能时,Kubernetes 是合适的选择。请参阅我们的 Kubernetes 扩展指南 和 微服务架构指南 了解决策框架。
我们如何在 Docker 中处理 Odoo 自定义模块?
将自定义模块安装为指向您的插件目录的绑定安装卷。在 Dockerfile 中,确保在 odoo.conf 中配置插件路径。对于 CI/CD,构建一个自定义 Docker 镜像来烘焙您的模块,确保版本一致性。请参阅我们现有的 Docker Odoo 部署指南 了解 Odoo 特定的配置。
接下来会发生什么
Docker is the foundation for modern ERP deployment.一旦您的容器化堆栈稳定,请探索零停机部署策略、生产监控和基础设施即代码来构建完全自动化的操作管道。
联系 ECOSIRE 获取 Docker 部署咨询,或探索我们的 Odoo 实施服务 进行完全托管的容器化 ERP 部署。
由 ECOSIRE 发布——帮助企业充满信心地部署企业软件。
作者
ECOSIRE Research and Development Team
在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。