SSD Nodes Learn 8GB RAM — 연 $66
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-01

Ubuntu 24.04에서 Listmonk 자체 호스팅하기

Listmonk v6.2.0을 Ubuntu 24.04에 설치하고 PostgreSQL, config.toml, systemd, TLS와 SMTP를 연결합니다. 메일 전송률은 소프트웨어가 아닌 발신 평판에 좌우되며 구축에는 몇 주가 걸립니다.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Listmonk에서 자체 호스팅 뉴스레터를 운영하는 데 필요한 사항

Listmonk는 자체 호스팅 뉴스레터 및 메일링 리스트 관리 도구입니다. Go 바이너리 1개, PostgreSQL 데이터베이스 1개, 설정 파일 1개, systemd 유닛 1개로 구성됩니다. Listmonk는 구독자와 캠페인 대기열을 저장하지만 메일을 직접 전송하지 않으므로 소형 VPS에서도 문제없이 실행됩니다. Listmonk는 각 메시지를 SMTP (simple mail transfer protocol) 서버로 전달합니다. 따라서 메일 전송률은 이 소프트웨어가 아니라 SMTP 서버의 평판에 따라 결정됩니다.

이 가이드에서는 2026년 7월 기준 최신 릴리스인 Listmonk v6.2.0을 Ubuntu 24.04에 설치합니다. 공인 IP 주소가 있는 VPS, 직접 관리하는 도메인 이름, PostgreSQL 12 이상이 필요합니다. 설치에는 약 1시간이 걸립니다. 발신 평판을 구축하는 데는 몇 주가 걸리며, 이 내용은 문서 후반부에서 다룹니다.

PostgreSQL 설치 및 데이터베이스 생성

Ubuntu 24.04에는 자체 저장소를 통해 PostgreSQL 16이 제공됩니다. 이는 Listmonk에 필요한 버전보다 훨씬 최신입니다.

sudo apt update
sudo apt install -y postgresql curl
sudo systemctl enable --now postgresql

하나의 psql 세션에서 role과 데이터베이스를 생성합니다. -v ON_ERROR_STOP=1은 첫 번째 명령문이 실패하면 psql을 종료합니다. 따라서 오타가 있어도 일부만 구성된 상태를 완료된 것으로 착각하지 않게 됩니다.

sudo -u postgres psql -v ON_ERROR_STOP=1 <<'SQL'
CREATE USER listmonk WITH PASSWORD 'pick-a-long-random-password';
CREATE DATABASE listmonk OWNER listmonk;
SQL

OWNER listmonk은 장식이 아닙니다. 스키마 설치 과정에서 테이블, type, index 및 function이 생성되므로 role이 데이터베이스의 소유자여야 합니다. 다른 role이 소유한 데이터베이스를 Listmonk에 지정하면 permission denied이 표시되며 설치가 중단됩니다. GRANT CONNECT을 실행한 후에도 마찬가지입니다.

계속 진행하기 전에 데이터베이스가 존재하는지 확인합니다.

sudo -u postgres psql -tAc "SELECT datname FROM pg_database WHERE datname='listmonk';"

그러면 listmonk이 출력됩니다. 빈 줄이 표시되면 CREATE 문이 실행되지 않은 것입니다. psql 출력을 다시 확인합니다.

Listmonk 바이너리 설치

Listmonk은 아키텍처별 정적 바이너리를 릴리스합니다. 먼저 현재 아키텍처를 확인해야 합니다. ARM VPS에서 amd64 바이너리를 실행하면 커널이 실행을 거부합니다.

dpkg --print-architecture
cd /tmp
curl -fsSLO https://github.com/knadh/listmonk/releases/download/v6.2.0/listmonk_6.2.0_linux_amd64.tar.gz
tar -xzf listmonk_6.2.0_linux_amd64.tar.gz
sudo install -m 755 listmonk /usr/bin/listmonk
listmonk --version

ARM VPS에서는 파일 이름의 amd64을(를) arm64(으)로 바꿉니다. listmonk --version 버전 문자열이 출력되면 바이너리가 시스템과 일치한다는 첫 번째 증거입니다.

config.toml 생성 및 접근 제한

