SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-28

Cài Mealie trên VPS bằng Docker Compose

Tự host Mealie trên VPS với Docker Compose: dán link công thức để lấy bước nấu sạch, kèm meal plan, shopping list, nginx, TLS và backup.

Chức năng của một recipe manager tự host

Một recipe manager tự host lưu các công thức nấu ăn trong database trên server do bạn sở hữu. Mealie là lựa chọn mà hầu hết các hộ gia đình dùng. Bạn dán địa chỉ của một trang công thức nấu ăn, Mealie sẽ đọc nguyên liệu, các bước thực hiện, khẩu phần và thời gian nấu từ trang đó, rồi bỏ qua phần câu chuyện và quảng cáo. Thứ được lưu vào bộ sưu tập của bạn là công thức món ăn.

Phần còn lại của app khá gọn. Có một kế hoạch bữa ăn hằng tuần để bạn kéo các công thức vào, cùng một shopping list được tạo từ kế hoạch đó. Mỗi người nấu ăn có một login riêng. Toàn bộ app chạy trong một container và gần như idle giữa các request, nên một VPS cấu hình vừa phải có thể chạy app mà không bị ảnh hưởng đáng kể.

Hướng dẫn này dùng Docker Compose. Nếu các thuật ngữ services: và volumes: còn mới với bạn, trước tiên hãy đọc cách tổ chức file Docker Compose, vì toàn bộ phần bên dưới chỉ gồm một compose file và bốn lệnh.

Cài đặt Mealie bằng Docker Compose

Mealie phát hành image lên GitHub container registry. Tính đến tháng 7 năm 2026, tag stable hiện tại là v3.22.0. Hãy ghim một version thay vì dùng latest: với latest, một docker compose pull vào một ngày không liên quan có thể đưa bạn sang một database migration mà bạn chưa sẵn sàng xử lý.

sudo mkdir -p /srv/mealie
cd /srv/mealie
sudo nano docker-compose.yml
services:
  mealie:
    image: ghcr.io/mealie-recipes/mealie:v3.22.0
    container_name: mealie
    restart: always
    ports:
      - "127.0.0.1:9925:9000"
    deploy:
      resources:
        limits:
          memory: 1000M
    volumes:
      - mealie-data:/app/data/
    environment:
      ALLOW_SIGNUP: "false"
      PUID: 1000
      PGID: 1000
      TZ: Europe/Amsterdam
      BASE_URL: https://recipes.example.com

volumes:
  mealie-data:

Trước khi khởi động, có 2 dòng cần kiểm tra.

Port được ghi là 127.0.0.1:9925:9000, không phải 9925:9000. Container lắng nghe trên port 9000 ở bên trong, còn host map port 9925 vào đó. Bind map này vào địa chỉ loopback giúp nginx truy cập được Mealie nhưng Internet không thể truy cập. Docker tự ghi rule vào packet filter, nên một 9925:9000 thông thường vẫn có thể truy cập từ bên ngoài ngay cả khi firewall báo port đó đã bị đóng. Cần hiểu rõ điểm bất ngờ này một lần: xem vì sao port Docker đã publish bỏ qua ufw.

BASE_URL phải là địa chỉ public chính xác mà bạn sẽ sử dụng, có scheme và không có dấu gạch chéo ở cuối. Mealie dùng địa chỉ này để tạo link reset password và link invitation. Nếu đặt thành http://localhost:9925, invitation bạn gửi cho partner sẽ chứa một link chỉ hoạt động trên chính server đó.

Khởi động ứng dụng và theo dõi lần boot đầu tiên.

sudo docker compose up -d
sudo docker compose logs -f mealie

Lần khởi động đầu tiên sẽ tạo SQLite database và chạy các migration. Quá trình này mất vài giây. Khi log ổn định và không còn in các dòng migration, hãy kiểm tra app locally.

curl -I http://127.0.0.1:9925

Một 200 OK nghĩa là app đã hoạt động. Connection refused nghĩa là container không chạy: chạy sudo docker compose ps và đọc exit code. Container dừng với code 137 là do bị kill vì vượt quá giới hạn bộ nhớ 1000M. Tình trạng này thường xảy ra trên các plan nhỏ nhất.

Đăng nhập lần đầu và tắt đăng ký mở

Tài khoản mặc định là changeme@example.com với mật khẩu MyPassword. Hãy đăng nhập bằng tài khoản này rồi đổi cả hai ngay, vì cặp thông tin này được in trong tài liệu và do đó có mặt trong mọi scanner.

