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

Cài Listmonk trên VPS Ubuntu 24.04 với PostgreSQL

Cài Listmonk v6.2.0 trên Ubuntu 24.04 với PostgreSQL, config.toml, systemd và TLS; kết nối SMTP, rồi hiểu chi phí thật để email vào inbox.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Listmonk cần gì để tự host newsletter

Listmonk là công cụ tự host để quản lý newsletter và mailing list: một binary Go, một database PostgreSQL, một file cấu hình và một systemd unit. Một VPS nhỏ có thể chạy Listmonk dễ dàng vì Listmonk lưu subscriber và xếp hàng campaign nhưng không tự gửi email. Listmonk chuyển từng email đến một server SMTP (simple mail transfer protocol), nên tỷ lệ email đến inbox phụ thuộc vào reputation của server đó, không phải phần mềm này.

Hướng dẫn này cài Listmonk v6.2.0, bản release hiện tại vào tháng 7 năm 2026, trên Ubuntu 24.04. Bạn cần một VPS có địa chỉ IP public, một domain name do bạn quản lý và PostgreSQL 12 trở lên. Quá trình cài đặt mất khoảng 1 giờ. Việc xây dựng sending reputation mất vài tuần; phần này được trình bày gần cuối hướng dẫn.

Cài PostgreSQL và tạo database

Ubuntu 24.04 phát hành PostgreSQL 16 trong repository riêng, mới hơn nhiều so với yêu cầu của Listmonk.

sudo apt update
sudo apt install -y postgresql curl
sudo systemctl enable --now postgresql

Tạo role và database trong cùng một phiên psql. -v ON_ERROR_STOP=1 khiến psql thoát ngay khi statement đầu tiên bị lỗi, nên lỗi gõ sai không để lại một cấu hình dở dang nhưng trông như đã hoàn tất.

sudo -u postgres psql -v ON_ERROR_STOP=1 <<'SQL'
CREATE USER listmonk WITH PASSWORD 'pick-a-long-random-password';
CREATE DATABASE listmonk OWNER listmonk;
SQL

OWNER listmonk không phải tùy chọn trang trí. Quá trình cài schema tạo table, type, index và function, nên role phải sở hữu database. Trỏ Listmonk vào database do role khác sở hữu sẽ khiến quá trình cài dừng với permission denied, ngay cả khi bạn đã chạy GRANT CONNECT.

Xác nhận database tồn tại trước khi tiếp tục.

sudo -u postgres psql -tAc "SELECT datname FROM pg_database WHERE datname='listmonk';"

Lệnh này in ra listmonk. Dòng trống nghĩa là statement CREATE chưa từng chạy, vì vậy hãy đọc lại output của psql.

Cài binary Listmonk

Listmonk phát hành một binary tĩnh cho từng architecture. Hãy kiểm tra architecture của máy trước, vì binary amd64 trên VPS ARM là file mà kernel sẽ từ chối thực thi.

dpkg --print-architecture
cd /tmp
curl -fsSLO https://github.com/knadh/listmonk/releases/download/v6.2.0/listmonk_6.2.0_linux_amd64.tar.gz
tar -xzf listmonk_6.2.0_linux_amd64.tar.gz
sudo install -m 755 listmonk /usr/bin/listmonk
listmonk --version

Trên VPS ARM, thay amd64 bằng arm64 trong tên file. listmonk --version in ra chuỗi phiên bản là bằng chứng đầu tiên cho thấy binary khớp với máy.

Tạo config.toml và khóa quyền truy cập

--new-config ghi config.toml vào thư mục làm việc hiện tại. Vì vậy, cd nằm bên trong sh -c, không nằm trước sudo.

sudo install -d -m 750 /etc/listmonk
sudo sh -c 'cd /etc/listmonk && listmonk --new-config'

File được tạo khá ngắn. Trong [app], address = "localhost:9000" bind HTTP server chỉ vào loopback, nên không thể truy cập panel quản trị từ Internet cho đến khi bạn đặt reverse proxy phía trước. Giữ nguyên dòng đó. Trong [db], bạn có host = "localhost", port = 5432, user = "listmonk", database = "listmonk"ssl_mode = "disable". Các giá trị mặc định này đã khớp với database bạn vừa tạo, nên dòng duy nhất cần thay đổi là dòng password.

ssl_mode = "disable" là đúng khi Postgres lắng nghe trên loopback của cùng máy, vì traffic đó không rời khỏi máy. Nếu chuyển database sang host khác, hãy đặt thành require, nếu không password sẽ đi qua network dưới dạng cleartext.

Sửa dòng password trong [db] để khớp với role, sau đó tạo service account và ngăn mọi login khác đọc file.