--new-config은 현재 작업 디렉터리에 config.toml를 작성합니다. 따라서 cdsudo 앞이 아니라 sh -c 안에 있습니다.

sudo install -d -m 750 /etc/listmonk
sudo sh -c 'cd /etc/listmonk && listmonk --new-config'

생성된 파일은 짧습니다. [app] 아래에서 address = "localhost:9000"은 HTTP 서버를 loopback에만 바인딩하므로, reverse proxy를 앞에 배치하기 전에는 관리자 패널에 인터넷에서 접근할 수 없습니다. 해당 줄은 그대로 둡니다. [db] 아래에는 host = "localhost", port = 5432, user = "listmonk", database = "listmonk"ssl_mode = "disable"이 있습니다. 이 기본값은 이미 생성한 데이터베이스와 일치하므로 변경해야 하는 줄은 password뿐입니다.

Postgres가 같은 시스템에서 loopback으로 수신 대기하는 동안에는 ssl_mode = "disable"이 올바릅니다. 해당 트래픽은 시스템 외부로 나가지 않기 때문입니다. 데이터베이스를 다른 호스트로 이동하면 require로 설정해야 합니다. 그렇지 않으면 password가 네트워크를 통해 평문으로 전송됩니다.

[db] 아래의 password 줄을 role에 맞게 수정한 다음, service account를 만들고 다른 모든 login이 파일에 접근하지 못하도록 합니다.

sudo useradd --system --home-dir /var/lib/listmonk --create-home --shell /usr/sbin/nologin listmonk
sudo chown -R root:listmonk /etc/listmonk
sudo chmod 640 /etc/listmonk/config.toml

이제 service account만 파일을 읽을 수 있고 다른 사용자는 읽을 수 없습니다.

sudo -u listmonk cat /etc/listmonk/config.toml > /dev/null && echo readable
stat -c '%U:%G %a' /etc/listmonk/config.toml

첫 번째 명령은 readable을 출력합니다. 두 번째 명령은 root:listmonk 640을 출력합니다. 권한이 없는 다른 account가 동일한 cat을 시도하면 Permission denied을 받습니다. 이것이 목적입니다. 이 파일에는 데이터베이스 password가 평문으로 저장되며, 일반적으로 서버에는 둘 이상의 login이 존재합니다. 동일한 원칙은 실행하는 모든 service에 적용됩니다. 따라서 최소 권한 service 사용자를 읽고 모든 곳에 적용합니다.

--install로 스키마 생성

--install은 테이블을 생성하고 기본 설정을 초기화합니다. 환경 변수를 사용하여 첫 번째 관리자 로그인 정보를 설정합니다. 그러면 패널에 접근할 수 있게 되기 전에 계정이 생성됩니다.

sudo -u listmonk env LISTMONK_ADMIN_USER=admin \
  LISTMONK_ADMIN_PASSWORD='another-long-random-password' \
  listmonk --config /etc/listmonk/config.toml --install --yes

--yes는 확인 프롬프트에 응답합니다. 자동화하기 전에 해당 프롬프트를 한 번 확인해야 합니다. --install는 최초 설치 명령이며 기존 Listmonk 스키마를 삭제합니다. 운영 중인 데이터베이스에서 두 번째로 실행하면 구독자 데이터가 삭제됩니다. 두 번 실행될 수 있는 스크립트에서는 --install --idempotent --yes를 사용합니다. 테이블이 이미 있으면 아무 작업도 수행하지 않습니다. 새 릴리스에 포함된 스키마 변경 사항은 --upgrade로 적용하며, --install로 적용하지 않습니다.

브라우저가 아니라 데이터베이스 측에서 결과를 확인합니다.

sudo -u postgres psql -d listmonk -c '\dt'
sudo -u postgres psql -d listmonk -tAc "SELECT username FROM users;"

첫 번째 명령은 Listmonk 테이블을 나열합니다. 여기에는 subscribers, lists, campaigns, templatesbounces이 포함됩니다. 두 번째 명령은 admin를 출력합니다. 두 번째 명령의 결과가 비어 있으면 환경 변수가 프로세스에 전달되지 않은 것입니다. 이 경우 패널이 브라우저에서 첫 번째 사용자를 생성하라고 요청합니다.

systemd에서 Listmonk 실행

/etc/systemd/system/listmonk.service을 작성합니다.