ALLOW_SIGNUP: "false" trong file compose là có chủ đích. Khi bật đăng ký mở, bất kỳ ai tìm được địa chỉ đều có thể tạo tài khoản trong kho công thức của bạn. Khi tắt đăng ký, bạn thêm người dùng trong khu vực quản trị. Mealie sẽ tạo một invitation link để bạn tự gửi cho họ. Link này được tạo từ BASE_URL, nên giá trị đó rất quan trọng. Nếu bạn chạy nhiều app trên cùng một server và muốn dùng một password cho tất cả, Mealie có thể chuyển việc đăng nhập cho một identity provider bên ngoài như một instance Authentik tự host.

Mealie gom người dùng vào một household. Mọi người trong cùng một household dùng chung bộ sưu tập công thức, meal plan và shopping list. Đây là cách phù hợp với một gia đình. Các household khác nhau trên cùng một server vẫn giữ bộ sưu tập riêng. Cách này phù hợp với nhà ở ghép khi không ai thống nhất được việc có dùng cá cơm hay không.

Trình import là lý do để chạy ứng dụng này

Mở bộ sưu tập công thức, chọn tạo công thức từ URL rồi dán một liên kết vào. Mealie tải trang về và tìm dữ liệu công thức có cấu trúc, tức khối dữ liệu mà máy có thể đọc và hầu hết các trang công thức nhúng vào để phục vụ công cụ tìm kiếm. Nếu có khối dữ liệu này, quá trình import sẽ sạch và hoàn tất ngay.

Bạn cũng có thể import từ ảnh hoặc văn bản thuần túy đã dán vào. Cách này phù hợp với ảnh chụp một trang trong sách dạy nấu ăn. Hai dạng này đi qua quy trình chậm hơn và cần kiểm tra lại sau đó, vì phân số viết tay rất dễ bị đọc nhầm.

Import hàng loạt cũng chạy từ màn hình này: dán danh sách địa chỉ, mỗi địa chỉ một dòng, rồi Mealie xử lý chúng ở chế độ nền. Bạn có thể chuyển toàn bộ một bộ sưu tập gồm hai trăm bookmark chỉ trong một lần.

Kế hoạch bữa ăn và danh sách mua sắm

Meal planner là một lịch. Kéo một công thức vào một ngày để lập kế hoạch cho ngày đó. Sau đó, shopping list tập hợp nguyên liệu từ các công thức đã lập kế hoạch thành một danh sách và gộp các mục trùng nhau. Vì vậy, nếu hai công thức cùng cần hành tây, danh sách chỉ có một dòng thay vì hai.

Danh sách là một trang live trên điện thoại khi bạn đang ở cửa hàng. Vì server của bạn lưu danh sách này, mọi người trong gia đình đều thấy cùng một danh sách theo thời gian thực. Khi một người đánh dấu đã mua sữa, mục đó cũng biến mất khỏi màn hình của người khác.

Đặt nginx và TLS phía trước

Mealie sử dụng HTTP thuần và không tự xử lý chứng chỉ. Hãy thực hiện TLS termination trong nginx đặt phía trước Mealie. Trước tiên, trỏ bản ghi DNS A đến server của bạn, vì bước cấp chứng chỉ sẽ xác minh tên đó.

sudo apt update && sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/mealie
server {
    listen 80;
    server_name recipes.example.com;

    client_max_body_size 64M;

    location / {
        proxy_pass http://127.0.0.1:9925;
        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;
    }
}
sudo ln -s /etc/nginx/sites-available/mealie /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

nginx -t in syntax is ok và test is successful là bước kiểm tra bắt buộc. Chỉ reload sau khi bước này thành công, vì reload cấu hình lỗi sẽ giữ cấu hình cũ đang chạy và che giấu lỗi cho đến lần restart tiếp theo.

client_max_body_size 64M cần thiết vì giá trị mặc định của nginx là 1 MB. Khi upload ảnh công thức hoặc restore backup qua trình duyệt, request sẽ có body lớn hơn mức đó. Nếu thiếu dòng này, nginx sẽ trả về 413 Request Entity Too Large chứ không phải Mealie, nên log của ứng dụng hoàn toàn không ghi nhận gì.

Sau đó, cấp chứng chỉ. Bước này và timer gia hạn được hướng dẫn trong cấp chứng chỉ Let's Encrypt cho nginx bằng certbot.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d recipes.example.com