sudo useradd --system --home-dir /var/lib/listmonk --create-home --shell /usr/sbin/nologin listmonk
sudo chown -R root:listmonk /etc/listmonk
sudo chmod 640 /etc/listmonk/config.toml

Bây giờ service account có thể đọc file, còn không tài khoản nào khác có thể đọc được.

sudo -u listmonk cat /etc/listmonk/config.toml > /dev/null && echo readable
stat -c '%U:%G %a' /etc/listmonk/config.toml

Lệnh đầu tiên in ra readable. Lệnh thứ hai in ra root:listmonk 640. Bất kỳ unprivileged account nào khác thử cùng cat sẽ nhận Permission denied. Đây là mục đích của việc này: file chứa database password dưới dạng cleartext, trong khi một server thường có nhiều login. Lý do tương tự áp dụng cho mọi service bạn chạy, vì vậy hãy đọc một lần về service user với quyền tối thiểu và áp dụng nguyên tắc này ở mọi nơi.

Tạo schema bằng --install

--install tạo các bảng và khởi tạo các thiết lập mặc định. Đặt thông tin đăng nhập admin đầu tiên bằng biến môi trường để tài khoản được tạo trước khi panel có thể truy cập.

sudo -u listmonk env LISTMONK_ADMIN_USER=admin \
  LISTMONK_ADMIN_PASSWORD='another-long-random-password' \
  listmonk --config /etc/listmonk/config.toml --install --yes

--yes trả lời prompt xác nhận. Hãy đọc prompt đó một lần trước khi tự động hóa, vì --install là chế độ cài đặt lần đầu và sẽ xóa schema Listmonk hiện có. Chạy lại lần thứ hai trên database đang hoạt động sẽ xóa các subscriber. Trong mọi script có thể chạy hai lần, hãy dùng --install --idempotent --yes; lệnh này không làm gì khi các bảng đã tồn tại. Các thay đổi schema được phát hành trong bản release mới phải áp dụng bằng --upgrade, không dùng --install.

Kiểm tra kết quả từ phía database thay vì từ trình duyệt.

sudo -u postgres psql -d listmonk -c '\dt'
sudo -u postgres psql -d listmonk -tAc "SELECT username FROM users;"

Lệnh đầu tiên liệt kê các bảng Listmonk, trong đó có subscribers, lists, campaigns, templatesbounces. Lệnh thứ hai in admin. Nếu lệnh thứ hai trả về kết quả rỗng, các biến môi trường chưa bao giờ được truyền đến process, nên panel sẽ yêu cầu bạn tạo user đầu tiên trong trình duyệt.

Chạy Listmonk bằng systemd

Viết /etc/systemd/system/listmonk.service.

[Unit]
Description=Listmonk newsletter and mailing list manager
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=listmonk
Group=listmonk
WorkingDirectory=/var/lib/listmonk
ExecStart=/usr/bin/listmonk --config /etc/listmonk/config.toml
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target

WorkingDirectory quan trọng vì Listmonk phân giải các đường dẫn tương đối, bao gồm đường dẫn media upload trên filesystem, dựa trên thư mục này. After=postgresql.service chỉ sắp xếp thứ tự khởi động, không chờ Postgres chấp nhận kết nối, nên Restart=on-failure xử lý trường hợp Listmonk khởi động sớm và không thể kết nối.

sudo systemctl daemon-reload
sudo systemctl enable --now listmonk
ss -ltnp | grep 9000
curl -sI http://127.0.0.1:9000/

ss phải hiển thị 127.0.0.1:9000 ở trạng thái LISTEN. curl trả về bất kỳ dòng trạng thái HTTP nào nghĩa là server đang phản hồi. curl thất bại với Connection refused nghĩa là tiến trình đã dừng trong lúc khởi động, và journalctl -u listmonk -n 50 --no-pager sẽ cho biết lý do. Lưu ý rằng enable --now là phần giúp service tiếp tục hoạt động sau khi reboot: tiến trình được khởi động thủ công sẽ biến mất sau lần nâng cấp kernel tiếp theo.

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

Listmonk sử dụng HTTP thuần trên loopback, nên nginx sẽ thực hiện TLS termination (bảo mật tầng truyền tải) và chuyển tiếp request.

server {
    listen 443 ssl;
    server_name lists.example.com;

    client_max_body_size 25m;

    location / {
        proxy_pass http://127.0.0.1:9000;
        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;
    }
}

