Odoo 把 CRM、销售、库存、采购、项目、会计和网站等业务模块放进同一套系统。对希望掌握数据、安装自定义模块或控制升级节奏的团队来说,把 Odoo 19 部署在自己的 VPS 上很有吸引力;但它也意味着你要自己负责 PostgreSQL、HTTPS、邮件、备份、安全更新和故障恢复。
这篇教程以 Ubuntu 24.04、Docker Compose、Odoo 19 Community、PostgreSQL 和 Caddy 为例,给出一套适合小团队起步的生产部署。重点不是“把 8069 端口打开就算完成”,而是让数据库不暴露公网、关闭公开数据库列表、备份数据库与 filestore,并给同版本更新和跨大版本升级留出回滚路径。
先确认自建是否真的适合你。三种方式的差异不只是服务器价格。
| 方式 | 主要优点 | 主要代价 | 适合场景 |
|---|---|---|---|
| Odoo Online | 几乎不用维护服务器 | 自定义模块和底层控制受限 | 只用标准功能的小团队 |
| Odoo.sh | 有托管、测试分支和升级流程 | 持续订阅成本较高 | 使用 Enterprise 与定制模块的企业 |
| VPS 自建 Community | 数据和部署节奏可控,可装社区模块 | 安全、备份、邮件和升级都由自己负责 | 有 Linux 运维能力、希望控制成本的团队 |
自建 Community 并不会自动获得 Enterprise 功能。Enterprise 模块与服务需要相应订阅,第三方模块也可能有单独许可证。决定迁移前,应列出正在使用的模块、用户数、邮件需求、合规要求和可接受停机时间,而不是只比较一台 VPS 的月租。
Odoo 的实际资源消耗取决于并发用户、已安装模块、自动任务、报表和附件量。下面是保守的起步建议,不是官方性能承诺:
| 场景 | 建议起步配置 | 说明 |
|---|---|---|
| 测试或 1–3 人 | 2 vCPU、4GB 内存、50GB NVMe | 不建议承载重要生产数据 |
| 5–15 人小团队 | 4 vCPU、8GB 内存、100GB NVMe | 可运行少量 worker、数据库与代理 |
| 20–50 人或报表较多 | 8 vCPU、16GB 内存、200GB+ NVMe | 需要压测、监控并考虑拆分数据库 |
| 大量附件或电商图片 | 计算盘加独立备份/对象存储 | 容量、IOPS 和恢复时间都要计算 |
优先选择 KVM、NVMe、稳定单核性能和可做异机备份的机房。磁盘容量不仅要容纳 PostgreSQL,还要容纳 /var/lib/odoo 中的 filestore、容器镜像、日志和至少一份临时备份。数据库写入延迟明显时,单纯增加 CPU 往往没有帮助。
准备以下内容:
- 一台安装 Ubuntu 24.04 的 VPS;
- 一个解析到 VPS 公网 IP 的域名,例如
erp.example.com; - 已安装 Docker Engine 与 Docker Compose v2;
- TCP 80、443 对公网开放,SSH 只向可信 IP 开放;
- 两组不同的高强度数据库密码,以及一个 Odoo 主密码。
先检查环境:
docker --version
docker compose version
free -h
df -h
sudo ss -lntup | grep -E ':(80|443|8069|8072|5432)\b' || true
生产环境不要把 8069、8072 或 5432 暴露到公网。用户只应通过 Caddy 的 443 端口访问 Odoo。
sudo mkdir -p /opt/odoo19/{config,addons,postgres-init,backups}
sudo chown -R "$USER":"$USER" /opt/odoo19
cd /opt/odoo19
umask 077
创建 .env:
POSTGRES_DB=odoo
POSTGRES_USER=odoo_db_owner
POSTGRES_PASSWORD=替换为数据库所有者强密码
ODOO_DB_USER=odoo_app
ODOO_DB_PASSWORD=替换为不同的应用数据库强密码
密码建议使用密码管理器生成,避免空格、引号和换行,方便 Compose 与备份脚本读取。随后限制权限:
chmod 600 .env
这里把数据库所有者与 Odoo 应用账号分开。Odoo 日常连接使用 odoo_app,它不是 PostgreSQL 超级用户,也没有创建数据库的权限。
创建 postgres-init/01-odoo-role.sh:
#!/bin/sh
set -eu
psql -v ON_ERROR_STOP=1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" \
--set=odoo_password="$ODOO_DB_PASSWORD" <<'SQL'
CREATE ROLE odoo_app
LOGIN
PASSWORD :'odoo_password'
NOSUPERUSER
NOCREATEDB
NOCREATEROLE;
GRANT CONNECT ON DATABASE odoo TO odoo_app;
GRANT USAGE, CREATE ON SCHEMA public TO odoo_app;
SQL
chmod 700 postgres-init/01-odoo-role.sh
该脚本只在 PostgreSQL 数据目录第一次初始化时运行。已有数据库卷不会重复执行;不要为了重跑初始化脚本而随意删除生产卷。
创建 config/odoo.conf:
[options]
admin_passwd = 替换为独立的Odoo主密码
db_host = db
db_port = 5432
db_name = odoo
dbfilter = ^odoo$
list_db = False
proxy_mode = True
workers = 3
max_cron_threads = 1
gevent_port = 8072
limit_time_cpu = 600
limit_time_real = 1200
limit_memory_soft = 1610612736
limit_memory_hard = 2147483648
addons_path = /usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons
data_dir = /var/lib/odoo
admin_passwd 是数据库管理主密码,不是普通管理员登录密码。list_db=False 与固定的 db_name、dbfilter 可以避免公网枚举数据库。proxy_mode=True 只应在 Odoo 确实位于可信反向代理后方时启用。
3 个 worker 适合作为 4 vCPU、8GB VPS 的起步值,但不能代替压测。worker 越多,内存上限越需要按实际业务调整。配置完成后执行:
chmod 600 config/odoo.conf
mkdir -p addons
第三方模块放进 addons 前,应检查它是否明确支持 Odoo 19,并在测试数据库安装。来源不明的模块等同于把任意代码放进 ERP 服务器执行。
创建 compose.yaml:
name: odoo19
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ODOO_DB_PASSWORD: ${ODOO_DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./postgres-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- backend
odoo:
image: odoo:19.0-20260908
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
HOST: db
PORT: 5432
USER: ${ODOO_DB_USER}
PASSWORD: ${ODOO_DB_PASSWORD}
command: ["odoo", "-c", "/etc/odoo/odoo.conf"]
volumes:
- odoo-data:/var/lib/odoo
- ./config:/etc/odoo:ro
- ./addons:/mnt/extra-addons
expose:
- "8069"
- "8072"
networks:
- frontend
- backend
caddy:
image: caddy:2.10-alpine
restart: unless-stopped
depends_on:
- odoo
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks:
- frontend
networks:
frontend:
backend:
internal: true
volumes:
postgres-data:
odoo-data:
caddy-data:
caddy-config:
这里使用当前官方镜像页面提供的 Odoo 19 日期标签,避免部署时无意跨版本;标签会随时间变化,正式上线前应在官方镜像页重新确认并记录镜像 digest。PostgreSQL 只连接内部网络,没有 ports,Odoo 也只向 Caddy 暴露服务。
先展开 Compose,确认变量已经替换且没有误把空值写入:
docker compose config > /tmp/odoo19-compose.resolved.yaml
grep -nE 'image:|POSTGRES_DB:|POSTGRES_USER:|HOST:|USER:' \
/tmp/odoo19-compose.resolved.yaml
不要把解析后的文件提交到 Git 或发给别人,因为其中包含密码。
创建 Caddyfile,把域名换成你的真实域名:
erp.example.com {
encode zstd gzip
@websocket path /websocket /websocket/*
reverse_proxy @websocket odoo:8072
reverse_proxy odoo:8069
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /data/access.log {
roll_size 50MiB
roll_keep 5
}
}
}
Odoo 官方要求面向公网的部署使用 HTTPS,并在反向代理后启用代理模式。实时聊天等功能会使用 /websocket/,因此不能只代理普通的 8069 请求。
先只启动数据库:
docker compose pull
docker compose up -d db
docker compose ps
docker compose logs --tail=100 db
确认 health 状态为 healthy 后,在公网代理尚未启动时初始化 Odoo 基础模块:
docker compose run --rm odoo \
odoo -c /etc/odoo/odoo.conf \
-d odoo -i base --without-demo=all --stop-after-init
CLI 初始化会创建默认管理员。不要带着默认密码上线,立即在本地终端设置新的登录名和随机密码:
read -rsp "新的 Odoo 管理员密码: " ODOO_ADMIN_PASSWORD; echo
docker compose run --rm -T \
-e ODOO_ADMIN_PASSWORD="$ODOO_ADMIN_PASSWORD" \
odoo odoo shell -c /etc/odoo/odoo.conf -d odoo <<'PY'
import os
admin = env.ref('base.user_admin')
admin.write({
'login': '[email protected]',
'password': os.environ['ODOO_ADMIN_PASSWORD'],
})
env.cr.commit()
PY
unset ODOO_ADMIN_PASSWORD
把 [email protected] 换成仅管理员知道的登录名。完成后再启动全部服务:
docker compose up -d
docker compose ps
docker compose logs --tail=100 odoo caddy
访问 https://erp.example.com。首次登录后配置公司、时区和邮件,但不要急着安装所有模块;模块会改变数据库结构、后台任务和资源占用。
如果使用 UFW:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
sudo ufw status verbose
从另一台网络环境检查:
curl -I https://erp.example.com
curl -I https://erp.example.com/web/database/manager
首页应返回 200 或合理的登录跳转。数据库管理器不应列出数据库;如果还能公开创建、删除或复制数据库,先检查 list_db=False、db_name、dbfilter 与容器实际读取的配置文件。
Odoo 的邀请、密码重置、报价单和通知依赖邮件。许多 VPS 默认限制出站 25 端口,直接在同一台服务器自建邮件系统还会遇到 PTR、SPF、DKIM、DMARC 和 IP 信誉问题。
小团队更适合使用可靠 SMTP 服务,在 Odoo 的“设置 → 技术 → 出站邮件服务器”中配置 587/STARTTLS 或 465/TLS,并发送真实测试邮件。不要只看“连接测试成功”,还要检查收件箱、垃圾箱与退信日志。
Odoo 的业务记录在 PostgreSQL,附件和二进制文件通常在 /var/lib/odoo/filestore/。只备份其中一个,恢复后就可能出现记录存在但附件丢失,或文件存在却无法对应数据库的问题。
下面是一种停写窗口内的保守备份方法:
cd /opt/odoo19
set -a
. ./.env
set +a
STAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="$PWD/backups/$STAMP"
mkdir -p "$BACKUP_DIR"
docker compose stop odoo
docker compose exec -T db \
pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc \
> "$BACKUP_DIR/odoo.dump"
docker run --rm \
-v odoo19_odoo-data:/source:ro \
-v "$BACKUP_DIR":/backup \
alpine:3.22 \
tar -czf /backup/odoo-data.tar.gz -C /source .
tar -czf "$BACKUP_DIR/odoo-config.tar.gz" \
compose.yaml Caddyfile config addons postgres-init
sha256sum "$BACKUP_DIR"/* > "$BACKUP_DIR/SHA256SUMS"
docker compose start odoo
备份目录必须再同步到另一台服务器或对象存储,并设置保留周期。.env 与 admin_passwd 属于敏感配置,应单独加密;不要把未加密副本与公开下载链接放在一起。更系统的流程可参考 VPS 备份恢复演练指南。
至少每季度在隔离环境做一次恢复演练,验证以下项目:
pg_restore --list能读取数据库备份;- filestore 与数据库来自同一备份窗口;
- 登录、附件下载、图片、邮件模板和关键业务模块正常;
- 自定义模块的代码版本与数据库一致;
- 实际恢复时间不超过业务允许的 RTO。
Odoo 官方区分 update 与 upgrade:
- 同一 19.0 版本内更新:获取较新的修复和安全更新,通常可以通过更新镜像完成;
- 从 18 升到 19 等大版本升级:数据库结构与模块都需要迁移,不能只把镜像标签改成 19。
同版本更新建议先在测试副本执行:
docker compose pull odoo
docker compose up -d odoo
docker compose logs -f --tail=200 odoo
执行前记录旧镜像 digest,并创建数据库、filestore 与配置的完整备份。跨大版本升级则应先申请或制作测试升级数据库,逐个验证官方模块和自定义模块,确认后再安排维护窗口。不要让自动更新工具在无人值守时替换 Odoo 主版本镜像。
最低限度应监控:
- VPS 内存、swap、磁盘空间、inode 与磁盘延迟;
- PostgreSQL 连接数、慢查询、数据库大小和备份结果;
- Odoo 进程重启次数、HTTP 5xx、请求延迟和 cron 失败;
- Caddy 证书续期与访问日志异常;
- 443 可用性、登录页响应和邮件发送结果。
如果容器不断重启,先执行 docker compose ps 和 docker compose logs --tail=200 odoo db,不要立即删除卷。磁盘增长异常时,可结合 Docker 占满磁盘排查指南,但不要运行会误删 Odoo 数据卷的清理命令。
检查数据库 health 状态、应用账号、密码与内部 DNS:
docker compose ps
docker compose logs --tail=200 db odoo
docker compose exec db pg_isready -U odoo_db_owner -d odoo
docker compose exec db psql -U odoo_db_owner -d odoo -c '\du'
确认 Odoo 使用 odoo_app,而不是误用数据库所有者。不要为解决连接失败而开放公网 5432;继续参考 PostgreSQL 连接不上排查指南。
先区分是 Odoo 未启动、数据库迁移失败、内存不足,还是 Caddy 无法连接 8069:
docker compose logs --tail=200 caddy odoo
docker compose exec caddy wget -S -O- http://odoo:8069/web/login
docker stats --no-stream
若 Odoo 进程被 OOM Kill,提高内存或减少 worker,而不是无限增加重启次数。通用代理问题可以参照 VPS 502/504 排查清单。
检查浏览器是否始终访问 HTTPS、proxy_mode=True 是否生效、系统时间是否正确,以及反向代理有没有改写 Cookie 或协议头。不要同时套多层代理规则后再猜是哪一层造成问题。
检查 /websocket 是否被代理到 8072,并查看 worker 日志。普通页面可访问不代表 WebSocket 路由正确。修改 Caddyfile 后先运行:
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
回到安装前的数据库与 filestore 快照,在测试环境确认模块支持 Odoo 19、Python 依赖完整且数据库迁移成功。不要直接删除报错模块目录:数据库中可能已经记录它处于已安装或升级中状态。
- 只有 22、80、443 按计划暴露公网,5432/8069/8072 未暴露;
- Odoo 使用非超级用户、无 CREATEDB 权限的 PostgreSQL 账号;
-
list_db=False、固定db_name和dbfilter已生效; - 默认管理员密码与
admin_passwd已分别更换; - HTTPS、WebSocket、SMTP 和密码重置邮件均实测通过;
- 数据库、filestore、配置和自定义模块进入异机备份;
- 在隔离环境完成至少一次恢复演练;
- 镜像标签和 digest 已记录,升级前有测试与回滚方案;
- 磁盘、5xx、容器重启和备份失败已经配置告警。
在 VPS 上自建 Odoo 19 的核心并不是一条 docker compose up -d,而是把应用、数据库、反向代理、邮件、附件和备份当成一套系统管理。对小团队而言,4 vCPU、8GB 内存可以作为谨慎的起点;实际容量仍应根据 worker 内存、并发和报表负载逐步调整。
上线前最重要的三件事是:让 PostgreSQL 与 Odoo 端口留在内部网络,关闭公开数据库管理入口,并验证数据库与 filestore 能从同一备份窗口恢复。如果团队没有能力持续完成这些工作,Odoo Online、Odoo.sh 或有运维支持的托管方案通常比一台更便宜的 VPS 更合适。
