从零搭建 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 技术栈

组件版本作用
Odoo17ERP 业务系统
PostgreSQL16数据库
Nginx1.27反向代理 + HTTPS
Prometheus2.54.1指标采集
Node Exporter1.8.2宿主机指标暴露
Grafana11.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: bridge

2.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/addons

2.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 = 100

2.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,创建数据库后再改回:

  1. 修改 odoo.conflist_db = True
  2. docker compose restart odoo
  3. 访问 http://IP:8069/web/database/manager 创建数据库
  4. 创建成功后,改回 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 cronsystemctl 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-net

4.4 验证

  • http://IP → 自动 301 跳转到 https://IP
  • https://IP → 浏览器提示"证书不受信任"(自签证书正常现象),点"继续访问"即可
  • http://IP:8069 → 拒绝连接(Odoo 端口已收敛至 Docker 内网)

⚠️ 踩坑记录:配置 Nginx 后 8069 仍可访问,原因是 docker-compose.yml 中 Odoo 服务还保留着 ports: - "8069:8069"。必须改为 expose,让 Odoo 仅在 Docker 内网暴露。

💡 补充说明:本文使用自签证书,仅适合纯内网、不对外暴露的练习场景。生产环境建议使用 Let's Encrypt 免费证书,避免浏览器"证书不受信任"警告,也避免用户养成"点继续访问"的习惯。申请工具推荐 Certbotacme.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-net

6.4 Grafana 配置

  1. 添加数据源:Connections → Data sources → Prometheus,URL 填 http://prometheus:9090
  2. 导入面板:Dashboards → Import,输入 ID 1860(Node Exporter Full)
  3. 手动选择变量

    • Job = node-exporter
    • Nodename = 容器哈希(见下方说明)
    • Instance = node-exporter:9100

💡 如何找到 Nodename 的值:在 Prometheus 页面(http://IP:9090)的查询框里输入 node_uname_info,点击 Execute。结果里 nodename 标签的值就是面板需要的值。容器环境下它通常是一串容器哈希,不是宿主机名,所以社区面板的默认变量会显示为空,需要手动选。

如果希望面板自动识别,可以把 Node Exporter 的 --path.rootfs 和宿主机 hostname 关联起来,或者在 Prometheus 里用 label_replacenodename 重写为可读名称。这一步属于进阶优化,不影响基本使用。

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$
3DROP DATABASE 被阻塞Odoo 保持 10 个活动连接docker compose stop odoo 切断连接
4pg_dump 报 Permission denied手动执行时变量未定义,文件被写到根目录写成脚本执行,不手动敲带变量的命令
5配置 Nginx 后 8069 仍可访问Odoo 服务仍保留 ports 映射改为 expose,端口仅在内网暴露
6最小化安装缺少工具Ubuntu 默认不带 cron 和 logrotateapt install -y cron logrotate
7logrotate 报 insecure permissions日志目录权限过宽(组可写)chmod 755 收紧目录权限
8cAdvisor 拉取失败gcr.io 国内无法直连换国内镜像源
9cAdvisor 无法识别容器Ubuntu 24.04 Docker 使用 containerd snapshotter改用 Node Exporter
10Prometheus targets 全部 UNKNOWNPrometheus 容器未重建,配置未生效docker compose rm -f 后重建
11Grafana 面板全部 N/A数据源命名冲突,面板绑定了空数据源删除冗余数据源,重命名为标准名
12Node 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+ 小时