SSD Nodes Learn 8GB RAM — $66/yr
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-02

How to Self-Host Listmonk for VPS Newsletter

Install Listmonk v6.2.0 for Ubuntu 24.04 with PostgreSQL, config.toml, systemd and TLS, then connect SMTP and see the real cost of delivery.

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

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 stress, because Listmonk dey store subscribers and queue campaigns, but e no dey deliver the mail by itself. E dey hand each message to an SMTP (simple mail transfer protocol) server, so na that server reputation dey decide your delivery rate, not this software own.

This guide dey 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. The installation dey take about one hour. The sending reputation dey take weeks, and we cover that part near the end.

Install PostgreSQL and create the database

Ubuntu 24.04 get PostgreSQL 16 for im own repository, and this version pass wetin Listmonk need.

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

Create the role and database inside one psql session. -v ON_ERROR_STOP=1 go make psql comot for the first statement wey fail, so typo no go leave you with setup wey look complete but no 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;
SQL

OWNER listmonk no be decoration. The schema install dey create tables, types, indexes and functions, so the role must own the database. If you point Listmonk to database wey another role own, the install 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 one go print listmonk. Empty line mean say the CREATE statement no run, so check the psql output again.

Install Listmonk binary

Listmonk dey release one static binary for each architecture. Check your own first, because amd64 binary for ARM VPS na file wey kernel no go run.

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

For ARM VPS, change amd64 to arm64 inside the file name. If listmonk --version print version string, na your first proof say the binary match the machine.

Generate config.toml and make am locked down

--new-config dey write config.toml inside the current working directory. Na why cd dey inside sh -c, and e no dey 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 the HTTP server to loopback only, so the admin panel no dey reachable from internet until you put reverse proxy for front of am. Leave that line as e dey. 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 the password line you must change.

ssl_mode = "disable" correct while Postgres dey listen on loopback for the same machine, because that traffic no dey comot from the machine. If you move the database go another host, set am to require, or the password go cross the network as cleartext.

Edit the password line under [db] make e match the role, then create the 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.toml

Now the 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.toml

The 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 the main point be this: this file hold your database password as cleartext, and server usually get more than one login. The same reason apply to every service wey you run, so read service users with least privilege once and apply am everywhere.

Create schema with --install

--install dey build the tables and put the default settings inside. 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 one time before you automate am, because --install na the first installer run and e go delete any existing Listmonk schema. If you run am again for a live database, e go destroy your subscribers. For any script wey fit run two times, use --install --idempotent --yes. E no dey do anything when the tables don already dey there. Schema changes wey come 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. Among dem na subscribers, lists, campaigns, templates and bounces. The second one dey print admin. If the second one return empty result, the environment variables no reach the process. That means 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.target

WorkingDirectory dey matter because Listmonk dey resolve relative paths, including filesystem media upload path, against am. After=postgresql.service only dey order the start; e no dey wait for Postgres to accept connections. So Restart=on-failure dey cover the case where Listmonk start small too early and 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 go survive reboot: process wey you start by hand go disappear after the next kernel upgrade.

Put nginx and TLS in front

Listmonk dey use 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;
    }
}

You need raise client_max_body_size 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 the redirect from port 80 for you: the steps dey the Let's Encrypt certificate guide for nginx. Open ports 80 and 443, and leave 9000 closed, because the proxy dey reach am through loopback. If you never configure the firewall, start with ufw firewall basics.

Then open the admin panel and set the root URL under Settings to https://lists.example.com. Fresh install dey carry http://localhost:9000, and Listmonk dey write that value inside every unsubscribe link and media URL wey e put for email. If you send campaign before you change am, each recipient go get links wey point to their own machine. The links no go work for the reader, and spam filter fit see dem as sign say the sender no fit configure their own domain.

Connect SMTP, wey no dey inside config.toml

Search config.toml for SMTP section, but 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.

For SMTP server itself, two options dey make sense. You fit run your own one. Then the reputation dey fully for your hand, but na serious project by itself: how to run your own mail server with Mailcow explain wetin e involve. Or point Listmonk to transactional relay, make another person manage the IP reputation.

Any way 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. When port 25 dey blocked, e 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 failing fast.

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. Na the receiving provider dey decide whether the mail go reach inbox, using the sending IP address and sending domain. New VPS IP no get any history, and every big mailbox provider dey see no history as small sign of suspicion.

Four things no be optional:

  • An SPF (sender policy framework) TXT record wey name the host wey get permission to send for your domain.
  • A DKIM (domainkeys identified mail) key wey you publish as TXT record, and na the mail server, no be Listmonk, go do the signing.
  • A DMARC (domain based message authentication, reporting and conformance) record, wey tell receivers wetin to do when the first two fail.
  • A bounce mailbox wey Listmonk dey read, so addresses wey reject mail go comot from the list instead of retrying forever.

Then send slowly at first. Domain wey never send mail before and suddenly deliver ten thousand messages within one hour get the exact pattern of compromised account, so filtering go treat am like one. Start with your most engaged subscribers and increase the volume over days.

Every template still need working unsubscribe link. For Listmonk template, na {{ UnsubscribeURL }} be the link, and campaign body go enter where {{ template "content" . }} dey, and {{ template "content" . }} must appear exactly once for each template. Campaign wey no get unsubscribe link go bring spam complaints instead of unsubscribes, and complaints na the fastest way to lose sending reputation wey you spend weeks building.

Backups, and wetin restore really need

Two things suppose comot from the box: 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).dump

That dump get subscribers, campaigns, templates and every setting, including the SMTP credentials, so encrypt am and keep am outside this server. Scheduling am na solved problem: see encrypted restic backups to remote storage. config.toml na just few lines, but e get 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 dey fail to start?

Read the journal first with journalctl -u listmonk -n 50 --no-pager. Almost every startup failure dey show for 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 Postgres driver wey dey report say the server reject am, so the config read correctly but the credentials dey wrong. Reset the role with sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';" and put the same string for the file.

pq: database "listmonk" does not exist mean say the database value for [db] no name any real database. sudo -u postgres psql -l go list wetin dey 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, so e no fit create tables inside am. Fix am with sudo -u postgres psql -c "ALTER DATABASE listmonk OWNER TO listmonk;" and run the install again.

The service no ever start and the journal name the config file. A process wey dey run as listmonk no fit open a 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 the mail server return for each attempt.

FAQ

I need my own mail server to use Listmonk?

No. Listmonk no be mail server. E need SMTP credentials for a server wey go accept your mail and deliver am. E fit be transactional relay or mail server wey you run by yourself. Set those credentials under Settings and SMTP for the admin panel, no be for config.toml, because mail settings dey inside the database. Use port 587 with STARTTLS or port 465 with implicit TLS, because most VPS providers dey block outbound port 25 for new accounts.

The root URL setting still dey at the install default of http://localhost:9000. Listmonk dey write that value inside unsubscribe links and media URLs when campaign dey send. 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. --yes dey remove the prompt wey for warn you. For any script wey fit run two times, use --install --idempotent --yes. E no do anything when the tables already dey exist. To apply schema changes for new release, stop the service, take a pg_dump, then run --upgrade.

Why Listmonk dey talk say password authentication failed for user listmonk?

The password for the [db] block inside /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", and pq na the Postgres driver wey dey pass the server rejection along. This mean say the config file don found and read. 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.