Docker Compose setup for listmonk.
Install Docker and Docker Compose:
curl -fsSL https://get.docker.com | shsudo mkdir -p /opt/listmonk-docker
sudo chown -R $USER:$USER /opt/listmonk-docker
git clone https://github.com/Afkanerd/listmonk-docker.git /opt/listmonk-docker
cd /opt/listmonk-dockercp .env.example .env
vim .envSet your database password and, optionally, the initial super admin credentials. listmonk does not take SMTP settings as environment variables, outbound mail is configured through the web UI after first boot (step 5).
chmod 600 .env compose.yaml
mkdir -p backups uploads && chmod 700 backups uploadsdocker compose up -d
docker compose psOn first boot, the app container installs the schema, applies migrations,
then starts. If LISTMONK_ADMIN_USER/LISTMONK_ADMIN_PASSWORD were set in
.env before this first run, the super admin account is created
automatically. Otherwise, create it from the web UI.
Log in at http://127.0.0.1:4000 (or your domain, see reverse proxy),
go to Settings > SMTP, add your relay, and send a test email. No mail of
any kind sends until this is configured.
The double opt-in email and its shared HTML/CSS wrapper are compiled into
the listmonk binary, they can't be edited from the admin UI. compose.yaml
mounts ./static-overrides into the container and passes
--static-dir to listmonk, which
merges these files over the built-in defaults file by file, anything you
don't touch keeps working as-is.
static-overrides/
├── email-templates/
│ ├── base.html # shared header/footer/CSS for all emails
│ ├── subscriber-optin.html # the double opt-in / welcome email body
│ └── subscriber-optin-campaign.html # used when a list has a custom opt-in campaign
└── public/
├── templates/optin.html # web page shown before confirming
└── static/style.css # CSS for public pages (opt-in, unsubscribe, archive)
subscriber-optin.html is already reworded as a welcome message rather than
a bare confirmation notice, use it as a starting point. Edit any file, then:
docker compose restart applistmonk only reads these files at startup, so a restart is required after every edit. See templating syntax and i18n if you want translated copy instead of hardcoded text.
Per-list opt-in behavior (single/double, opt-in campaign message) is set under Lists > your list > Opt-in in the admin UI. See concepts for how lists and opt-in types work.
server {
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:4000;
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;
}
}Backups run on the schedule in .env (default @daily) and are saved to
./backups as pg_dumpall
cluster dumps (roles and data, not just one database). Uses
postgres-backup-local.
docker exec listmonk_backup /backup.shWrites to ./backups/last/, daily/, weekly/, and monthly/, each with a
*-latest.sql.gz symlink to the newest file.
Cluster dumps include CREATE ROLE/CREATE DATABASE, so they must be
restored into an empty Postgres volume, not layered onto a live one. This is
a full point-in-time recovery, not a merge.
# stop app and db
docker compose stop app db
# wipe the postgres volume, this destroys current DB state
docker compose rm -f db
docker volume rm listmonk_db_data
# start postgres fresh, it recreates an empty listmonk role/db from .env
docker compose up -d db
# stream the dump in. two harmless errors are expected here:
# "role listmonk already exists" and "database listmonk already exists",
# since step 3 already created them. everything after that restores.
zcat ./backups/daily/listmonk-latest.sql.gz | docker exec -i listmonk_db psql -U listmonk -d listmonk
# bring the app back up
docker compose up -d app# logs
docker compose logs -f
# stop
docker compose down
# update
docker compose pull && docker compose up -dMigrations run automatically on startup, so no manual step is needed after an update.