How to self-host Listmonk on Ubuntu VPS
Install Listmonk v6.2.0 on Ubuntu 24.04 with PostgreSQL 16, systemd and TLS. See SMTP setup, the config.toml details, and why delivery reputation takes weeks.
Wetin self-hosted newsletter for Listmonk need
Listmonk na self-hosted newsletter and mailing list manager: one Go binary, one PostgreSQL database, one config file, and one systemd unit. Small VPS fit run am without wahala, because Listmonk dey store subscribers and queue campaigns, but e no dey deliver mail by itself. E dey hand each message over to SMTP (simple mail transfer protocol) server, so na that server reputation decide your delivery rate, no be this software.
This guide go install Listmonk v6.2.0, wey be the current release as of July 2026, for Ubuntu 24.04. You need VPS wey get public IP address, domain name wey you control, and PostgreSQL 12 or newer. Installation go take about one hour. Sending reputation dey take weeks, and na near the end we cover that part.
Install PostgreSQL and create the database
Ubuntu 24.04 get PostgreSQL 16 for e own repository, and this version pass wetin Listmonk need.
sudo apt update
sudo apt install -y postgresql curl
sudo systemctl enable --now postgresqlCreate the role and database inside one psql session. -v ON_ERROR_STOP=1 make psql comot after the first statement wey fail, so typo no go leave half-finished setup wey look complete.
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;
SQLOWNER listmonk no be decoration. Schema installation dey create tables, types, indexes, and functions, so the role must own the database. If you point Listmonk to database wey another role own, installation go stop with permission denied, even after you don run GRANT CONNECT.
Confirm say the database dey exist before you continue.
sudo -u postgres psql -tAc "SELECT datname FROM pg_database WHERE datname='listmonk';"That command go print listmonk. Empty line mean say the CREATE statement no run, so read the psql output again.
Install Listmonk binary
Listmonk dey publish static binary for each architecture. Check your own one first, because amd64 binary for ARM VPS na file wey kernel no go execute.
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 --versionFor ARM VPS, change amd64 to arm64 for the file name. listmonk --version wey dey print version string na your first proof say binary match the machine.
Generate config.toml lock am down
--new-config dey write config.toml inside current working directory. Na why cd dey inside sh -c, no be before sudo.
sudo install -d -m 750 /etc/listmonk
sudo sh -c 'cd /etc/listmonk && listmonk --new-config'The generated file short. Under [app], address = "localhost:9000" dey bind HTTP server to loopback only, so internet no fit reach the admin panel until you put reverse proxy for front of am. Leave that line as e be. Under [db] you go see host = "localhost", port = 5432, user = "listmonk", database = "listmonk" and ssl_mode = "disable". Those defaults already match the database wey you create, so na only password line you need change.
ssl_mode = "disable" correct while Postgres dey listen on loopback for the same machine, because that traffic no dey leave the machine. If you move the database go another host, set am to require, or password go cross the network as cleartext.
Edit password line under [db] make e match the role, then create service account and remove the file from every other login.
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.tomlNow service account fit read the file, and nobody else fit.
sudo -u listmonk cat /etc/listmonk/config.toml > /dev/null && echo readable
stat -c '%U:%G %a' /etc/listmonk/config.tomlThe first command dey print readable. The second dey print root:listmonk 640. Any other unprivileged account wey try the same cat go get Permission denied. Na so e suppose be: this file hold your database password as cleartext, and server usually get more than one login. The same thing apply to every service wey you run, so read service users wey get least privilege once and apply am everywhere.
Create the schema with --install
--install dey build the tables and seed the default settings. Set the first admin login with environment variables, so the account go dey available before anybody fit reach the panel.
sudo -u listmonk env LISTMONK_ADMIN_USER=admin \
LISTMONK_ADMIN_PASSWORD='another-long-random-password' \
listmonk --config /etc/listmonk/config.toml --install --yes--yes dey answer the confirmation prompt. Read that prompt once before you automate am, because --install na the first-time installer and e go delete any existing Listmonk schema. If you run am again for live database, e go destroy your subscribers. For any script wey fit run twice, use --install --idempotent --yes. E no do anything when the tables don already dey. Schema changes wey ship with new release dey apply with --upgrade, never with --install.
Check the result from the database side, no be from the browser.
sudo -u postgres psql -d listmonk -c '\dt'
sudo -u postgres psql -d listmonk -tAc "SELECT username FROM users;"The first one dey list the Listmonk tables, including subscribers, lists, campaigns, templates and bounces. The second one dey print admin. If the second one return empty result, environment variables no reach the process. For that case, the panel go ask you to create the first user for the browser instead.
Run Listmonk under systemd
Write /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.targetWorkingDirectory dey important because Listmonk dey resolve relative paths, including filesystem media upload path, based on am. After=postgresql.service only dey arrange the start order; e no dey wait for Postgres to accept connections. So Restart=on-failure dey cover the case where Listmonk start small before time and e no fit connect.
sudo systemctl daemon-reload
sudo systemctl enable --now listmonk
ss -ltnp | grep 9000
curl -sI http://127.0.0.1:9000/ss suppose show 127.0.0.1:9000 for LISTEN state. If curl return any HTTP status line, e mean say server dey answer. If curl fail with Connection refused, e mean say process die during startup, and journalctl -u listmonk -n 50 --no-pager go talk why. Remember say enable --now na the part wey survive reboot: process wey you start by hand go disappear after the next kernel upgrade.
Put nginx and TLS for front
Listmonk dey speak plain HTTP for loopback, so nginx dey terminate TLS (transport layer security) and forward the 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 suppose increase because subscriber imports and media uploads na file posts, and nginx dey reject anything wey pass 1 MB by default with 413 Request Entity Too Large. Use certbot issue the certificate; e go also write the listen 443 ssl lines and redirect from port 80 for you: steps dey for Let's Encrypt certificate guide for nginx. Open ports 80 and 443, but leave 9000 closed because proxy dey reach am through loopback. If firewall never get configuration yet, start with ufw firewall basics.
Then open the admin panel and set the root URL under Settings to https://lists.example.com. Fresh install get http://localhost:9000, and Listmonk dey write that value inside every unsubscribe link and media URL wey e put for email. Send campaign before you change am, and every recipient go get links wey dey point to their own machine. The links no go work for the reader, and spam filter fit see am as sender wey no fit configure their own domain.
Connect SMTP, wey no dey inside config.toml
Search config.toml for SMTP section, you no go find any. Mail settings dey inside database, for settings table, and you fit edit dem for admin panel under Settings and SMTP. Na why the generated file short like this, and na why SMTP change no need restart.
You get two correct options for the SMTP server itself. You fit run your own one, but that go put the reputation fully for your hand and e be serious project by itself: how to run your own mail server with Mailcow explain wetin e involve. Or point Listmonk to transactional relay and make another person manage the IP reputation.
Any option wey you choose, use port 587 with STARTTLS, or port 465 with implicit TLS. No plan to use outbound port 25. Most VPS providers dey block am by default for new accounts. Blocked port 25 go look exactly like connection wey hang, because dem dey drop the packets instead of refusing dem. So the client go wait for timeout instead of fail immediately.
Test am before you trust am. Create list, add your own address as subscriber, and send campaign to one recipient. Open the message wey arrive and read the full headers. The Authentication-Results header wey the receiving side add tells you whether SPF and DKIM pass.
Deliverability na the whole work
Listmonk dey build the message, track the list, and hand the mail over. Receiving provider dey make every decision about whether the mail go reach inbox, based on the sending IP address and sending domain. New VPS IP no get any history, and every big mailbox provider dey treat no history as small suspicion.
Four things no be optional:
- SPF (sender policy framework) TXT record wey name the host wey get permission to send for your domain.
- DKIM (domainkeys identified mail) key wey you publish as TXT record. Na the mail server, no be Listmonk, suppose sign the mail.
- DMARC (domain based message authentication, reporting and conformance) record wey tell receivers wetin to do when the first two fail.
- Bounce mailbox wey Listmonk dey read, so addresses wey reject mail go comot from the list instead of retrying forever.
Then start by sending slowly. Domain wey never send mail before and suddenly deliver ten thousand messages inside one hour get the same pattern as compromised account, so providers go filter am like one. Start with subscribers wey dey engage pass, then increase the volume over several days.
Every template still need working unsubscribe link. For Listmonk template, na {{ UnsubscribeURL }}, and campaign body go enter where {{ template "content" . }} dey. {{ template "content" . }} must appear exactly once for each template. Campaign wey no get unsubscribe link go bring spam complaints instead of unsubscribes. Complaints na the fastest way to lose sending reputation wey you spend weeks building.
Backup, and wetin restore really need
Two things must comot from the server: the database dump and config.toml. Add the media directory if you dey upload images into campaigns.
sudo -u postgres pg_dump -Fc listmonk > listmonk-$(date +%F).dumpThat dump get subscribers, campaigns, templates and every setting, including the SMTP credentials, so encrypt am and keep am outside this server. Scheduling that one don get solution: see encrypted restic backups to remote storage. config.toml na just few lines, but e hold the database password, so handle am the same way.
Upgrades get one order. Stop the service, take a dump, replace the binary for /usr/bin, run listmonk --config /etc/listmonk/config.toml --upgrade, then start the service. Schema migrations only dey move forward, so that dump na your only way to go back.
Why Listmonk no dey start?
Read the journal first with journalctl -u listmonk -n 50 --no-pager. Almost every startup failure dey show as one line inside the [db] block.
pq: password authentication failed for user "listmonk" mean say the password for [db] no match the Postgres role. The pq prefix na the Postgres driver wey dey report say server reject am. So config read correctly, but the credentials wrong. Reset the role with sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';" and put the same exact string for the file.
pq: database "listmonk" does not exist mean say the database value for [db] no point to real database. sudo -u postgres psql -l go list wetin dey actually for the server, including the spelling wey you use by mistake.
permission denied during --install mean say the role fit connect, but e no own the database. Because of that, e no fit create tables inside am. Fix am with sudo -u postgres psql -c "ALTER DATABASE listmonk OWNER TO listmonk;", then run the install again.
The service no ever start and the journal name the config file. Process wey dey run as listmonk no fit open config.toml wey remain as root:root with mode 600. stat -c '%U:%G %a' /etc/listmonk/config.toml suppose print root:listmonk 640, and the directory above am suppose be root:listmonk 750.
The panel dey work but no mail dey arrive. That one no be startup problem. Check Settings and SMTP first. Then check the campaign own log for the admin panel. The log record the error wey mail server return for each attempt.
FAQ
I need my own mail server before I fit use Listmonk?
No. Listmonk no be mail server. E need SMTP credentials for server wey accept your mail and deliver am. E fit be transactional relay or mail server wey you run yourself. Set the credentials under Settings and SMTP for the admin panel, no be for config.toml, because mail settings dey inside database. Use port 587 with STARTTLS or port 465 with implicit TLS, because most VPS providers dey block outbound port 25 for new accounts.
Why links for my campaign dey point to localhost:9000?
The root URL setting still dey use the install default of http://localhost:9000. Listmonk dey write that value inside unsubscribe links and media URLs when e send campaign. Open Settings for the admin panel, set the root URL to your real HTTPS address, then save am. You no fit correct messages wey don already deliver, so send test campaign to yourself and click the unsubscribe link inside am before you mail real list.
If I run --install again, e go wipe my subscribers?
Yes. --install na the first-time installer and e dey drop the existing Listmonk schema, while --yes dey remove the prompt wey for warn you. For any script wey fit run twice, use --install --idempotent --yes. E no go do anything when the tables already exist. To apply schema changes for new release, stop the service, take a pg_dump, then run --upgrade.
Why Listmonk dey say password authentication failed for user listmonk?
The password inside the [db] block of /etc/listmonk/config.toml no match the Postgres role wey get the same name. The journal line na pq: password authentication failed for user "listmonk", while pq na the Postgres driver passing the server rejection across. This one mean say e find and read the config file. Reset the role password with sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';", write the same string inside the config file, then run sudo systemctl restart listmonk.