Odoo 17 + Docker 生产环境搭建:备份、监控与故障演练全记录
从零搭建 Odoo ERP 生产环境:一次完整的容器化部署与运维实践
本文记录了我从零开始,在一台 4C4G 虚拟机上完成 Odoo ERP 生产环境搭建与维护的全过程。涵盖容器化部署、安全加固、备份恢复、反向代理、HTTPS、日志管理、监控告警、故障演练等生产运维核心环节。
一、项目背景
1.1 目标
搭建一套接近生产级别的 Odoo ERP 环境,练习以下能力:
- 容器化部署与编排
- 数据库安全加固
- 备份与恢复流程
- 反向代理与 HTTPS
- 日志管理
- 监控与告警
- 故障演练
1.2 环境信息
| 项目 | 配置 |
|---|---|
| 虚拟机 | 4 核 CPU / 4GB 内存 / 40GB 硬盘 |
| 操作系统 | Ubuntu 24.04 LTS |
| 容器引擎 | Docker + Docker Compose |
| 部署目录 | /opt/odoo-server/odoo-deploy |
1.3 技术栈
| 组件 | 版本 | 作用 |
|---|---|---|
| Odoo | 17 | ERP 业务系统 |
| PostgreSQL | 16 | 数据库 |
| Nginx | 1.27 | 反向代理 + HTTPS |
| Prometheus | 2.54.1 | 指标采集 |
| Node Exporter | 1.8.2 | 宿主机指标暴露 |
| Grafana | 11.2.0 | 监控可视化 |
二、基础部署:Odoo + PostgreSQL
2.1 目录结构
odoo-deploy/
├── docker-compose.yml
├── odoo.conf
├── postgresql.conf
├── addons/
├── logs/
│ └── odoo/
├── nginx/
│ ├── odoo.conf
│ ├── certs/
│ └── logs/
├── monitoring/
│ └── prometheus/
│ ├── prometheus.yml
│ └── alerts.yml
├── backups/
└── docs/2.2 docker-compose.yml(基础版)
services:
db:
image: postgres:16
container_name: odoo-db
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo_ChangeMe_2024
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
volumes:
- pgdata:/var/lib/postgresql/data
- ./postgresql.conf:/etc/postgresql/postgresql.conf
command: >
postgres
-c config_file=/etc/postgresql/postgresql.conf
networks:
- odoo-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
odoo:
image: odoo:17
container_name: odoo-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
expose:
- "8069"
- "8072"
volumes:
- odoofilestore:/var/lib/odoo
- ./odoo.conf:/etc/odoo/odoo.conf
- ./addons:/mnt/extra-addons
- ./logs/odoo:/var/log/odoo
environment:
- HOST=db
- USER=odoo
- PASSWORD=odoo_ChangeMe_2024
command: odoo -c /etc/odoo/odoo.conf
networks:
- odoo-net
volumes:
pgdata:
odoofilestore:
networks:
odoo-net:
driver: bridge2.3 odoo.conf(4C4G 调优版)
[options]
; ===== 数据库连接 =====
db_host = db
db_port = 5432
db_user = odoo
db_password = odoo_ChangeMe_2024
db_name = False
dbfilter = ^odoo_prod$
; ===== 核心性能参数(4C4G 调优)=====
; worker 数量 = (2 × CPU核心数) + 1
workers = 9
; 内存软硬限制
limit_memory_soft = 1258291200
limit_memory_hard = 3355443200
; 单请求超时
limit_time_cpu = 600
limit_time_real = 1200
; 请求数上限,防止内存泄漏累积
limit_request = 8192
; ===== 反向代理 =====
proxy_mode = True
; ===== 日志 =====
logfile = /var/log/odoo/odoo.log
log_level = info
; ===== 安全 =====
list_db = False
; 数据目录
data_dir = /var/lib/odoo
; 额外 addons 目录
addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons2.4 postgresql.conf(关键调优)
listen_addresses = '*'
max_connections = 100
# 内存(4G 内存机器,PostgreSQL 约占 1GB)
shared_buffers = 1GB
effective_cache_size = 2GB
work_mem = 16MB
maintenance_work_mem = 128MB
# WAL
wal_buffers = 16MB
checkpoint_completion_target = 0.9
min_wal_size = 1GB
max_wal_size = 4GB
# 查询规划
random_page_cost = 1.1
effective_io_concurrency = 200
# 日志
log_min_duration_statement = 1000
log_checkpoints = on
# 其他
default_statistics_target = 1002.5 启动与初始化
cd /opt/odoo-server/odoo-deploy
mkdir -p addons logs/odoo
docker compose up -d
docker compose logs -f odoo看到 HTTP service (werkzeug) running on 0.0.0.0:8069 即成功。
2.6 创建数据库
首次部署时 list_db = False 会导致数据库管理页被禁用,需要临时改为 list_db = True,创建数据库后再改回:
- 修改
odoo.conf:list_db = True docker compose restart odoo- 访问
http://IP:8069/web/database/manager创建数据库 - 创建成功后,改回
list_db = False并重启
⚠️ 踩坑记录:dbfilter = ^%d$会按访问域名匹配数据库。由于我用 IP 访问,与数据库名不匹配,导致登录页被重定向到数据库管理页。改为精确匹配dbfilter = ^odoo_prod$后解决。
三、备份与恢复
3.1 热备份脚本 backup.sh
#!/bin/bash
BACKUP_DIR="/opt/odoo-server/odoo-deploy/backups"
LOG_FILE="/opt/odoo-server/odoo-deploy/backups/backup.log"
DATE=$(date +%F_%H%M%S)
RETENTION_DAYS=7
mkdir -p "$BACKUP_DIR"
echo "==============================" >> "$LOG_FILE"
echo "[$(date '+%F %T')] 开始备份" >> "$LOG_FILE"
cd /opt/odoo-server/odoo-deploy
# 1. 备份数据库
echo "[$(date '+%F %T')] 导出数据库 odoo_prod" >> "$LOG_FILE"
docker compose exec -T db pg_dump -U odoo odoo_prod > "$BACKUP_DIR/odoo_prod_$DATE.sql" 2>> "$LOG_FILE"
if [ $? -ne 0 ]; then
echo "[$(date '+%F %T')] 数据库备份失败" >> "$LOG_FILE"
exit 1
fi
# 2. 备份 filestore
echo "[$(date '+%F %T')] 打包 filestore" >> "$LOG_FILE"
docker run --rm \
-v odoo-deploy_odoofilestore:/data \
-v "$BACKUP_DIR":/backup \
alpine tar czf "/backup/filestore_$DATE.tar.gz" -C /data . 2>> "$LOG_FILE"
if [ $? -ne 0 ]; then
echo "[$(date '+%F %T')] filestore 备份失败" >> "$LOG_FILE"
exit 1
fi
# 3. 清理旧备份
echo "[$(date '+%F %T')] 清理 $RETENTION_DAYS 天前的备份" >> "$LOG_FILE"
find "$BACKUP_DIR" -name "odoo_prod_*.sql" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "filestore_*.tar.gz" -mtime +$RETENTION_DAYS -delete
# 4. 完成
echo "[$(date '+%F %T')] 备份完成" >> "$LOG_FILE"
ls -lh "$BACKUP_DIR"/*.sql "$BACKUP_DIR"/*.tar.gz 2>/dev/null >> "$LOG_FILE"3.2 定时任务
chmod +x backup.sh
crontab -e添加:
0 2 * * * /opt/odoo-server/odoo-deploy/backup.sh⚠️ 踩坑记录:最小化安装的 Ubuntu 未预装 cron,需要sudo apt install -y cron并systemctl enable cron。
3.3 恢复流程(已验证)
# 1. 停 Odoo(切断活动连接)
docker compose stop odoo
# 2. 删库(模拟故障)
docker compose exec db psql -U odoo -d postgres -c "DROP DATABASE odoo_prod;"
docker compose exec db psql -U odoo -d postgres -c "CREATE DATABASE odoo_prod OWNER odoo;"
# 3. 恢复数据库
docker compose exec -T db psql -U odoo odoo_prod < backups/odoo_prod_具体时间.sql
# 4. 恢复 filestore
docker run --rm \
-v odoo-deploy_odoofilestore:/data \
-v "$(pwd)/backups":/backup \
alpine sh -c "rm -rf /data/* && tar xzf /backup/filestore_具体时间.tar.gz -C /data"
# 5. 启动 Odoo
docker compose start odoo
# 6. 浏览器验证数据完整⚠️ 踩坑记录:
DROP DATABASE时被活动连接阻塞,需先docker compose stop odoo- 手动执行
pg_dump时变量未定义,文件被写到根目录/,报Permission denied。必须写成脚本执行,不能手动敲带变量的命令。
四、反向代理与 HTTPS
4.1 Nginx 配置
nginx/odoo.conf:
upstream odoo {
server odoo:8069;
}
upstream odoo-chat {
server odoo:8072;
}
# HTTP -> HTTPS 跳转
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
# HTTPS 主服务
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/certs/odoo.crt;
ssl_certificate_key /etc/nginx/certs/odoo.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/odoo.access.log;
error_log /var/log/nginx/odoo.error.log;
client_max_body_size 100M;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location /longpolling {
proxy_pass http://odoo-chat;
}
location / {
proxy_pass http://odoo;
proxy_redirect off;
proxy_read_timeout 720s;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
}
location ~* /web/static/ {
proxy_cache_valid 200 90m;
proxy_buffering on;
expires 864000;
proxy_pass http://odoo;
}
}4.2 生成自签证书
mkdir -p nginx/certs
cd nginx/certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout odoo.key \
-out odoo.crt \
-subj "/C=CN/ST=Guangdong/L=Huizhou/O=HomeLab/CN=192.168.136.155"4.3 docker-compose.yml 中 Nginx 服务
nginx:
image: nginx:1.27
container_name: odoo-nginx
restart: unless-stopped
depends_on:
- odoo
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/odoo.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
- ./nginx/logs:/var/log/nginx
networks:
- odoo-net4.4 验证
http://IP→ 自动 301 跳转到https://IPhttps://IP→ 浏览器提示"证书不受信任"(自签证书正常现象),点"继续访问"即可http://IP:8069→ 拒绝连接(Odoo 端口已收敛至 Docker 内网)
⚠️ 踩坑记录:配置 Nginx 后 8069 仍可访问,原因是
docker-compose.yml中 Odoo 服务还保留着ports: - "8069:8069"。必须改为expose,让 Odoo 仅在 Docker 内网暴露。💡 补充说明:本文使用自签证书,仅适合纯内网、不对外暴露的练习场景。生产环境建议使用 Let's Encrypt 免费证书,避免浏览器"证书不受信任"警告,也避免用户养成"点继续访问"的习惯。申请工具推荐 Certbot 或 acme.sh。如果服务器没有公网域名,也可以考虑使用内部 CA 签发证书,把根证书分发到客户端信任列表。
五、日志管理
5.1 logrotate 配置
/etc/logrotate.d/odoo-nginx:
/opt/odoo-server/odoo-deploy/logs/odoo/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 admin_123 admin_123
}
/opt/odoo-server/odoo-deploy/nginx/logs/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 admin_123 admin_123
}5.2 测试
sudo logrotate -f /etc/logrotate.d/odoo-nginx
ls -lh logs/odoo/
ls -lh nginx/logs/⚠️ 踩坑记录:
- 最小化安装未预装 logrotate,需
apt install logrotate- 日志目录权限过宽会报
parent directory has insecure permissions,需chmod 755收紧
六、监控与告警
6.1 Prometheus 配置
monitoring/prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']6.2 告警规则
monitoring/prometheus/alerts.yml:
groups:
- name: odoo-alerts
rules:
- alert: NodeDown
expr: up{job="node-exporter"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Node Exporter 不可达"
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.9
for: 2m
labels:
severity: warning
annotations:
summary: "内存使用率超过 90%"
- alert: HighDiskUsage
expr: (1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})) > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "根分区使用率超过 85%"
- alert: HighCpuLoad
expr: node_load1 > 4
for: 5m
labels:
severity: warning
annotations:
summary: "CPU 1 分钟负载超过 4"6.3 docker-compose.yml 中监控服务
node-exporter:
image: prom/node-exporter:v1.8.2
container_name: odoo-node-exporter
restart: unless-stopped
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
networks:
- odoo-net
prometheus:
image: prom/prometheus:v2.54.1
container_name: odoo-prometheus
restart: unless-stopped
volumes:
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
- promdata:/prometheus
ports:
- "9090:9090"
networks:
- odoo-net
grafana:
image: grafana/grafana:11.2.0
container_name: odoo-grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafanadata:/var/lib/grafana
ports:
- "3000:3000"
networks:
- odoo-net6.4 Grafana 配置
- 添加数据源:Connections → Data sources → Prometheus,URL 填
http://prometheus:9090 - 导入面板:Dashboards → Import,输入 ID
1860(Node Exporter Full) 手动选择变量:
Job=node-exporterNodename= 容器哈希(见下方说明)Instance=node-exporter:9100
💡 如何找到
Nodename的值:在 Prometheus 页面(http://IP:9090)的查询框里输入node_uname_info,点击 Execute。结果里nodename标签的值就是面板需要的值。容器环境下它通常是一串容器哈希,不是宿主机名,所以社区面板的默认变量会显示为空,需要手动选。如果希望面板自动识别,可以把 Node Exporter 的
--path.rootfs和宿主机 hostname 关联起来,或者在 Prometheus 里用label_replace把nodename重写为可读名称。这一步属于进阶优化,不影响基本使用。
6.5 告警触发验证
# 停掉 node-exporter
docker compose stop node-exporter
# 等 1 分钟,访问 http://IP:9090/alerts
# NodeDown 会从 Inactive → Pending → Firing
# 恢复
docker compose start node-exporter⚠️ 踩坑记录:
gcr.io国内无法直连,cAdvisor 镜像拉取失败,需换国内镜像源- cAdvisor 在 Ubuntu 24.04 + containerd snapshotter 下无法识别容器,改用 Node Exporter
- Grafana 数据源命名冲突(prometheus vs prometheus-1),需删除冗余数据源并重命名
- 社区面板变量依赖
node_uname_info的 nodename 标签,容器环境下该值为容器哈希,需手动选择
七、故障演练
7.1 演练一:Odoo 容器停止
docker compose stop odoo现象:
- 访问
https://IP→ 502 Bad Gateway (nginx/1.27.5) nginx/logs/error.log显示:connect() failed (111: Connection refused) while connecting to upstream
恢复:
docker compose start odoo恢复时间:约 10 秒。
结论:Nginx 独立于 Odoo,后端崩溃不影响入口可用性;502 排查第一步看 Nginx error.log。
7.2 演练二:数据库停止(可选)
docker compose stop db
docker compose start db
docker compose restart odoo结论:数据库重启后需重启 Odoo 重建连接池,否则 Odoo 仍持有失效连接。
7.3 演练三:数据库删除与恢复(前期已完成)
- 操作:
pg_dump备份 →DROP DATABASE→ 从备份恢复 - 恢复时间:约 2 分钟
- 数据完整性:✅ 模块与记录完整
- 结论:备份流程可用,恢复步骤已验证
7.4 演练四:告警触发验证
- 操作:
docker compose stop node-exporter - 现象:NodeDown 从 Inactive → Pending → Firing
- 恢复:
docker compose start node-exporter - 结论:告警规则、
for持续时间、severity分级均正常工作
八、踩坑总结
| # | 问题 | 根因 | 解决方案 |
|---|---|---|---|
| 1 | 数据库连接认证失败 | PostgreSQL 初始化时密码已写入数据卷,后续修改无效 | docker compose down -v 清空数据卷重建 |
| 2 | 登录页被重定向到数据库管理页 | dbfilter = ^%d$ 按域名匹配,IP 访问不匹配 | 改为 dbfilter = ^odoo_prod$ |
| 3 | DROP DATABASE 被阻塞 | Odoo 保持 10 个活动连接 | 先 docker compose stop odoo 切断连接 |
| 4 | pg_dump 报 Permission denied | 手动执行时变量未定义,文件被写到根目录 | 写成脚本执行,不手动敲带变量的命令 |
| 5 | 配置 Nginx 后 8069 仍可访问 | Odoo 服务仍保留 ports 映射 | 改为 expose,端口仅在内网暴露 |
| 6 | 最小化安装缺少工具 | Ubuntu 默认不带 cron 和 logrotate | apt install -y cron logrotate |
| 7 | logrotate 报 insecure permissions | 日志目录权限过宽(组可写) | chmod 755 收紧目录权限 |
| 8 | cAdvisor 拉取失败 | gcr.io 国内无法直连 | 换国内镜像源 |
| 9 | cAdvisor 无法识别容器 | Ubuntu 24.04 Docker 使用 containerd snapshotter | 改用 Node Exporter |
| 10 | Prometheus targets 全部 UNKNOWN | Prometheus 容器未重建,配置未生效 | docker compose rm -f 后重建 |
| 11 | Grafana 面板全部 N/A | 数据源命名冲突,面板绑定了空数据源 | 删除冗余数据源,重命名为标准名 |
| 12 | Node Exporter 面板变量为空 | 容器环境下 nodename 为容器哈希 | 手动选择变量值 |
九、项目成果
9.1 完成清单
| 阶段 | 内容 | 状态 |
|---|---|---|
| 容器化部署 | Odoo 17 + PostgreSQL 16 + Docker Compose | ✅ |
| 安全加固 | dbfilter + list_db = False | ✅ |
| 备份流程 | pg_dump + filestore + crontab + 7 天保留 | ✅ |
| 备份验证 | 删库后完整恢复,数据完整 | ✅ |
| 日志管理 | logrotate 轮转 Odoo 与 Nginx 日志 | ✅ |
| 反向代理 | Nginx 反代,Odoo 端口收敛至内网 | ✅ |
| HTTPS | 自签证书 + HTTP 301 强制跳转 | ✅ |
| 监控 | Prometheus + Node Exporter + Grafana | ✅ |
| 告警 | 4 条规则,验证完整触发链路 | ✅ |
| 故障演练 | 容器停止 + 告警触发 + 恢复流程 | ✅ |
9.2 资源占用
| 指标 | 数值 |
|---|---|
| 内存占用 | ~1.1 GB / 3.8 GB |
| Swap 占用 | 0 GB / 2 GB |
| 根分区占用 | 33.7% |
| 运行时长 | 8+ 小时 |
评论已关闭