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 nguyên liệu và các bước, rồi dùng meal plan, shopping list, nginx, TLS và backup.
Trình quản lý công thức tự host hoạt động như thế nào
Trình quản lý công thức tự host lưu công thức của bạn trong database trên server do bạn sở hữu, và Mealie là lựa chọn mà phần lớn các hộ gia đình dùng lâu dài. Bạn dán địa chỉ của một trang công thức, Mealie sẽ đọc nguyên liệu, các bước thực hiện, số 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 trong bộ sưu tập của bạn là 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ông thức vào, và một shopping list được tạo từ kế hoạch đó. Mỗi người nấu ăn có một tài khoản đăng nhập riêng. Toàn bộ app chạy trong một container và hầu như không dùng tài nguyên giữa các request, nên một VPS cấu hình vừa phải có thể chạy mà gần như không nhận thấy tải.
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 cấu trúc các file Docker Compose, vì toàn bộ phần dưới đây 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 7 năm 2026, tag stable hiện tại là v3.22.0. Hãy pin một phiên bản 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 qua một lần migration database mà bạn chưa chuẩn bị.
sudo mkdir -p /srv/mealie
cd /srv/mealie
sudo nano docker-compose.ymlservices:
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:Có 2 dòng cần kiểm tra trước khi khởi động.
Cổng được ghi là 127.0.0.1:9925:9000 chứ không phải 9925:9000. Container lắng nghe trên cổng 9000 bên trong, còn host map cổng 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, vì vậy một 9925:9000 thông thường vẫn có thể truy cập từ bên ngoài dù firewall báo cổng đã bị đóng. Bạn nên hiểu rõ điểm bất ngờ này một lần: xem vì sao các cổng Docker đã publish không tuân theo ufw.
BASE_URL phải là địa chỉ public chính xác mà bạn 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 đặt lại mật khẩu và link mời. Nếu đặt thành http://localhost:9925, lời mời bạn gửi cho đối tác sẽ chứa một link chỉ hoạt động trên chính server đó.
Khởi động ứng dụng và monitor lần boot đầu tiên.
sudo docker compose up -d
sudo docker compose logs -f mealieLần khởi động đầu tiên sẽ tạo database SQLite 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:9925Nếu nhận được 200 OK, app đang hoạt động. 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 đã bị kill vì vượt quá giới hạn bộ nhớ 1000M. Điều này thường xảy ra trên các gói VPS 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 đó nằm 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 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, chẳng hạn như một instance Authentik tự host.
Mealie nhóm 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 riêng biệt trên cùng server vẫn giữ các bộ sưu tập riêng, phù hợp với nhà ở chung khi không ai thống nhất được về cá cơm.
Trình import, lý do chính để 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 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 đã 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. Các nguồn 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 sai.
Import hàng loạt 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 ở background. Bạn có thể chuyển cả bộ sưu tập gồm hai trăm bookmark 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 sẽ lấy nguyên liệu từ các công thức đã lên kế hoạch, gộp thành một danh sách và kết hợp các mục trùng nhau. Vì vậy, nếu hai công thức đều cần hành tây thì 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 ở 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 sữa đã mua, mục đó cũng biến mất khỏi màn hình của người kia.
Đặt nginx và TLS phía trước
Mealie dùng HTTP thuần và không tự xử lý certificate. Hãy thực hiện TLS termination trong nginx đặt phía trước Mealie. Trước tiên, trỏ DNS A record đến server của bạn, vì bước cấp certificate sẽ xác minh hostname đó.
sudo apt update && sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/mealieserver {
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 nginxnginx -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 một config bị lỗi sẽ giữ config 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 browser, request body sẽ lớn hơn mức đó. Nếu thiếu dòng này, nginx sẽ trả về 413 Request Entity Too Large, không phải Mealie, nên application log hoàn toàn không có thông tin gì.
Sau đó cấp certificate. Bước này và renewal timer được hướng dẫn trong cấp certificate Let's Encrypt cho nginx bằng certbot.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d recipes.example.comCertbot sửa server block để listen trên 443 và thêm redirect từ port 80. Mở site qua https:// và xác nhận browser chấp nhận certificate. 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 đang đặt là http và cần sửa lại. 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 subpath như example.com/recipes vì frontend không hỗ trợ chạy từ subpath. Hãy dùng một subdomain.
Bản sao lưu và 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ả recipe, hình ảnh 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 mealieTên volume có tên project làm tiền tố. Tên project là tên thư mục chứa file compose. Từ /srv/mealie, volume là 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. Cần dừng container trước, vì SQLite thường đang ghi dữ liệu. Bản sao đang ghi có thể được restore nhưng không đọc đượ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ưới dạng JSON cùng với các hình ảnh. 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 bản sao file thô có thể không hoạt động. Restore archive có tính phá hủy dữ liệu 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 nơi khác. Đó là mục đích của backup được 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 mealieTăng phiên bản đã ghim 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 phiên bản lớn mới, vì migration bị lỗi giữa chừng có thể để lại database mà image trước đó không còn mở được. Đọc release notes cho tất cả các phiên bản nằm giữa phiên bản hiện tại và phiên bản mới.
Khi importer không hoạt động
Một số site hoàn toàn không xuất bản dữ liệu công thức có cấu trúc, nên Mealie chỉ import tiêu đề và danh sách nguyên liệu trống. Bạn không thể khắc phục việc này bằng cấu hình. Hãy dán nội dung công thức vào thủ công.
Các lỗi khác bắt nguồn từ lớp bảo vệ bot ở phía trước site công thức. Lớp này trả về cho Mealie một trang challenge thay vì công thức. Mealie đã giả lập trình duyệt và luân phiên user agent để giảm trường hợp này. Nếu site vẫn từ chối, các tùy chọn được tài liệu hóa là chuyển scraper qua một proxy có uy tín địa chỉ IP tốt hơn, hoặc chạy một instance FlareSolverr để giải challenge trong trình duyệt thật. Cả hai đều là tùy chọn và đều được cấu hình thông qua biến môi trường trên container.
Nếu import thất bại vì server của bạn hoàn toàn không thể kết nối đến site, đó là một vấn đề khác. Hãy kiểm tra trực tiếp từ máy chủ bằng curl -I https://the-site.example/recipe và đọc dòng status trước khi kết luận scraper bị lỗi.
Phù hợp trong hệ thống
Mealie là một ứng dụng self-hosted phù hợp để triển khai đầu tiên 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 bạn nhắc. Mealie giải quyết nhu cầu tương tự như tự quản lý thư viện ảnh bằng Immich, nhưng nhẹ hơn nhiều, và thuộc danh sách rộng hơn về những dịch vụ đáng self-host trong năm nay. Một server nhỏ có thể chạy cả hai. Immich không phải lựa chọn duy nhất cho nhu cầu 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 đủ để bạn nên đọc trước khi dành phần dung lượng còn lại của disk cho chúng.
FAQ
Tại sao nhập 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 nhận được tiêu đề nhưng 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, bạn có thể trỏ Mealie đến một proxy có reputation địa chỉ tốt hơn, hoặc đến một instance FlareSolverr tự host để xử lý challenge trong trình duyệt thật. Xác nhận server có thể truy cập trang đó trước bằng curl -I rồi mới thay đổi cấu hình.
Tôi có cần PostgreSQL không, hay SQLite là đủ?
SQLite đủ dùng cho một hộ gia đình và là lựa chọn mặc định. 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ì quá trình restore sẽ xóa mọi dữ liệu trước khi nạp archive.
Tôi có thể chạy Mealie mà không cần domain name không?
Có, nếu chỉ dùng 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, và bỏ qua nginx. Link invitation và reset password đượ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 Mealie ra Internet qua HTTP thuần, vì khi đó thông tin đăng nhập sẽ được gửi không mã hóa.
Làm thế nào để cấp login riêng cho gia đình?
Giữ ALLOW_SIGNUP ở giá trị "false" và thêm người dùng từ khu vực admin. Mealie sẽ tạo một link invitation để bạn gửi cho họ. Đưa tất cả người cùng sử dụng một bếp vào cùng household để họ dùng chung recipes, 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.
Điều gì xảy ra với recipes nếu tôi ngừng chạy Mealie?
Bạn vẫn lấy được chúng. 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 recipes thành các file markdown thuần, có thể đọc bằng bất kỳ text editor nào mà không cần phần mềm chuyên dụng. Hãy tạo một bản export trước khi cần đến và kiểm tra rằng bạn có thể mở nó.