Certbot sẽ sửa server block để listen trên cổng 443 và thêm redirect từ cổng 80. Truy cập site qua https:// và xác nhận trình duyệt chấp nhận chứng chỉ. Nếu Mealie tải được nhưng các link của chính nó lại chuyển bạn đến http://, thì BASE_URL vẫn có giá trị http và cần được sửa. Sau đó chạy sudo docker compose up -d để tạo lại container với giá trị mới.

Không thể chạy Mealie dưới một subpath như example.com/recipes, vì frontend không thể được phục vụ từ subpath. Hãy dùng một subdomain.

Bản backup và thao tác restore thực sự làm gì

Mọi dữ liệu Mealie quản lý đều nằm trong /app/data/ bên trong container. Đây là volume mealie-data. Sao chép volume này là bạn đã sao chép cả recipes, images và database.

sudo docker volume ls
sudo docker compose stop mealie
sudo docker run --rm -v mealie_mealie-data:/data -v "$PWD":/backup \
  alpine tar czf /backup/mealie-data.tgz -C /data .
sudo docker compose start mealie

Tên volume có tên project làm tiền tố. Tên project là thư mục chứa file compose. Từ /srv/mealie, volume có tên mealie_mealie-data. Vì vậy, lệnh đầu tiên là docker volume ls: hãy dùng tên lệnh này in ra, không dùng tên trong hướng dẫn. Phải dừng container trước, vì SQLite thường đang ghi dở. Nếu sao chép khi container vẫn chạy, file có thể không restore được.

Mealie cũng có trang backup riêng trong khu vực quản trị. Trang này tạo một archive portable chứa database ở dạng JSON cùng với images. Hãy dùng cách này khi chuyển giữa các server, vì archive vẫn hoạt động sau khi đổi version, trong khi việc sao chép file thô có thể không hoạt động. Restore archive có tính phá hủy theo thiết kế: nó xóa database hiện tại trước khi nạp archive và không thể hoàn tác. Bạn sẽ bị đăng xuất khi thao tác hoàn tất.

Cả hai bản sao đều không phải backup nếu vẫn nằm trên cùng server. Hãy định kỳ đẩy archive sang một nơi khác. Đây là mục đích của backup mã hóa ngoài server bằng restic.

Cập nhật Mealie

cd /srv/mealie
sudo nano docker-compose.yml
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs -f mealie

Tăng version đã pin trong file, sau đó pull và tạo lại container. Migration chạy trong lần khởi động đầu tiên của image mới. Hãy sao lưu volume trước khi chuyển sang major version mới, vì migration thất bại giữa chừng có thể để lại database mà image cũ không còn mở được. Đọc release notes cho tất cả các phiên bản nằm giữa version hiện tại và version mới.

Khi importer lỗi

Một số website không công bố dữ liệu công thức có cấu trúc. Khi đó, Mealie chỉ import tiêu đề và danh sách nguyên liệu trống. Bạn không thể xử lý việc này bằng cấu hình. Hãy dán nội dung công thức vào theo cách thủ công.

Các lỗi khác xuất phát từ lớp bảo vệ bot phía trước website công thức. Lớp này trả về cho Mealie một trang yêu cầu xác minh thay vì công thức. Mealie đã giả lập trình duyệt và xoay vòng user agent để giảm tình trạng này. Nếu website vẫn từ chối, các tùy chọn được tài liệu hóa là gửi scraper qua proxy có uy tín địa chỉ IP tốt hơn, hoặc chạy một instance FlareSolverr để xử lý yêu cầu xác minh trong trình duyệt thật. Cả hai đều là tùy chọn và đều được cấu hình bằng biến môi trường trên container.

Nếu import lỗi vì server hoàn toàn không thể kết nối đến website, đó là một vấn đề khác. Hãy kiểm tra trực tiếp từ server bằng curl -I https://the-site.example/recipe và đọc dòng trạng thái trước khi kết luận scraper bị lỗi.

Nó phù hợp ở đâu