client_max_body_size phải được tăng vì thao tác import subscriber và upload media đều gửi file, trong khi nginx mặc định từ chối mọi request lớn hơn 1 MB bằng 413 Request Entity Too Large. Cấp certificate bằng certbot. Công cụ này cũng tự ghi các dòng listen 443 ssl và cấu hình redirect từ port 80 cho bạn: các bước được nêu trong hướng dẫn cấp certificate Let's Encrypt cho nginx. Mở port 80 và 443, đồng thời để port 9000 đóng vì proxy truy cập port này qua loopback. Nếu firewall chưa được cấu hình, hãy bắt đầu với kiến thức cơ bản về firewall ufw.

Sau đó mở admin panel và đặt root URL trong Settings thành https://lists.example.com. Bản cài đặt mới chứa http://localhost:9000, và Listmonk ghi giá trị đó vào mọi unsubscribe link và media URL mà nó chèn vào email. Nếu gửi campaign trước khi thay đổi giá trị này, mỗi recipient sẽ nhận các link trỏ về chính máy của họ. Các link đó không hoạt động với người đọc. Với spam filter, chúng còn cho thấy sender không biết cấu hình domain của mình.

Kết nối SMTP, không nằm trong config.toml

Tìm config.toml để xem có section SMTP hay không, nhưng bạn sẽ không thấy. Các thiết lập mail nằm trong database, trong bảng settings, và bạn chỉnh sửa chúng trong admin panel tại Settings và SMTP. Vì vậy, file được tạo ra vẫn rất ngắn. Đây cũng là lý do thay đổi SMTP không cần restart.

Có hai lựa chọn thực tế cho chính SMTP server. Bạn có thể tự vận hành. Khi đó, reputation của IP hoàn toàn do bạn chịu trách nhiệm, và đây là một dự án độc lập: tự vận hành mail server với Mailcow giải thích những việc cần làm. Hoặc trỏ Listmonk đến một transactional relay và để nhà cung cấp khác quản lý reputation của IP.

Dù chọn cách nào, hãy dùng port 587 với STARTTLS hoặc port 465 với implicit TLS. Đừng dựa vào outbound port 25. Hầu hết nhà cung cấp VPS mặc định chặn port này trên tài khoản mới. Khi port 25 bị chặn, kết nối trông giống hệt như bị treo vì các packet bị drop thay vì bị từ chối. Do đó, client phải chờ timeout thay vì fail ngay.

Hãy test trước khi sử dụng chính thức. Tạo một list, thêm địa chỉ của bạn làm subscriber rồi gửi campaign cho một người nhận. Mở message đã nhận và đọc toàn bộ header. Header Authentication-Results do phía nhận thêm vào cho biết SPF và DKIM có pass hay không.

Khả năng gửi mail là toàn bộ công việc

Listmonk tạo message, theo dõi list và chuyển mail đi. Mọi quyết định về việc mail có đến inbox hay không đều do nhà cung cấp nhận mail đưa ra, dựa trên sending IP address và sending domain. IP mới trên VPS chưa có lịch sử nào, và mọi nhà cung cấp mailbox lớn đều xem việc không có lịch sử là dấu hiệu hơi đáng ngờ.

Có 4 thứ bắt buộc phải có:

  • Bản ghi SPF (sender policy framework) dạng TXT, chỉ rõ host được phép gửi mail cho domain của bạn.
  • Key DKIM (domainkeys identified mail) được publish dưới dạng bản ghi TXT. Mail server phải thực hiện việc ký, không phải Listmonk.
  • Bản ghi DMARC (domain based message authentication, reporting and conformance), cho biết receiver phải xử lý thế nào khi 2 cơ chế đầu tiên không đạt.
  • Một bounce mailbox để Listmonk đọc. Nhờ đó, các địa chỉ từ chối mail sẽ bị xóa khỏi list thay vì bị retry vô hạn.

Sau đó, ban đầu hãy gửi mail từ từ. Một domain chưa từng gửi mail nhưng đột nhiên gửi 10000 message trong 1 giờ có đúng hình dạng của một account bị compromise, nên sẽ bị filter như trường hợp đó. Hãy bắt đầu với những subscriber tương tác nhiều nhất và tăng volume trong vài ngày.

Mọi template cũng cần có unsubscribe link hoạt động. Trong template của Listmonk, link này là {{ UnsubscribeURL }}, còn campaign body nằm tại vị trí của {{ template "content" . }} và phải xuất hiện đúng 1 lần trong mỗi template. Campaign không có unsubscribe link sẽ nhận spam complaint thay vì lượt unsubscribe. Complaint là cách nhanh nhất để mất sending reputation mà bạn đã mất nhiều tuần xây dựng.

Bản backup và những gì thao tác restore thực sự cần

Có 2 thứ phải được đưa ra khỏi máy chủ: bản dump của database và config.toml. Nếu bạn upload hình ảnh vào campaign, hãy thêm thư mục media.

