Cách phát triển module Odoo trong Docker mà tôi từng dùng suốt các bài đầu loạt này: sửa file trên máy, docker cp vào container, rồi -u module, rồi restart. Lặp mãi rất mệt. Có cách gọn hơn nhiều: bind-mount thư mục addons từ máy host vào container — sửa file trên máy, container thấy ngay, khỏi copy. Ghép với --dev=all, vòng lặp code trở nên mượt mà. Đây là cách dựng một docker-compose.yml cho dev.
Hai kiểu volume: bind-mount vs named volume
Đây là mấu chốt cả bài. Docker có hai kiểu gắn thư mục vào container:
- Named volume (
odoo-data:/var/lib/odoo): Docker tự quản, dữ liệu bền, nhưng bạn không sửa trực tiếp được từ máy host một cách tiện lợi. Hợp cho filestore, database. - Bind-mount (
./addons:/mnt/extra-addons): trỏ thẳng một thư mục trên máy host vào container. Sửa file ở host là container thấy ngay lập tức. Đây chính là thứ ta cần cho code.

Hình 1: docker-compose.yml cho dev. Dòng vàng ./addons:/mnt/extra-addons là bind-mount — thư mục addons/ trên máy bạn xuất hiện trong container tại /mnt/extra-addons (đúng addons_path). command: odoo --dev=all để auto-reload. Còn odoo-data là named volume cho filestore — dữ liệu bền, khỏi soi tay.
Bind-mount hoạt động thật
Để bạn thấy đây không phải lý thuyết, tôi bind-mount một thư mục host chứa module quan_ca_phe vào container, rồi sửa file trên host và đọc lại trong container:

Hình 2: Bằng chứng thật. Container thấy đủ file của module ngay qua bind-mount. Rồi tôi ghi dòng đánh dấu # sua-tren-host-151840 vào file trên host, và container đọc lại được đúng dòng đó — không hề docker cp. Sửa ở host, container thấy tức thì. Ghép với --dev=all: sửa .py là Odoo tự reload, sửa .xml là F5 thấy.
Vài lưu ý dựng compose dev
- Mở cả cổng 8072 (websocket/longpolling): thiếu nó thì thông báo realtime và một số widget không chạy.
odoo.confcũng nên bind-mount (./config:/etc/odoo) để chỉnh tham số mà khỏi vào container.- Quyền file: trên Linux, UID trong container (thường là
odoo) phải đọc được thư mục host. Lỗi quyền làm module "vô hình" như đã nói ở bài addons_path. - Đừng bind-mount
/var/lib/odoo(filestore) ra host — để named volume cho Docker quản, tránh rối quyền và rác.
Vòng lặp dev hoàn chỉnh
# 1. Khởi động stack
docker compose up -d
# 2. Cài module lần đầu (đọc từ thư mục bind-mount)
docker compose exec odoo odoo -d dev -i quan_ca_phe --stop-after-init
# 3. Sửa code trên máy bạn bằng IDE quen thuộc
# - sửa .py → --dev=all tự reload
# - sửa .xml → F5 thấy ngay (nhờ tính năng 'xml' của dev)
# - đổi schema (thêm trường) → chỉ khi đó mới cần -u:
docker compose exec odoo odoo -d dev -u quan_ca_phe --stop-after-init
So với docker cp + restart mỗi lần, đây là trời với vực. Đổi trường/model mới cần -u; còn sửa logic Python hay giao diện XML thì gần như tức thì.
Khác gì compose production?
- Dev bind-mount code để sửa nóng; production đóng code vào image (COPY trong Dockerfile) để bất biến, tái lập được.
- Dev bật
--dev=all; production tuyệt đối không. - Dev một worker (
--workers=0) cho dễ debug; production nhiều worker.
Nhớ ba ý
- Bind-mount (
./addons:/mnt/extra-addons) cho container thấy code trên máy bạn tức thì — tôi đã chứng minh sửa host, container đọc đúng dòng đó, khỏidocker cp. - Ghép với
command: odoo --dev=all: sửa.pytự reload, sửa.xmlF5 thấy; chỉ đổi schema mới cần-u. - Dùng named volume cho filestore (
/var/lib/odoo) và database — bền, khỏi soi tay; nhớ mở cả cổng 8072 cho websocket.
Xong chương môi trường & công cụ (15 bài), ta bước vào lõi lập trình Odoo: ORM. Phần sau mở màn bằng khái niệm nền tảng nhất — recordset: vì sao trong Odoo mọi thứ là tập bản ghi chứ không phải object đơn lẻ.