SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

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 các image lên GitHub container registry. Tính đến tháng 10 năm 2026, tag stable hiện tại là v3.28.0. Hãy pin một version thay vì dùng latest: với latest, một docker compose pull vào một ngày bất kỳ có thể đưa bạn sang một database migration mà bạn chưa chuẩn bị.

sudo mkdir -p /srv/mealie
cd /srv/mealie
sudo nano docker-compose.yml
services:
  mealie:
    image: ghcr.io/mealie-recipes/mealie:v3.28.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:

Có 2 dòng cần xem lại trước khi khởi động.

Port được ghi là 127.0.0.1:9925:9000 chứ không phải 9925:9000. Container listen trên port 9000 ở bên trong, còn host map port 9925 vào đó. Bind map này vào loopback address 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, vì vậy 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. Bạn nên hiểu rõ điều bất ngờ này một lần: xem vì sao các port Docker đã publish bỏ qua ufw.

BASE_URL phải là public address chính xác mà bạn sẽ sử dụng, có scheme và không có trailing slash. Mealie dùng địa chỉ này để tạo password reset link và invitation link. 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 service 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 tạo SQLite database và chạy các migration, 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 đã chạy. Connection refused nghĩa là container không chạy: hãy chạy sudo docker compose ps và đọc exit code. Container dừng với code 137 là container đã bị kill vì vượt quá memory limit 1000M, 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, sau đó đổi cả hai ngay lập tức, vì cặp thông tin này được in trong tài liệu và do đó có trong mọi scanner.

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

Mealie đưa người dùng vào một household, còn các household nằm trong một group. Mọi người trong cùng một household dùng chung meal plan và shopping list, phù hợp với nhu cầu của một gia đình. Recipe collection có phạm vi rộng hơn: các household chia sẻ recipe, tag và category với mọi household khác trong cùng group. Vì vậy, các household riêng trên cùng một server vẫn có plan và list riêng, nhưng dùng chung recipe; mô hình này vẫn phù hợp với một flatshare nơi không ai thống nhất được chuyện có cho anchovy hay không. Nếu người dùng cần các collection hoàn toàn riêng biệt, hãy tạo group riêng cho họ, vì không có gì được chia sẻ giữa các group.

Trình importer, cũng 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 liên kết vào. Mealie sẽ tải trang 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 đều nhúng vào để công cụ tìm kiếm sử dụng. Khi có khối dữ liệu này, quá trình import sẽ sạch và hoàn tất ngay.

Nếu cấu hình một AI provider, bạn cũng có thể import từ hình ảnh hoặc văn bản thuần được 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. Image import cần provider có khả năng đọc hình ảnh. Nếu không có provider, các kiểu import này sẽ không khả dụng. Chúng đi qua quy trình chậm hơn và cần được kiểm tra sau đó, vì phân số viết tay rất dễ bị đọc sai.

Bulk import cũng chạy từ cùng màn hình đó: dán danh sách địa chỉ, mỗi địa chỉ một dòng, rồi Mealie sẽ xử lý chúng trong background. Một bộ sưu tập gồm hai trăm bookmark có thể được chuyển sang 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à việc restore thực sự thực hiện gì

Toàn bộ dữ liệu Mealie quản lý 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ùng lúc recipe, image và database. Tài liệu của Mealie cũng xem việc dừng container rồi sao chép thư mục này là cách tốt nhất để backup một bản cài đặt SQLite.

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 là mealie_mealie-data. Vì vậy, lệnh đầu tiên dùng docker volume ls: hãy dùng tên mà lệnh in ra, không dùng tên trong hướng dẫn này. Cần dừng container trước, vì SQLite thường đang ghi dở. Nếu sao chép khi database đang hoạt động, bản sao có thể không restore được.

Mealie cũng có trang backup riêng trong khu vực admin. Trang này ghi một archive zip chứa database dưới dạng database.json. Không nên dựa vào archive này để chuyển qua một version khác. Archive phải được restore bằng đúng version Mealie đã tạo nó. Nếu version không khớp, quá trình restore sẽ dừng với lỗi. Để chuyển giữa các server, hãy chạy cùng pinned version trên server mới, sau đó sao chép volume sang đó hoặc restore archive tại đó. 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 quá trình hoàn tất.

Không bản sao nào là 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. Đó 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 việc import URL công thức lại thất bại?

Có 2 nguyên nhân phổ biến. 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 bảo vệ bot phía trước trang trả về trang challenge thay vì công thức. Với trường hợp thứ hai, có thể cấu hình Mealie dùng proxy có reputation địa chỉ tốt hơn hoặc một instance FlareSolverr tự host để xử lý challenge bằng trình duyệt thật. Trước khi thay đổi bất kỳ thứ gì, hãy xác nhận server có thể truy cập trang đó bằng curl -I.

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

SQLite đủ dùng cho 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 sẽ gây lỗi database bị khóa và có thể 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 network 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. Các link invitation và password reset được tạo từ BASE_URL, nên giá trị sai sẽ tạo ra các link mà người khác không thể mở. Không expose nó ra Internet qua plain HTTP, vì khi đó thông tin đăng nhập sẽ được gửi dưới dạng không mã hóa.

Làm cách nào để cấp login riêng cho gia đình?

Giữ ALLOW_SIGNUP ở giá trị "false" rồi thêm mọi người từ khu vực admin. Thao tác này tạo một link invitation để bạn gửi cho họ. Đặt tất cả những người dùng chung một kitchen vào cùng household để họ dùng chung meal plan và shopping list. Dù theo cách nào, recipe cũng được chia sẻ với mọi household trong group, nên những người cần collection riêng phải ở các group riêng.

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

Chúng vẫn còn. Tính năng backup trong admin ghi data của bạn dưới dạng JSON. Mealie cũng có thể export recipe thành các file markdown plain text, vẫn có thể đọc bằng bất kỳ text editor nào mà không cần cài thêm software. 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.