Mealie là một ứng dụng self-hosted đầu tiên phù hợp cho hộ gia đình, vì những người sống cùng bạn sẽ tự sử dụng mà không cần nhắc. Công việc này có cùng dạng với việc chạy thư viện ảnh riêng bằng Immich nhưng nhẹ hơn nhiều, đồng thời thuộc danh sách rộng hơn về những thứ đáng self-host trong năm nay. Một server nhỏ có thể chạy cả hai. Immich không phải ứng viên duy nhất cho công việc thứ hai đó. Nếu bạn vẫn đang cân nhắc, mức RAM tối thiểu và các lệnh backup của PhotoPrism và Immich khác nhau đủ nhiều để bạn nên đọc trước khi dùng hết phần dung lượng còn lại. Nếu hộ gia đình cũng lưu kế hoạch và ghi chú bên cạnh các món ăn, workspace AFFiNE self-hosted cũng chỉ là một bài tập compose file tương tự, nhưng cần 4 container và nhiều RAM hơn đáng kể so với Mealie. Vì vậy, trước tiên hãy kiểm tra server còn bao nhiêu tài nguyên. Nếu hộ gia đình muốn theo dõi việc tập luyện bên cạnh các bữa ăn, openGym lưu các buổi tập và cũng yêu cầu tag đã pin, compose file và certificate giống như phần bạn vừa cấu hình. Buổi tối sau bữa ăn cũng có một phiên bản tương tự, vì Halcyon biến thư viện Jellyfin hiện có thành một cửa hàng cho thuê phim thập niên 1990 mà bạn có thể đi quanh, tức là thêm một container nhỏ phía trước thứ bạn đang chạy thay vì thêm một database cần backup. Không phải mọi ứng dụng trong nhóm này đều dành cho hộ gia đình. Nếu sau này cùng thói quen đó cần phục vụ công việc, bàn hỗ trợ Chatwoot self-hosted cũng dùng lại tag đã pin và certificate, nhưng cần Postgres, Redis và outbound mail hoạt động phía sau. Đây là một tenant nặng hơn nhiều so với một ứng dụng lưu công thức nấu ăn và nên có server riêng.

FAQ

Tại sao nhập URL công thức lại thất bại?

Có 2 nguyên nhân thường gặp. Một là trang không cung cấp dữ liệu công thức có cấu trúc, nên scraper không tìm thấy gì và bạn chỉ nhận được tiêu đề mà không có nguyên liệu. Hai là lớp chống bot phía trước trang trả về trang yêu cầu xác minh thay vì công thức. Với trường hợp thứ hai, Mealie có thể trỏ đến một proxy có độ uy tín địa chỉ tốt hơn hoặc đến một instance FlareSolverr tự host để xử lý yêu cầu xác minh bằng trình duyệt thực. Xác nhận server có thể truy cập trang đó hay không bằng curl -I trước khi thay đổi bất kỳ thứ gì.

Tôi có cần PostgreSQL không, hay SQLite là đủ?

SQLite đủ dùng cho hộ gia đình và là lựa chọn mặc định. Hãy chuyển sang PostgreSQL khi data directory nằm trên network attached storage, vì SQLite trên network filesystem có thể gây lỗi database bị khóa và làm hỏng file. Khi restore bằng PostgreSQL, database user phải là superuser vì thao tác restore sẽ xóa mọi thứ trước khi nạp archive.

Tôi có thể chạy Mealie mà không cần domain name không?

Có, trong mạng riêng của bạn. Đặt BASE_URL thành địa chỉ bạn sẽ thực sự nhập, chẳng hạn http://192.168.1.20:9925, rồi bỏ qua nginx. Link mời và link reset password được tạo từ BASE_URL, nên giá trị sai sẽ tạo ra các link mà không ai khác mở được. Không expose nó ra Internet bằng plain HTTP, vì khi đó thông tin đăng nhập được gửi dưới dạng không mã hóa.

Làm thế nào để cấp login riêng cho gia đình tôi?

Giữ ALLOW_SIGNUP ở giá trị "false" và thêm mọi người từ khu vực admin. Thao tác này tạo ra một link mời để bạn gửi cho họ. Đưa mọi người dùng chung một kitchen vào cùng household để họ dùng chung các công thức, meal plan và shopping list. Các household riêng trên cùng một server sẽ giữ các collection riêng biệt.

Công thức của tôi sẽ ra sao nếu tôi ngừng chạy Mealie?

Chúng vẫn còn. Chức năng backup trong admin ghi dữ liệu của bạn dưới dạng JSON. Mealie cũng có thể export công thức thành các file markdown dạng plain text. Các file này vẫn đọc được bằng mọi text editor mà không cần phần mềm nào khác. Hãy thực hiện một lần export trước khi cần đến nó và kiểm tra rằng bạn có thể mở file.