[Unit]
Description=Listmonk newsletter and mailing list manager
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=listmonk
Group=listmonk
WorkingDirectory=/var/lib/listmonk
ExecStart=/usr/bin/listmonk --config /etc/listmonk/config.toml
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target

WorkingDirectory이 중요한 이유는 Listmonk가 파일 시스템 미디어 업로드 경로를 포함한 상대 경로를 이 경로를 기준으로 해석하기 때문입니다. After=postgresql.service은 시작 순서만 지정하며, Postgres가 연결을 수락할 때까지 기다리지 않습니다. 따라서 Restart=on-failure은 Listmonk가 너무 일찍 시작되어 연결하지 못하는 경우를 처리합니다.

sudo systemctl daemon-reload
sudo systemctl enable --now listmonk
ss -ltnp | grep 9000
curl -sI http://127.0.0.1:9000/

ss에는 LISTEN 상태의 127.0.0.1:9000이 표시되어야 합니다. curl이 HTTP 상태 줄을 반환하면 서버가 응답하고 있다는 뜻입니다. curlConnection refused과 함께 실패하면 시작 중에 프로세스가 종료된 것이며, journalctl -u listmonk -n 50 --no-pager에 원인이 표시됩니다. enable --now이 재부팅 후에도 유지되는 부분이라는 점에 유의합니다. 수동으로 시작한 프로세스는 다음 커널 업그레이드 후 종료됩니다.

nginx와 TLS를 앞에 배치

Listmonk는 loopback에서 일반 HTTP를 사용하므로 nginx가 TLS(전송 계층 보안)를 종료하고 요청을 전달합니다.

server {
    listen 443 ssl;
    server_name lists.example.com;

    client_max_body_size 25m;

    location / {
        proxy_pass http://127.0.0.1:9000;
        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;
    }
}

구독자 가져오기와 미디어 업로드는 파일 POST이므로 client_max_body_size 값을 높여야 합니다. nginx는 기본적으로 1 MB를 초과하는 요청을 413 Request Entity Too Large로 거부합니다. certbot으로 인증서를 발급하면 listen 443 ssl 줄과 port 80에서의 redirect도 자동으로 작성합니다. 절차는 nginx용 Let's Encrypt 인증서 가이드에 설명되어 있습니다. port 80과 443을 열고 port 9000은 닫아 둡니다. proxy가 loopback을 통해 해당 port에 접속하기 때문입니다. firewall을 아직 설정하지 않았다면 ufw firewall 기본 사항부터 진행합니다.

그런 다음 admin panel을 열고 Settings에서 root URL을 https://lists.example.com로 설정합니다. 새로 설치한 환경에는 http://localhost:9000가 설정되어 있으며, Listmonk는 이메일에 넣는 모든 unsubscribe link와 media URL에 이 값을 기록합니다. 값을 변경하기 전에 campaign을 보내면 모든 수신자에게 자신의 컴퓨터를 가리키는 link가 전달됩니다. 수신자에게는 link가 작동하지 않으며, spam filter에는 발신자가 자신의 domain을 제대로 설정하지 못한 것으로 보입니다.

config.toml에 없는 SMTP 연결 설정

config.toml에서 SMTP 섹션을 찾아도 찾을 수 없습니다. 메일 설정은 데이터베이스의 settings 테이블에 저장되며, Settings 및 SMTP 아래의 관리자 패널에서 편집합니다. 따라서 생성된 파일이 짧게 유지됩니다. 또한 SMTP를 변경해도 재시작할 필요가 없습니다.

SMTP 서버 자체를 운영하는 방법은 두 가지입니다. 직접 운영하면 평판을 전적으로 관리할 수 있지만, 그 자체로 별도의 실제 프로젝트가 됩니다. Mailcow로 자체 메일 서버 운영에서 필요한 작업을 설명합니다. 또는 Listmonk를 트랜잭션 릴레이로 지정하고 IP 평판 관리를 다른 업체에 맡길 수 있습니다.

