简介:这是一份围绕 Odoo SaaS Kit 的 PDF 技术文档,重点讲解基于 Docker 的多租户 Odoo 实例管理系统,面向已有服务器管理和 Odoo 使用经验、负责企业级应用部署与运维的 IT 专业人员。文档覆盖部署链路:安装 docker、erppeek、paramiko 等 Python 库,规划 Odoo-SAAS-Data、docker_vhosts、common_addons 目录,构建基础 Odoo Docker 镜像,配置 Nginx 虚拟主机与 PostgreSQL,并为 Odoo 用户授权 Docker 与 Nginx 控制权限。随后说明如何配置 SaaS 服务器、创建订阅计划,使客户自助生成实例,同时设置计费周期、试用期与模块列表;还特别提醒构建镜像时须保持主机与容器内 Odoo 用户 ID 和组 ID 一致,避免共享文件夹权限问题。针对实际运维,文档整理模块上传、域名管理、日志查看、备份恢复和重启客户端进程等常见问题解法,便于快速排障。资源包共 1 个 PDF 文件,大小 3.99MB,适合作为部署参考与运维手册。目前已有 140 人浏览学习,适合有 Docker 与 Nginx 基础的技术人员直接借鉴。
1. Odoo SaaS Kit:为什么多租户实例管理是Odoo商业化绕不开的一道坎
做Odoo实施的团队迟早会遇到同一个问题:客户从3家涨到30家,不可能给每家单独买一台服务器,也不可能在一套Odoo里塞三十个互不相干的业务库。Odoo SaaS Kit这个方向解决的就是这件事——用Docker把每个租户的Odoo实例、数据库、资源配额管起来,做到开新客户像开账号一样快。它不是某个插件,而是一整套“部署+配置+管理”的方案:容器编排管实例生命周期,PostgreSQL隔离租户数据,反向代理按域名分发请求,再加一套运维流程做备份、升级和扩容。适合正在做Odoo实施交付的顾问、小团队SaaS创业者,以及要给客户提供Odoo订阅服务的技术负责人。
2. 先搭地基:Odoo SaaS Kit 的架构选型与Docker最小拓扑
2.1 多租户的三种隔离方式:为什么最终选“一实例一库一容器组”
Odoo做多租户,隔离方式大致有三条路:共享数据库共享Schema、共享数据库独立Schema、独立数据库。第一条路在Odoo里基本走不通,因为Odoo的模块安装在数据库级别,不同客户装了不同模块之后,公共表结构根本没法统一。第二条路要改造ORM层,动到Odoo框架底子,风险大且升级时每次都要打补丁,不适合长期维护。
SaaS Kit走的是第三条路:每个租户一个独立PostgreSQL数据库,前端可以共用一套Odoo容器,也可以每个租户独立容器组。新手阶段建议从“共用Odoo容器+独立租户库”起步,成本和运维量最低;等某个租户的负载明显影响其他人时,再把这个租户拆成独立容器组。这套方案依赖Odoo官方就支持的多数据库机制:PostgreSQL里建了库,Odoo通过db_filter参数自动识别可用的租户库,不需要改Odoo源码。隔离性、升级灵活性、交付速度三个维度上,它都是最稳的折中。
2.2 最小容器拓扑里每个角色的职责与关键参数
一套能跑起来的SaaS Kit最小拓扑是四个容器:PostgreSQL、Odoo、Redis、Nginx。数据库负责租户数据隔离,Odoo负责应用逻辑,Redis在Odoo里做session存储和缓存,Nginx负责按域名把请求路由到Odoo。四个容器放在同一个自定义bridge网络里,互相通过容器名通信。
| 容器角色 | 镜像 | 关键端口 | 职责 |
|---|---|---|---|
| db | postgres:16 | 5432 | 每租户一个独立database |
| odoo | odoo:20 | 8069 | 处理HTTP请求与业务逻辑 |
| redis | redis:7-alpine | 6379 | session存储、缓存 |
| nginx | nginx:1.27-alpine | 80/443 | 域名路由、TLS终止、WebSocket转发 |
Odoo容器里有几个参数直接影响多租户行为,必须在compose里显式声明。db_filter告诉Odoo哪些数据库属于租户,常见的做法是设为^odoo_,只匹配以odoo_开头的库,避免把PostgreSQL里其他管理库暴露到前端。list_db=false关闭数据库列表展示,防止用户在登录页看到全部租户库名。proxy_mode=true让Odoo信任Nginx转发过来的HTTP头,否则生成的回链URL会带上内网端口。workers数量一般按CPU核数*2+1估算,max_cron_threads保留1个线程跑定时任务,多租户场景下cron线程太多会互相抢数据库连接。
2.3 版本锁定:为什么从Odoo 20和PostgreSQL 16起步
镜像tag选择上,我的建议是直接锁定Odoo 20和PostgreSQL 16,不要用latest。latest在Docker里是个黑匣子,今天拉下来能用,三个月后再拉可能就是新版本,升级引发的兼容问题排查起来非常被动。Odoo 20是目前社区活跃度最高的版本线,Docker镜像、第三方模块适配都齐全,Python版本和依赖在镜像里已经固定,省去不少手工编译的麻烦。
PostgreSQL 16对Odoo 20的支持成熟,还有一个实际原因:备份恢复工具pg_dump/pg_restore在大版本之间不能保证向下兼容。如果线上是PostgreSQL 16,恢复演练和灾备环境也必须是16,锁死版本能避免“生产好好的,备份恢复不回去”的血泪事故。Redis用7-alpine即可,Odoo对Redis版本不敏感,但alpine镜像体积小、内存占用低,适合容器密集部署。最后一个建议:统一从私有镜像仓库拉取这三个镜像,把版本号写进compose文件,不要在日常操作里手动docker pull覆盖tag。
3. 部署落地:用docker compose跑起第一套多租户Odoo
3.1 第一版docker-compose.yml:先接受“一容器一库”的简单拓扑
先写第一版compose文件,不追求完美,目标是让db、odoo、redis、nginx四个容器一次起来,并且能通过db_filter识别租户库。后续要拆独立租户容器组时再扩展,但架构骨架不变。
version: "3.8" services: db: image: postgres:16 container_name: saas_db environment: POSTGRES_USER: odoo POSTGRES_PASSWORD: odoo_password POSTGRES_DB: postgres volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U odoo"] interval: 10s timeout: 5s retries: 5 networks: - saas_net odoo: image: odoo:20 container_name: saas_odoo depends_on: db: condition: service_healthy environment: HOST: db USER: odoo PASSWORD: odoo_password DB_FILTER: "^odoo_" LIST_DB: "false" PROXY_MODE: "true" WORKERS: "5" MAX_CRON_THREADS: "1" volumes: - odoo_data:/var/lib/odoo - ./addons:/mnt/extra-addons networks: - saas_net redis: image: redis:7-alpine container_name: saas_redis command: ["redis-server", "--appendonly", "yes"] networks: - saas_net nginx: image: nginx:1.27-alpine container_name: saas_nginx depends_on: - odoo ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./certs:/etc/nginx/certs:ro networks: - saas_net volumes: db_data: odoo_data: networks: saas_net: driver: bridge这段compose的逻辑是:db容器先启动,healthcheck通过后odoo容器才会创建,这套依赖机制在compose里用condition: service_healthy实现。DB_FILTER: "^odoo_"是关键的正则,Odoo启动时会扫描PostgreSQL里所有数据库,只把以odoo_开头的库当作租户实例。LIST_DB: "false"对应Odoo配置里的list_db = False,登录页不再展示数据库下拉框。PROXY_MODE: "true"对应proxy_mode = True,后面Nginx转发时才不会出URL端口错误。
workers数量这里写了5,适合2核4G的初始机器。workers不是越大越好,每个worker都持有数据库连接池,PostgreSQL默认max_connections只有100,workers太多反而拖垮数据库,这是第一次部署时最容易忽略的容量关系。
3.2 初始化租户数据库并让Odoo识别多库
compose文件写好后,先启动数据库容器,确认健康状态再启动Odoo,然后手动创建第一个租户库。不要用docker compose up -d一把梭,第一次跑先拆开做,方便定位是数据库没起来还是Odoo连接失败。
docker compose up -d db docker compose exec db pg_isready -U odoo docker compose exec db createdb -U odoo -O odoo odoo_tenant_001 docker compose up -d第一行只启动db容器,第二行用pg_isready探测PostgreSQL是否接受连接。返回accepting connections说明就绪,再执行第三行创建租户库。-U odoo指定用户,-O odoo把新库的属主设为odoo用户,这一步不能漏,否则Odoo用odoo用户登录后对这个库没有完整权限,运行时会报permission denied。第四行把odoo、redis、nginx一起拉起来,Odoo启动时读到DB_FILTER,自动把odoo_tenant_001识别为可访问的租户库。
打开浏览器访问http://服务器IP/web/database/selector,如果配置生效,页面不会列出数据库列表,需要手动输入租户库名odoo_tenant_001才能进入初始化安装界面。看到这个行为说明多租户入口已经通了。首次进入会在租户库里初始化Odoo基础模块,耗时一两分钟,耐心等页面跳转。
3.3 Nginx反向代理与租户域名路由
Odoo本身不处理域名路由,它只按数据库名分发请求,域名到租户库的映射要Nginx来做。最常见的方案是通配符解析:每个租户一个二级域名,结构是tenant001.yourdomain.com,DNS里加一条*.yourdomain.com到服务器IP的A记录,Nginx用一个server块接住所有租户请求,转发给Odoo容器。
upstream odoo_backend { server saas_odoo:8069; keepalive 64; } server { listen 80; server_name *.yourdomain.com; proxy_read_timeout 720s; proxy_connect_timeout 10s; proxy_send_timeout 720s; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; location / { proxy_pass http://odoo_backend; } }关键在两处:server_name *.yourdomain.com匹配所有租户子域,proxy_pass http://odoo_backend把请求转给compose网络里的odoo容器。Odoo拿到请求后根据Host头里的子域前缀,结合dbfilter正则匹配对应租户库,这就是为什么dbfilter必须用^odoo_开头来匹配数据库名的原因——tenant001.yourdomain.com需要和库名odoo_tenant_001在命名上强对应。
WebSocket要单独处理,Odoo的实时通信(比如消息通知)走WebSocket协议,Nginx默认转发不了。需要先定义map $http_upgrade $connection_upgrade { default upgrade; '' close; }这段映射,放到http块里,再在server块里带上Upgrade和Connection头。不带这段,Odoo后台的在线用户列表和聊天功能会偶发断连,报错日志里能看到websocket connection failed。
4. 实例管理打通:租户生命周期、备份恢复与资源配额
4.1 租户生命周期管理的常规操作序列
SaaS Kit的管理对象是租户实例,实例管理的第一步是把生命周期标准化:创建、暂停、迁移、归档、销毁,每个动作对应一组确定的命令。不要手动去容器里乱改,要把命令沉淀成脚本,否则租户一多必然出错。
# 创建租户 docker compose exec db createdb -U odoo -O odoo odoo_tenant_002 # 暂停租户(停止Odoo侧服务,保留数据) docker compose stop odoo # 恢复服务 docker compose start odoo # 归档租户(导出数据并下线) docker compose exec db pg_dump -U odoo -Fc odoo_tenant_002 > backups/odoo_tenant_002.dump docker compose exec db dropdb -U odoo odoo_tenant_002创建租户其实只做一件事:建一个空数据库。Odoo会在首次访问时自动完成模块初始化,不需要事先安装任何东西。暂停和恢复针对整个Odoo容器操作,这在一容器多库的架构下是硬伤——停一个租户所有人都访问不了。所以暂停操作更适合用在“整体维护窗口”场景,单独的租户停用要通过nginx层做路由拦截或者用数据库级权限控制,不能简单stop Odoo容器。
归档操作要特别注意顺序:先pg_dump导出,确认dump文件大小合理,再dropdb删除库。顺序反了就得从备份恢复,多租户系统里没有后悔药吃。dropdb之后dbfilter正则自动不再匹配这个库,租户的域名访问会直接404,属于安全的离线状态。
4.2 备份与恢复:多租户下恢复比备份难十倍
单机单库的备份很简单,多租户场景下难点在恢复:不能把整个PostgreSQL目录拷贝覆盖,那会把所有租户一起回滚;也不能在恢复时让dbfilter误匹配到半初始化状态的库。我的建议是采用租户级备份策略:每个租户库独立导出,独立恢复,互不影响。
# 备份单个租户(压缩格式,支持选择性恢复) docker compose exec db pg_dump -U odoo -Fc odoo_tenant_001 > backups/odoo_tenant_001_$(date +%F).dump # 恢复单个租户到新库 docker compose exec db createdb -U odoo -O odoo odoo_tenant_001_restored docker compose exec db pg_restore -U odoo -d odoo_tenant_001_restored --no-owner --no-privileges backups/odoo_tenant_001_20250601.dump恢复时务必用--no-owner --no-privileges,否则dump里的属主信息会和当前容器里的odoo用户不一致,Odoo连上去后经常报权限错误。恢复完成后,新库名如果不符合dbfilter正则,Odoo不会识别它,这时把容器里的DB_FILTER临时改成正则匹配新库名,或者直接在Nginx层加一条测试域名指过去,验证数据完整后再把旧库切换走。
cron定时备份的脚本建议分成两层:每晚全量备份所有租户库,每小时只对变更最频繁的两三个租户做增量。PostgreSQL没有内置增量,常见的做法是结合WAL归档,但对SaaS Kit这种规模来说太重。实际够用的方案是:夜间cron跑全套pg_dump,白天每两小时对活跃租户单独跑一次轻量dump,保留最近7天备份。恢复演练每月至少做一次,不要等真出事了才试。
4.3 容器资源配额与SaaS套餐定价的映射
多租户系统管理到后期,最大的问题不是功能,是资源分配。一个租户写死一个Odoo容器不现实(资源浪费严重),但共用容器又没法控制单个租户的CPU和内存占用。SaaS Kit的常见做法是在容器编排层给租户定义配额模板,用compose里的mem_limit和cpus字段控制。
# 示例:独立租户容器组的资源配额 services: odoo_tenant_001: image: odoo:20 mem_limit: 2g cpus: 1.0 environment: WORKERS: "3"先把配额模板和服务套餐绑定:基础版1G内存1核,专业版2G内存2核,旗舰版4G内存4核。Odoo的workers数量跟随内存配额走,1G内存最多配3个workers,再高数据库连接池会先撑不住。这个映射关系要在交付文档里写明,否则销售签了高并发客户,部署时内存配额给不够,后续全是性能投诉。
docker update可以在容器运行中动态调整配额,比如某个租户月底结账时CPU飙高,临时docker update --cpus 2 --mem_limit 4g saas_odoo_tenant_001,结完账再调回来。调整后观察容器重启情况,内存配额调低到当前占用以下,容器会被内核直接杀掉,这个操作必须谨慎。quota和计费系统联动时,建议按“配额阶梯计费”而不是“实际消耗计费”,否则AWS账单波动会让用户投诉到怀疑人生,SaaS套餐的费用策略里这是最稳的一种设计。
5. 避坑清单:Docker部署Odoo SaaS Kit的常见问题与排查路径
5.1 现象:Docker Desktop启动失败,提示virtualization support not detected
Windows环境跑Docker Desktop最常撞上的错误就是启动时弹出virtualization support not detected。原因是BIOS里的虚拟化技术没开,或者Windows的虚拟机平台功能没启用。
解决路径:重启进BIOS/UEFI,找到Intel VT-x或AMD-V选项并开启;然后在Windows功能里勾选“适用于Linux的Windows子系统”和“虚拟机平台”,执行bcdedit /set hypervisorlaunchtype auto后重启。启动不了Docker Desktop,后面所有compose操作都无法进行。Linux服务器上如果遇到docker: failed to start daemon,先查内核模块lsmod | grep overlay和cgroup挂载,大多数是内核太旧或系统盘空间不足导致dockerd启动失败。
5.2 现象:连接不上Docker引擎,报failed to connect to the docker api
failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine这个错误在Windows和Mac上都很常见。表面意思是Docker客户端连不上引擎,实际是Docker Desktop启动到一半卡住了,或者引擎在后台崩溃。
解决路径:先打开Docker Desktop界面看引擎状态图标,是红色说明引擎没起来,点Restart;如果Restart无效,执行wsl --shutdown关掉WSL虚拟机再重开Docker Desktop。Windows服务里重启com.docker.service也行。最直接的办法是查Docker Desktop日志,路径在%LOCALAPPDATA%\Docker\log\,看到EOF或connection reset基本是WSL2内核问题,执行wsl --update更新内核后解决。
5.3 现象:容器间网络不通,Odoo连接PostgreSQL失败
compose文件部署时一切正常,一旦有人手动docker run补充容器,就会出现Odoo容器连不上db容器的情况。原因很简单:docker run默认加入的是bridge网络,而compose创建的是自定义网络saas_net,两个网络的容器无法通过容器名互相解析。
解决路径:所有补充容器都加--network saas_net参数,或者把已有容器连进去:docker network connect saas_net <container_name>。排查时用docker network inspect saas_net查看节点列表,确认目标容器在不在网络里。另一个常见故障是容器网络模式设置了network_mode: host,这会导致compose网络无法管理和隔离流量,SaaS Kit多租户场景下不建议使用host网络。
5.4 现象:docker pull odoo镜像下载慢,卡在等待层数据
多租户系统一旦跑起来,扩容时最不想遇到的就是拉镜像卡死。odoo官方镜像体积不小,层数多,国内网络环境下直接拉官方仓库经常几KB每秒。
解决路径:配置registry mirror是第一步。在/etc/docker/daemon.json里加"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"],重启docker daemon生效。如果公司网络有缓存代理,用registry-cache做内网镜像仓库,20个租户的服务器都从内网仓库拉取,速度质变。切记不要在业务高峰期docker pull大镜像,这会挤占容器网络带宽,出现整组服务响应变慢的情况。实在拉不下来的镜像,换一台网络通畅的机器拉好再docker save成tar包,scp过去docker load,这是最土但最有效的后悔药。
5.5 现象:所有租户域名都进入了同一个Odoo实例
多个租户域名配好后,发现访问tenant002.yourdomain.com却打开了tenant001的登录页,或者干脆提示数据库不存在。这个坑十有八九是db_filter和域名路由失配。Odoo的多租户识别链路是Nginx按Host转发到Odoo,Odoo再用Host子域前缀去匹配数据库名。任何一个环节的名字不对,都会指向错误的实例。
解决路径:确认数据库名和子域名的映射关系。tenant002.yourdomain.com对应的库名必须是odoo_tenant_002,dbfilter正则写成^odoo_才能匹配上。再看Nginx配置里有没有多个server块互相抢流量,server_name *.yourdomain.com和server_name tenant002.yourdomain.com同时存在时,Nginx按精确优先规则匹配,但很多人把精确域名写在通配前面,导致所有请求都走精确匹配。最后检查Odoo容器环境变量,DB_FILTER有没有被后面加载的配置文件覆盖,docker compose exec odoo env | grep DB_FILTER直接看运行环境,结果最可信。
6. 进阶验证:把SaaS Kit从“能跑”推到“敢上线”
6.1 上线前要过的三关:恢复演练、并发压测、版本升级
第一关是备份恢复演练,选一个非生产租户库,导出dump再恢复到全新库,全程记录耗时。多租户系统最容易翻车的就是灾备环节——备份天天跑,恢复没人试过。第二关是并发压测,用ab -n 1000 -c 20 http://localhost/观察Odoo在20并发下的响应时间和错误率,如果p95超过2秒,先调workers数量,再检查PostgreSQL的shared_buffers。第三关是版本升级演练,把odoo镜像tag从20升到下一个版本前,先在测试环境完整跑一遍odoo-bin -u all -d odoo_tenant_001,确认模块迁移没有破坏性变更。
6.2 用两条命令把多租户状态纳入日常巡检
日常巡检不一定要上监控系统,两条命令足够发现90%的问题。docker stats --no-stream看所有容器的CPU和内存占用,哪个租户容器内存持续飙到配额上限,就该考虑拆分或升级套餐了。docker logs --since 30m saas_odoo | grep -E "CRITICAL|ERROR"看近半小时的应用错误日志,Odoo的ERROR日志通常会带租户库名,能直接定位是哪个实例出了问题。
我第一次上线SaaS Kit时最怕的不是某个容器挂了,而是备份恢复没人演练过,结果第一个月就有一个租户误删数据,靠pg_dump恢复到十分钟前,才意识到这套方案的设计核心从来不是容器编排多花哨,而是数据兜底够不够稳。后来我把备份恢复脚本挂进cron,每月自动邮件通知演练结果,再也没有因为人为误操作失眠过。做多租户Odoo,先敬畏数据,再追求效率,希望帮到你。
本文还有配套的精品资源,点击获取