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
- Prerequisites
- Step 1 — Download the official Compose file
- Step 2 — Set secrets and keep the app port private
- Step 3 — Start listmonk and create the Super Admin
- Step 4 — Serve listmonk over HTTPS with Caddy
- Step 5 — Set the root URL and media storage
- Step 6 — Connect an SMTP server
- Step 7 — Turn on bounce processing (optional)
- Back up and restore
- Update listmonk
- Troubleshooting
- password authentication failed for user "listmonk"
- Bind for 127.0.0.1:5432 failed: port is already allocated
- Links in emails point to localhost or the wrong address
- Test connection fails in the SMTP settings
- Caddy returns 502 Bad Gateway
- 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
sudorights. 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.comwith 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:
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 1 vCPU |
| Memory | Not published | 1 GB RAM |
| Disk | Not published | 10 GB plus room for the database and uploads |
| Database | PostgreSQL 12 or newer | PostgreSQL 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:
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.ymlRead 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:
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 .envCopy 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:
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.ymlYou 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:
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:
docker compose up -d
docker compose ps
docker compose logs app --tail 30docker 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:
curl -I http://127.0.0.1:9000/admin/loginYou 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:
sed -i '/^LISTMONK_ADMIN_/d' .env
cat .envStep 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:
listmonk.example.com {
reverse_proxy 127.0.0.1:9000
}Reload Caddy and test the public URL:
sudo systemctl reload caddy
curl -I https://listmonk.example.com/admin/loginMake 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:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseIf 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 port587. - 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 examplehttps://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-Pathheader 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:
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/backupsThe 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.
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 -dReplace 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:
cd /opt/listmonk
docker compose down app
docker compose pull
docker compose up app -d
docker compose logs app --tail 30The 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.
Links in emails point to localhost or the wrong address
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
- Compare listmonk with a full marketing automation suite in How to install Mautic.
- Learn more about Compose files in Docker Compose basics.
- Host more apps behind the same proxy with Caddy as a reverse proxy.
- See servers for newsletters and campaigns on the email marketing hosting page.
- Explore templating, the API and roles in the listmonk documentation.
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.