어느 경우든 STARTTLS에는 port 587을 사용하고, 암시적 TLS에는 port 465를 사용합니다. 아웃바운드 port 25는 사용하지 않는 계획을 세워야 합니다. 대부분의 VPS 제공업체는 신규 계정에서 port 25를 기본적으로 차단합니다. 차단된 port 25는 패킷을 거부하지 않고 삭제하므로 연결이 멈춘 것과 정확히 동일하게 보입니다. 따라서 클라이언트는 즉시 실패하지 않고 timeout을 기다립니다.

신뢰하기 전에 테스트합니다. 목록을 만들고 자신의 주소를 subscriber로 추가한 다음, 수신자 1명에게 campaign을 보냅니다. 수신한 메시지를 열고 전체 header를 확인합니다. 수신 측에서 추가한 Authentication-Results header를 보면 SPF와 DKIM이 통과했는지 확인할 수 있습니다.

전달성은 전체 작업입니다

Listmonk는 메시지를 작성하고, 목록을 추적하며, 메일을 전달합니다. 해당 메일이 받은편지함에 도착하는지 여부는 모두 수신 제공업체가 발신 IP 주소와 발신 도메인을 기준으로 결정합니다. 새 VPS IP에는 이력이 전혀 없습니다. 모든 대형 메일함 제공업체는 이력이 없는 발신자를 약간 의심스럽게 봅니다.

다음 4가지는 필수입니다.

  • 도메인 대신 메일을 보낼 수 있는 호스트를 지정하는 SPF (sender policy framework) TXT 레코드입니다.
  • TXT 레코드로 게시하는 DKIM (domainkeys identified mail) 키입니다. 서명은 Listmonk가 아니라 메일 서버가 수행해야 합니다.
  • 앞의 2가지가 실패했을 때 수신자가 어떻게 처리할지 지정하는 DMARC (domain based message authentication, reporting and conformance) 레코드입니다.
  • Listmonk가 읽는 반송 메일함입니다. 메일을 거부한 주소가 계속 재시도되지 않고 목록에서 제거되도록 해야 합니다.

처음에는 천천히 발송합니다. 한 번도 메일을 보낸 적 없는 도메인이 갑자기 1시간에 10000개의 메시지를 전달하면, 정확히 침해된 계정과 같은 형태로 보입니다. 따라서 해당 메일은 침해된 계정의 메일처럼 필터링됩니다. 가장 적극적으로 참여하는 구독자부터 시작하고, 며칠에 걸쳐 발송량을 늘립니다.

모든 템플릿에는 작동하는 수신 거부 링크도 필요합니다. Listmonk 템플릿에서는 해당 링크가 {{ UnsubscribeURL }}이고, 캠페인 본문은 {{ template "content" . }}이 있는 위치에 삽입됩니다. {{ template "content" . }}은 템플릿마다 정확히 한 번 나타나야 합니다. 수신 거부 링크가 없는 캠페인은 수신 거부 대신 스팸 신고를 유발합니다. 스팸 신고는 몇 주 동안 쌓은 발신 평판을 잃게 만드는 가장 빠른 원인입니다.

백업과 복원에 실제로 필요한 항목

서버에서 외부로 보관해야 하는 항목은 2가지입니다. 데이터베이스 덤프와 config.toml입니다. 캠페인에 이미지를 업로드한다면 미디어 디렉터리도 추가합니다.

sudo -u postgres pg_dump -Fc listmonk > listmonk-$(date +%F).dump

이 덤프에는 구독자, 캠페인, 템플릿과 모든 설정이 저장됩니다. SMTP 자격 증명도 포함되므로 덤프를 암호화하고 이 서버 외부에 보관합니다. 백업 예약은 이미 해결된 문제입니다. 원격 저장소에 암호화된 restic 백업을 참조합니다. config.toml는 몇 줄에 불과하지만 데이터베이스 암호가 포함되므로 동일하게 처리합니다.

업그레이드는 정해진 순서로 진행합니다. 서비스를 중지하고 덤프를 생성한 다음, /usr/bin의 바이너리를 교체하고 listmonk --config /etc/listmonk/config.toml --upgrade을 실행한 후 서비스를 시작합니다. 스키마 마이그레이션은 정방향으로만 실행되므로, 이전 상태로 돌아갈 수 있는 유일한 방법은 이 덤프입니다.

Listmonk가 시작되지 않는 이유는 무엇입니까?

