Skip to content

TutorialsBusiness apps

How to install listmonk with Docker Compose and HTTPS

Self-host the listmonk newsletter manager with Docker Compose and PostgreSQL, serve it over HTTPS with Caddy, connect SMTP and set up backups and upgrades.

  • Intermediate
  • 40 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Download the official Compose file
  3. Step 2 — Set secrets and keep the app port private
  4. Step 3 — Start listmonk and create the Super Admin
  5. Step 4 — Serve listmonk over HTTPS with Caddy
  6. Step 5 — Set the root URL and media storage
  7. Step 6 — Connect an SMTP server
  8. Step 7 — Turn on bounce processing (optional)
  9. Back up and restore
  10. Update listmonk
  11. Troubleshooting
  12. password authentication failed for user "listmonk"
  13. Bind for 127.0.0.1:5432 failed: port is already allocated
  14. Links in emails point to localhost or the wrong address
  15. Test connection fails in the SMTP settings
  16. Caddy returns 502 Bad Gateway
  17. Next steps

listmonk is a self-hosted newsletter and mailing list manager: you keep subscribers, lists, templates and campaign statistics in your own PostgreSQL database and send through any SMTP service. It suits teams that want a fast, single-binary alternative to hosted newsletter tools, and it also has an API for transactional messages.

This guide installs listmonk with the official Docker Compose file from the project repository. You keep the app on 127.0.0.1:9000, publish it over HTTPS with Caddy, create the Super Admin account safely, set the root URL and media storage, connect an SMTP relay, and optionally turn on bounce processing. It finishes with backups, restore, upgrades and troubleshooting.

Prerequisites

  • A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13.
  • A non-root user with sudo rights. If you have not set one up yet, follow Secure a new Linux server and Set up SSH keys.
  • Docker Engine with the Compose plugin: Install Docker on Ubuntu or Install Docker on Debian.
  • Caddy installed on the host as described in Caddy as a reverse proxy.
  • A domain name such as listmonk.example.com with an A record (and AAAA record if you use IPv6) pointing at your server.
  • Access to an SMTP service for sending, for example a transactional email provider, with its host, port, username and password.

listmonk needs PostgreSQL 12 or newer; the official Compose file already runs PostgreSQL 17 in a container. The project does not publish minimum CPU or memory requirements. The figures below are a conservative starting point, not an official or benchmarked number:

ResourceMinimum (official)Suggested starting point
CPUNot published1 vCPU
MemoryNot published1 GB RAM
DiskNot published10 GB plus room for the database and uploads
DatabasePostgreSQL 12 or newerPostgreSQL 17 from the official Compose file

Large lists and fast sending rates use more CPU and database I/O, so watch docker stats during your first big campaign and scale up if needed.

Step 1 — Download the official Compose file

Create the project folder, make your user its owner, and download docker-compose.yml from the listmonk repository exactly as the installation guide shows:

Bash
sudo mkdir -p /opt/listmonk && sudo chown $USER:$USER /opt/listmonk
cd /opt/listmonk
curl -LO https://github.com/knadh/listmonk/raw/master/docker-compose.yml
less docker-compose.yml

Read through the file before you run it. It defines two services: app (image listmonk/listmonk:latest) and db (image postgres:17-alpine with the named volume listmonk-data). The app is configured entirely with LISTMONK_* environment variables, and its start command runs --install --idempotent, then --upgrade, then the server. Media uploads are stored in ./uploads, which becomes /opt/listmonk/uploads on the host.

Step 2 — Set secrets and keep the app port private

The upstream file uses listmonk as the database password and publishes port 9000 on every address of the server. Create a .env file with a random database password and the credentials for the first Super Admin account, and make it readable only by you:

Bash
cd /opt/listmonk
echo "LISTMONK_DB_PASSWORD=$(openssl rand -hex 24)" > .env
echo "LISTMONK_ADMIN_USER=admin" >> .env
echo "LISTMONK_ADMIN_PASSWORD=$(openssl rand -base64 18)" >> .env
chmod 600 .env
cat .env

Copy the generated admin password into your password manager now. Then point the database password at the new variable and bind the app to the loopback address only:

Bash
sed -i 's/"9000:9000"/"127.0.0.1:9000:9000"/' docker-compose.yml
sed -i 's/&db-password listmonk/\&db-password ${LISTMONK_DB_PASSWORD}/' docker-compose.yml
grep -nE '9000:9000|db-password' docker-compose.yml

You should see the ports line as "127.0.0.1:9000:9000" and the POSTGRES_PASSWORD line ending in ${LISTMONK_DB_PASSWORD}. Because the file uses a YAML anchor, both PostgreSQL and listmonk pick up the same value. Confirm that Compose resolves it:

Bash
docker compose config | grep -E 'POSTGRES_PASSWORD|LISTMONK_db__password|published'

Step 3 — Start listmonk and create the Super Admin

Start both containers in the background:

Bash
docker compose up -d
docker compose ps
docker compose logs app --tail 30

docker compose ps should list listmonk_app and listmonk_db as running, with the database marked healthy. On this first start listmonk creates its tables and, because LISTMONK_ADMIN_USER and LISTMONK_ADMIN_PASSWORD are set, the Super Admin account. Check that the app answers locally:

Bash
curl -I http://127.0.0.1:9000/admin/login

You should get an HTTP status line such as HTTP/1.1 200 OK. The admin variables are only read on the very first start, so remove them from .env once you have stored the password:

Bash
sed -i '/^LISTMONK_ADMIN_/d' .env
cat .env

Step 4 — Serve listmonk over HTTPS with Caddy

Add a site block for your domain to /etc/caddy/Caddyfile. Caddy obtains and renews the TLS certificate automatically:

Caddyfile
listmonk.example.com {
    reverse_proxy 127.0.0.1:9000
}

Reload Caddy and test the public URL:

Bash
sudo systemctl reload caddy
curl -I https://listmonk.example.com/admin/login

Make sure the firewall only allows SSH and web traffic. Port 9000 does not need a rule, because it is bound to 127.0.0.1:

Bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

If you prefer Nginx or Traefik, use Nginx with Certbot or Traefik and proxy to the same address.

Step 5 — Set the root URL and media storage

Open https://listmonk.example.com/admin/login and sign in with the Super Admin account. Since version 4, listmonk uses this login page and supports several users with roles, so create separate accounts for colleagues under Settings → Users instead of sharing yours.

Go to Settings → General and set Root URL to https://listmonk.example.com, without a trailing slash. listmonk uses this public URL in every link it generates: unsubscribe and preference pages, tracking links, archive pages and media URLs. While you are there, set Default from email to an address on your sending domain and enter your address under Admin notification e-mails. Click Save; listmonk reloads its settings.

Then open Settings → Media. With the filesystem provider, set Upload path to /listmonk/uploads, the folder that the Compose file maps to /opt/listmonk/uploads on the host. The Upload URI is the public path under the root URL where these files are served; keep the default unless you have a reason to change it. Upload a test image under Campaigns → Media and check that it appears in /opt/listmonk/uploads.

Review Settings → Privacy as well. It controls options such as Individual subscriber tracking, Include List-Unsubscribe header, Allow exporting and Allow wiping, which decide what subscribers can do with their own data and what you record about them.

Step 6 — Connect an SMTP server

listmonk does not deliver mail itself: it hands every message to the SMTP servers you add under Settings → SMTP.

Open Settings → SMTP and fill in the details from your email provider or relay:

  • Host and Port: the relay's host name, for example smtp.example.com, and port 587.
  • Auth protocol, Username and Password: as given by the provider; many use an API key as the password.
  • TLS: STARTTLS, which port 587 expects.
  • HELO hostname: optional, a hostname that belongs to you.

Click Test connection and send a test message to yourself before you save. You can add more than one SMTP server and enable several at once. Retries sets how often a failed message is retried silently on other connections from the pool before it is logged as an error.

Your provider will ask you to publish SPF and DKIM records for the sending domain, and you should add a DMARC record. Without them, many receiving servers put your campaigns in spam or reject them.

Step 7 — Turn on bounce processing (optional)

Bounces tell you which addresses no longer accept mail. Go to Settings → Bounces and select Enable bounce processing. You then have two ways to receive bounces:

  • Webhooks (Enable bounce webhooks): listmonk has built-in endpoints for Amazon SES (/webhooks/service/ses), SendGrid (/webhooks/service/sendgrid), Postmark (/webhooks/service/postmark), Azure Communication Services, Forward Email and Lettermint. Configure the full URL, for example https://listmonk.example.com/webhooks/service/ses, in your provider's dashboard.
  • Mailbox (Enable bounce mailbox): listmonk scans a POP3 mailbox that receives bounces, for example a dedicated address set as the Return-Path header under Settings → SMTP → Custom headers.

For each bounce type (soft, hard, complaint) you choose a count and an Action. The Amazon SES example in the documentation uses soft bounces with a count of 2 and action none, and hard bounces and complaints with a count of 1 and action blocklist. Keeping your list clean this way protects your sender reputation.

Back up and restore

listmonk keeps nearly all of its state in PostgreSQL. A complete backup has three parts: a database dump, the uploads folder, and your docker-compose.yml and .env. The dump can be taken while listmonk is running:

Bash
sudo mkdir -p /opt/backups && sudo chown $USER:$USER /opt/backups && chmod 700 /opt/backups
cd /opt/listmonk
docker compose exec -T db pg_dump -U listmonk -Fc listmonk > /opt/backups/listmonk-db-$(date +%F).dump
tar czf /opt/backups/listmonk-files-$(date +%F).tar.gz -C /opt/listmonk docker-compose.yml .env uploads
ls -lh /opt/backups

The dump contains your subscribers' personal data, so keep the backup folder private and copy the files off the server, for example with rsync or to object storage. Schedule the two commands with cron once you have tested a restore.

To restore, unpack the files into /opt/listmonk, start only the database, recreate the empty database and load the dump. The .env file must contain the same password that the database volume was created with.

Bash
cd /opt/listmonk
tar xzf /opt/backups/listmonk-files-2026-10-09.tar.gz -C /opt/listmonk
docker compose stop app
docker compose up -d --wait db
docker compose exec -T db dropdb -U listmonk --if-exists listmonk
docker compose exec -T db createdb -U listmonk listmonk
docker compose exec -T db pg_restore -U listmonk -d listmonk < /opt/backups/listmonk-db-2026-10-09.dump
docker compose up -d

Replace the dates with those of your backup files. Sign in and check that your lists, subscribers and campaigns are back.

Update listmonk

Read the release notes first and take a database backup, because listmonk's upgrade guide asks for one before every upgrade and a database upgraded by a newer version should not be used with an older one. With the current Compose file, the documented upgrade is:

Bash
cd /opt/listmonk
docker compose down app
docker compose pull
docker compose up app -d
docker compose logs app --tail 30

The start command runs --upgrade automatically, so schema migrations happen when the new container starts. The image tag latest always follows the newest release; to stay on a specific version, replace it in docker-compose.yml with a release tag from the GitHub releases page, for example listmonk/listmonk:v6.2.0, and change it deliberately when you upgrade.

Major versions sometimes change the Compose file itself. Version 6, for example, renamed the database host from listmonk_db to db. Before a major upgrade, compare your file with the current upstream docker-compose.yml and carry over your two local changes.

Troubleshooting

password authentication failed for user "listmonk"

PostgreSQL sets the password only when it initialises an empty volume. If you started the stack before Step 2, the volume still uses the old password. On a fresh install with no data, remove the volume with docker compose down -v and start again. Otherwise set the new password inside the database with docker compose exec db psql -U listmonk -c "ALTER USER listmonk PASSWORD 'value-from-env';" and restart the app.

Bind for 127.0.0.1:5432 failed: port is already allocated

Another PostgreSQL server already listens on port 5432 on the host. listmonk reaches its database over the internal Docker network, so you can delete the ports: entry of the db service, or change it to "127.0.0.1:5433:5432", and run docker compose up -d again.

The Root URL under Settings → General is still the default. Set it to your HTTPS address without a trailing slash, save, and send a new test campaign; messages that were already sent keep their old links.

Test connection fails in the SMTP settings

Check host, port and TLS mode first: port 587 expects STARTTLS. Test whether the server can reach the relay with nc -vz smtp.example.com 587. A timeout means a firewall between the server and the relay drops the connection; an authentication error means wrong credentials or an API key without sending permission.

Caddy returns 502 Bad Gateway

listmonk is not answering on 127.0.0.1:9000. Run docker compose ps and docker compose logs app --tail 50 in /opt/listmonk. A crash loop at start usually means the app cannot reach the database or the .env file is missing.

Next steps

Frequently asked questions

Can listmonk send email on its own?

No. listmonk sends through the SMTP server you configure. Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request through a support ticket. Until then, send mail through an SMTP relay on port 587.

Do I have to run listmonk --install by hand with Docker?

No. The official docker-compose.yml starts the app with --install --idempotent and then --upgrade on every start, so the schema is created on an empty database once and upgraded automatically after you pull a new image.

How is the first admin account created?

Set LISTMONK_ADMIN_USER and LISTMONK_ADMIN_PASSWORD for the first docker compose up and listmonk creates the Super Admin automatically. Without them, the first person who opens the web interface creates it, which is why this guide sets them.

What do I need to back up?

The PostgreSQL database holds lists, subscribers, campaigns, templates and settings, so dump it with pg_dump. Also keep the uploads folder with your media files, docker-compose.yml and the .env file with the database password.

Which HyperDC servers can run listmonk?

listmonk runs in Docker on any HyperDC Linux VPS, VDS or dedicated server with root access and a supported Ubuntu or Debian release. Mail itself is delivered by the SMTP service you connect.

Sources

Genera password

Please confirm