Kituwa IT · Self-hosting guides

Install listmonk with Docker Compose

Self-hosted newsletter and mailing lists

newsletteremailcampaigns

listmonk is a self-hosted newsletter and mailing list manager: subscribe forms, templates, segments, and campaign analytics, with a real Postgres database behind it. It's the tool for people who send a weekly update and don't want to pay Mailchimp for the privilege.

Category
Productivity
License
AGPL-3.0
Needs
Postgres
Image
listmonk/listmonk

What it is

Double opt-in and unsubscribe handled properly, subscriber segments you can target, a template editor with merge tags, and per-campaign open/click/unsubscribe rates. It also accepts existing lists by CSV import, so migrating from a hosted provider is a file upload.

Before you start

  • Sending bulk email from a home server is a reputation minefield. You need a domain with SPF, DKIM and DMARC aligned, and a dedicated sending IP. Without those you'll land in spam and there's no code fix.
  • Use a transactional provider's SMTP for actual delivery, and listmonk for list management. Trying to deliver tens of thousands of messages from your own residential IP will not work.
  • Double opt-in isn't optional if you're in the EU. It's built in, so leave it on.

1 — Create the directory

Command / configuration
mkdir -p ~/services/listmonk/{data,pgdata,static}
cd ~/services/listmonk
openssl rand -hex 32
openssl rand -base64 24

2 — Write the compose file

Command / configuration
services:
  app:
    image: listmonk/listmonk:latest
    container_name: listmonk
    restart: unless-stopped
    ports:
      - "127.0.0.1:9000:9000"
    environment:
      LISTMONK_app__address: 0.0.0.0:9000
      LISTMONK_app__admin__user: CHANGE_ME_admin
      LISTMONK_app__admin__password: CHANGE_ME
      LISTMONK_app__db__user: listmonk
      LISTMONK_app__db__password: CHANGE_ME
      LISTMONK_app__db__database: listmonk
      LISTMONK_app__db__host: db
      LISTMONK_app__db__port: 5432
      LISTMONK_app__db__ssl_mode: disable
      LISTMONK_app__site__name: My Newsletter
      LISTMONK_app__site__url: https://news.example.com
    volumes:
      - ./data:/var/lib/listmonk
      - ./static:/listmonk/static
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17-alpine
    container_name: listmonk-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: listmonk
      POSTGRES_PASSWORD: CHANGE_ME
      POSTGRES_DB: listmonk
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U listmonk"]
      interval: 10s
      timeout: 5s
      retries: 5

The LISTMONK_ prefix with double underscores maps to nested config keys. If a setting looks ignored, check the underscores — the same convention Gitea uses.

3 — Start it

Command / configuration
docker compose up -d
docker compose logs -f app | grep -i "listening\|migrat\|error"

4 — First-run setup

  1. Log in at https://news.example.com with the admin credentials above, and change the password.
  2. Under Settings → SMTP, enter your provider's credentials. Send a test campaign to yourself with Send test email before doing anything else — check the headers with dig +short TXT _dmarc.yourdomain.com and verify SPF and DKIM pass.
  3. Import your existing list, or create a subscription page under Lists → your list → Opt-in forms and embed the hosted form.
  4. Build a template. The editor is a block editor; the useful part is the merge tags — {{ .Subscriber.Name }}, {{ .Subscriber.Attribs.city }} for custom fields.
  5. Send your first real campaign to a small segment. Watch the open rate in the analytics, then check the Sent log for any bounces and let listmonk clean the list.

5 — Keep the list clean

Command / configuration
# export the list regularly - it's your data
docker exec listmonk-app sh -c 'listmonk subscriber export --with-subscriptions -o /tmp/list.csv' \
  && docker cp listmonk-app:/tmp/list.csv ./list-$(date +%F).csv

# back up
docker exec -u postgres listmonk-db pg_dump -U listmonk listmonk | gzip > listmonk-$(date +%F).sql.gz
Unsubscribe links must actually workA broken or missing unsubscribe link is a spam complaint waiting to happen, and it will cost you the sending reputation you've spent months building. Always use listmonk's {{ .UnsubscribeURL }} merge tag in every template and test it in a real send. Never hand-roll the link.

Help when you need it

Want help getting this running?

We can help with a supported Linux host, application setup, migration or troubleshooting. Contact us to confirm the software, scope and scheduling before ordering.

Related guides

← Browse all 30 guides · Back to top