먼저 journalctl -u listmonk -n 50 --no-pager로 journal을 확인합니다. 거의 모든 시작 오류는 [db] 블록의 한 줄에 표시됩니다.

pq: password authentication failed for user "listmonk"[db]의 비밀번호가 Postgres role과 일치하지 않는다는 의미입니다. pq 접두사는 Postgres driver가 서버의 거부 응답을 보고하는 것입니다. 따라서 설정은 올바르게 읽혔고 credentials가 잘못된 것입니다. sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';"으로 role을 재설정하고 파일에 동일한 문자열을 입력합니다.

pq: database "listmonk" does not exist[db]database 값이 실제 database를 지정하지 않는다는 의미입니다. sudo -u postgres psql -l은 잘못 입력한 철자를 포함하여 서버에 실제로 있는 항목을 나열합니다.

--installpermission denied이 표시되면 role은 연결할 수 있지만 database의 소유자가 아니므로 테이블을 생성할 수 없다는 의미입니다. sudo -u postgres psql -c "ALTER DATABASE listmonk OWNER TO listmonk;"으로 수정한 후 설치를 다시 실행합니다.

서비스가 시작되지 않고 journal에 config 파일이 표시됩니다. listmonk로 실행되는 프로세스는 mode 600으로 설정된 root:rootconfig.toml를 열 수 없습니다. stat -c '%U:%G %a' /etc/listmonk/config.toml의 출력은 root:listmonk 640여야 하며, 그 상위 directory는 root:listmonk 750이어야 합니다.

panel은 작동하지만 메일이 도착하지 않습니다. 이는 시작 문제가 아닙니다. 먼저 Settings와 SMTP를 확인한 다음 admin panel에서 campaign 자체의 log를 확인합니다. 이 log에는 각 시도에 대해 mail server가 반환한 오류가 기록됩니다.

FAQ

Listmonk을 사용하려면 자체 mail server가 필요합니까?

아닙니다. Listmonk은 mail server가 아닙니다. 전자 메일을 수락하고 전달하는 server의 SMTP 자격 증명이 필요합니다. 이 server는 transactional relay일 수도 있고 직접 운영하는 mail server일 수도 있습니다. 관리자 패널의 Settings 및 SMTP에서 해당 자격 증명을 설정합니다. 메일 설정은 데이터베이스에 저장되므로 config.toml에는 설정하지 않습니다. 대부분의 VPS provider는 신규 계정에서 outbound port 25를 차단하므로, STARTTLS를 사용하는 port 587 또는 implicit TLS를 사용하는 port 465를 사용합니다.

root URL 설정이 아직 설치 기본값인 http://localhost:9000으로 되어 있기 때문입니다. Listmonk은 campaign을 전송하는 시점에 이 값을 unsubscribe link와 media URL에 기록합니다. 관리자 패널에서 Settings를 열고 root URL을 실제 HTTPS 주소로 설정한 다음 저장합니다. 이미 전달된 메시지는 수정할 수 없습니다. 따라서 실제 list로 메일을 보내기 전에 test campaign을 자신에게 보내고 해당 메시지의 unsubscribe link를 클릭합니다.

--install을 다시 실행하면 subscriber가 삭제됩니까?

예. --install는 최초 설치용 명령이며 기존 Listmonk schema를 삭제합니다. --yes은 삭제 전에 표시되는 경고 prompt를 제거합니다. 두 번 실행될 수 있는 script에서는 이미 table이 존재할 때 아무 작업도 하지 않는 --install --idempotent --yes을 사용합니다. 새 release에서 schema 변경 사항을 적용하려면 service를 중지하고 pg_dump을 생성한 다음 --upgrade을 실행합니다.

Listmonk에서 user listmonk의 password authentication failed라고 표시되는 이유는 무엇입니까?

/etc/listmonk/config.toml[db] block에 있는 password가 같은 이름의 Postgres role과 일치하지 않기 때문입니다. journal line은 pq: password authentication failed for user "listmonk"입니다. pq은 Postgres driver가 server의 거부 응답을 전달하고 있다는 뜻입니다. 따라서 config file은 찾아서 읽은 상태입니다. sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';"으로 role의 password를 재설정하고 동일한 문자열을 config file에 입력한 다음 sudo systemctl restart listmonk을 실행합니다.