sudo -u postgres pg_dump -Fc listmonk > listmonk-$(date +%F).dump

Bản dump đó chứa subscriber, campaign, template và mọi setting, bao gồm thông tin xác thực SMTP. Vì vậy, hãy mã hóa nó và lưu bên ngoài máy chủ này. Lập lịch cho việc đó đã có cách xử lý ổn định: xem backup restic được mã hóa đến storage từ xa. config.toml chỉ gồm vài dòng nhưng chứa mật khẩu database, nên cũng phải xử lý như vậy.

Khi upgrade, hãy thực hiện đúng thứ tự. Dừng service, tạo dump, thay binary trong /usr/bin, chạy listmonk --config /etc/listmonk/config.toml --upgrade, rồi khởi động service. Schema migration chỉ chạy theo chiều tiến, nên bản dump đó là cách duy nhất để quay lại trạng thái trước đó.

Vì sao Listmonk không khởi động?

Trước tiên, đọc journal bằng journalctl -u listmonk -n 50 --no-pager. Gần như mọi lỗi khởi động đều xuất hiện trên một dòng trong block [db].

pq: password authentication failed for user "listmonk" có nghĩa là mật khẩu trong [db] không khớp với Postgres role. Prefix pq là driver Postgres báo server từ chối kết nối, nên config đã được đọc đúng nhưng thông tin xác thực bị sai. Reset role bằng sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';" rồi ghi đúng chuỗi đó vào file.

pq: database "listmonk" does not exist có nghĩa là giá trị database trong [db] không chỉ đến một database có thật. sudo -u postgres psql -l liệt kê những gì thực sự có trên server, bao gồm cả cách viết mà bạn đã nhập nhầm.

permission denied trong lúc chạy --install có nghĩa là role có thể kết nối nhưng không sở hữu database, nên không thể tạo table trong đó. Sửa bằng sudo -u postgres psql -c "ALTER DATABASE listmonk OWNER TO listmonk;" rồi chạy lại quá trình cài đặt.

Service không bao giờ khởi động và journal ghi tên file config. Process chạy dưới tài khoản listmonk không thể mở config.toml nếu file đó vẫn có owner root:root với mode 600. stat -c '%U:%G %a' /etc/listmonk/config.toml phải in ra root:listmonk 640, còn thư mục bên trên phải là root:listmonk 750.

Panel hoạt động nhưng không có mail đến nơi. Đây không phải lỗi khởi động. Trước tiên, kiểm tra Settings và SMTP, sau đó kiểm tra log riêng của campaign trong admin panel. Log này ghi lại lỗi mà mail server trả về cho từng lần gửi.

FAQ

Tôi có cần mail server riêng để dùng Listmonk không?

Không. Listmonk không phải là mail server. Nó cần thông tin xác thực SMTP của một server nhận mail và gửi mail đi. Server đó có thể là transactional relay hoặc mail server do bạn tự vận hành. Nhập thông tin xác thực trong Settings và SMTP của admin panel, không nhập trong config.toml, vì cấu hình mail được lưu trong database. Dùng port 587 với STARTTLS hoặc port 465 với implicit TLS, vì hầu hết nhà cung cấp VPS chặn outbound port 25 trên các account mới.

Cấu hình root URL vẫn đang dùng giá trị mặc định khi cài đặt là http://localhost:9000. Listmonk ghi giá trị đó vào unsubscribe link và media URL tại thời điểm gửi campaign. Mở Settings trong admin panel, đặt root URL thành địa chỉ HTTPS thật của bạn rồi lưu lại. Không thể sửa các message đã gửi, vì vậy hãy gửi một campaign thử cho chính bạn và bấm unsubscribe link trong đó trước khi gửi cho danh sách thật.

Chạy lại --install có xóa subscriber không?

Có. --install là installer chạy lần đầu và nó xóa schema hiện có của Listmonk. --yes loại bỏ prompt cảnh báo bạn về việc này. Trong mọi script có thể chạy hai lần, hãy dùng --install --idempotent --yes. Tùy chọn này không làm gì nếu các table đã tồn tại. Để áp dụng thay đổi schema trong một release mới, hãy dừng service, tạo pg_dump, rồi chạy --upgrade.

Vì sao Listmonk báo password authentication failed for user listmonk?

Password trong block [db] của /etc/listmonk/config.toml không khớp với Postgres role cùng tên. Dòng trong journal là pq: password authentication failed for user "listmonk". pq là Postgres driver chuyển tiếp phản hồi từ chối của server. Điều này cho biết file cấu hình đã được tìm thấy và đọc thành công. Reset password của role bằng sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';", ghi đúng chuỗi đó vào file cấu hình, rồi chạy sudo systemctl restart listmonk.