# 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.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- Docker Engine with the Compose plugin: [Install Docker on Ubuntu](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-debian).
- Caddy installed on the host as described in [Caddy as a reverse proxy](/guides/caddy-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:

| 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:

```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'
```

> **Note**
>
> If `grep` finds no match, the upstream file has changed since this guide was written. Open it with `nano docker-compose.yml` and make the same two changes by hand. The PostgreSQL port stays bound to `127.0.0.1:5432` as shipped, so it is not reachable from the internet.

## 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](/guides/nginx-reverse-proxy-certbot) or [Traefik](/guides/traefik-reverse-proxy) and proxy to the same address.

> **Tip**
>
> listmonk's configuration docs list which routes must be public (`/subscription/*`, `/link/*`, `/campaign/*`, `/public/*`, `/webhooks/service/*`, `/uploads/*`) and which are private (`/admin/*`, `/api/*`, `/webhooks/bounce`). If you put an auth proxy or web application firewall in front, keep the public routes open so subscription forms, tracking links and unsubscribe pages keep working.

## 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**.

> **Note**
>
> 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: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

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.

> **Warning**
>
> The restore below deletes the current listmonk database. Run it only on a server where you intend to replace the data, and take a fresh dump first if anything there matters.

```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](https://github.com/knadh/listmonk/releases) 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.

### 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](/guides/install-mautic).
- Learn more about Compose files in [Docker Compose basics](/guides/docker-compose-basics).
- Host more apps behind the same proxy with [Caddy as a reverse proxy](/guides/caddy-reverse-proxy).
- See servers for newsletters and campaigns on the [email marketing hosting](/email-marketing-hosting) page.
- Explore templating, the API and roles in the [listmonk documentation](https://listmonk.app/docs/).

## 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.

---

Source: <https://hyperdc.com/guides/tutorials/install-listmonk>\
Updated: 2026-10-09
