SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-07

Ubuntu 24.04에 Listmonk 뉴스레터 서버 직접 구축하기

Ubuntu 24.04 환경에서 Listmonk를 설치하고 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 바이너리, 하나의 PostgreSQL 데이터베이스, 하나의 설정 파일, 하나의 systemd 유닛으로 구성됩니다. Listmonk는 구독자를 저장하고 캠페인 대기열을 관리할 뿐 메일을 직접 발송하지 않으므로, 소규모 VPS에서도 가볍게 실행할 수 있습니다. 각 메시지는 SMTP(Simple Mail Transfer Protocol) 서버로 전달되며, 따라서 메일 도달률은 이 소프트웨어가 아닌 해당 서버의 평판에 의해 결정됩니다.

이 가이드는 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 세션에서 역할과 데이터베이스를 생성합니다. -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는 단순한 장식이 아닙니다. 스키마 설치 과정에서 테이블, 타입, 인덱스, 함수가 생성되므로 해당 역할이 데이터베이스의 소유자여야 합니다. 다른 역할이 소유한 데이터베이스를 Listmonk에 연결하면 GRANT CONNECT을 실행했더라도 permission denied 오류와 함께 설치가 중단됩니다.

다음 단계로 넘어가기 전에 데이터베이스가 존재하는지 확인합니다.

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를 사용하는 경우 파일 이름에서 amd64arm64로 변경하십시오. listmonk --version을 실행하여 버전 문자열이 출력되면 해당 바이너리가 시스템과 호환된다는 첫 번째 증거입니다.

config.toml 생성 및 권한 제한

--new-config은(는) 현재 작업 디렉터리에 config.toml을(를) 생성합니다. 이것이 cd이(가) sudo 이전이 아닌 sh -c 내부에 위치하는 이유입니다.

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

생성된 파일은 짧습니다. [app] 아래에서 address = "localhost:9000"은(는) HTTP 서버를 루프백 주소에만 바인딩하므로, 리버스 프록시를 앞단에 배치하기 전까지는 인터넷에서 관리자 패널에 접근할 수 없습니다. 해당 줄은 그대로 두십시오. [db] 아래에는 host = "localhost", port = 5432, user = "listmonk", database = "listmonk", ssl_mode = "disable" 설정이 있습니다. 이 기본값들은 이미 생성한 데이터베이스와 일치하므로, 비밀번호 줄만 변경하면 됩니다.

Postgres가 동일한 서버의 루프백에서 대기 중일 때는 ssl_mode = "disable" 설정이 올바릅니다. 해당 트래픽이 서버 외부로 나가지 않기 때문입니다. 데이터베이스를 다른 호스트로 옮길 경우 require로 설정하십시오. 그렇지 않으면 비밀번호가 평문으로 네트워크를 통해 전송됩니다.

[db] 아래의 비밀번호 줄을 해당 역할에 맞게 수정하십시오. 그 후 서비스 계정을 생성하고 다른 모든 계정이 이 파일에 접근하지 못하도록 조치하십시오.

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

이제 서비스 계정만 파일을 읽을 수 있으며 다른 사용자는 접근할 수 없습니다.

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을(를) 출력합니다. 권한이 없는 다른 계정이 동일한 cat를 시도하면 Permission denied이(가) 발생합니다. 이것이 핵심입니다. 이 파일에는 데이터베이스 비밀번호가 평문으로 저장되어 있으며, 서버에는 보통 하나 이상의 계정이 존재하기 때문입니다. 동일한 논리가 운영하는 모든 서비스에 적용되므로, 최소 권한 서비스 사용자 문서를 읽고 모든 곳에 적용하십시오.

--install 플래그로 스키마 생성하기

--install은 테이블을 생성하고 기본 설정을 시드(seed)합니다. 환경 변수를 사용하여 첫 번째 관리자 계정을 설정하면, 패널에 접속하기 전에 이미 계정이 존재하게 됩니다.

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, templates, bounces 등이 포함됩니다. 두 번째 명령은 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을 실행하면 127.0.0.1:9000이 LISTEN 상태로 표시되어야 합니다. curl가 HTTP 상태 라인을 반환한다면 서버가 정상적으로 응답하고 있는 것입니다. curlConnection refused 오류와 함께 실패한다면 시작 과정에서 프로세스가 종료된 것이며, journalctl -u listmonk -n 50 --no-pager를 통해 그 이유를 확인할 수 있습니다. enable --now은 재부팅 후에도 유지되는 설정이라는 점을 유의하십시오. 수동으로 시작한 프로세스는 커널 업그레이드 이후에는 사라집니다.

Nginx와 TLS를 전면에 배치하기

Listmonk는 루프백 인터페이스에서 일반 HTTP로 통신하므로, Nginx를 사용하여 TLS(Transport Layer Security)를 종료하고 요청을 전달합니다.

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;
    }
}

구독자 가져오기나 미디어 업로드 시 파일 전송이 발생하므로 client_max_body_size 값을 높여야 합니다. Nginx는 기본적으로 1 MB를 초과하는 요청을 413 Request Entity Too Large 오류로 거부합니다. certbot을 사용하여 인증서를 발급받으십시오. certbot은 listen 443 ssl 설정 줄을 자동으로 작성하고 포트 80에서 리다이렉트 설정도 처리합니다. 자세한 단계는 Nginx용 Let's Encrypt 인증서 가이드를 참조하십시오. 포트 80과 443은 개방하되, 프록시가 루프백을 통해 접근하므로 9000번 포트는 닫아둡니다. 방화벽 설정이 되어 있지 않다면 ufw 방화벽 기초부터 시작하십시오.

그다음 관리자 패널을 열고 설정(Settings)에서 루트 URL을 https://lists.example.com로 지정합니다. 새로 설치하면 기본값이 http://localhost:9000로 설정되어 있는데, Listmonk는 이 값을 이메일에 포함되는 모든 수신 거부 링크와 미디어 URL에 삽입합니다. 이 값을 변경하기 전에 캠페인을 발송하면 수신자는 자신의 컴퓨터를 가리키는 링크를 받게 됩니다. 이 링크는 수신자 측에서 작동하지 않으며, 스팸 필터는 이를 도메인 설정이 미흡한 발송자로 간주합니다.

config.toml에 없는 SMTP 연결

config.toml에서 SMTP 섹션을 검색해도 찾을 수 없습니다. 메일 설정은 데이터베이스의 settings 테이블에 저장되며, 관리자 패널의 Settings 및 SMTP 메뉴에서 수정합니다. 생성된 파일이 짧게 유지되는 이유가 바로 이것이며, SMTP 설정을 변경해도 재시작이 필요 없는 이유이기도 합니다.

SMTP 서버 자체를 운영하는 방법에는 두 가지 현실적인 선택지가 있습니다. 직접 서버를 운영하는 경우 평판 관리의 책임이 전적으로 본인에게 있으며, 이는 그 자체로 상당한 작업이 필요합니다. Mailcow를 이용한 자체 메일 서버 운영 문서에서 관련 내용을 다룹니다. 또는 Listmonk를 트랜잭션 릴레이 서비스에 연결하여 IP 평판 관리를 외부 업체에 맡길 수도 있습니다.

어떤 방식을 선택하든 STARTTLS를 사용하는 587 포트나 암시적 TLS를 사용하는 465 포트를 사용하십시오. 아웃바운드 25 포트 사용은 고려하지 않는 것이 좋습니다. 대부분의 VPS 제공업체는 새 계정에서 기본적으로 이 포트를 차단합니다. 차단된 25 포트는 패킷이 거부되는 것이 아니라 드롭되므로 연결이 중단된 것처럼 보입니다. 이로 인해 클라이언트는 즉시 실패하지 않고 타임아웃이 발생할 때까지 대기하게 됩니다.

신뢰하기 전에 반드시 테스트하십시오. 목록을 생성하고 본인의 이메일 주소를 구독자로 추가한 뒤, 수신자 1명을 대상으로 캠페인을 발송하십시오. 수신된 메시지를 열어 전체 헤더를 확인하십시오. 수신 측에서 추가한 Authentication-Results 헤더를 통해 SPF 및 DKIM 통과 여부를 확인할 수 있습니다.

메일 도달률은 운영의 핵심입니다

Listmonk는 메시지를 작성하고, 수신자 목록을 관리하며, 메일을 발송하는 역할을 합니다. 메일이 수신함에 도달할지 여부는 전적으로 수신 측 제공업체가 결정하며, 이때 발신 IP 주소와 발신 도메인을 기준으로 판단합니다. 새로 할당받은 VPS IP는 아무런 이력이 없으며, 모든 대형 메일 제공업체는 이력이 없는 IP를 다소 의심스러운 것으로 간주합니다.

다음 네 가지는 필수 사항입니다.

  • 도메인에서 메일을 발송할 수 있는 호스트를 지정하는 SPF (sender policy framework) TXT 레코드.
  • TXT 레코드로 게시된 DKIM (domainkeys identified mail) 키. 이때 서명은 Listmonk가 아닌 메일 서버가 수행해야 합니다.
  • 앞선 두 설정이 실패했을 때 수신 측이 취할 행동을 지시하는 DMARC (domain based message authentication, reporting and conformance) 레코드.
  • Listmonk가 읽어 들일 바운스 메일함. 이를 통해 메일을 거부한 주소는 목록에서 자동으로 제외되어 무한 재시도를 방지합니다.

처음에는 천천히 발송하십시오. 메일 발송 이력이 없는 도메인이 갑자기 한 시간에 만 통의 메일을 보내는 것은 계정이 탈취된 전형적인 징후이므로, 스팸으로 필터링됩니다. 가장 활발하게 반응하는 구독자부터 시작하여 며칠에 걸쳐 발송량을 늘리십시오.

모든 템플릿에는 정상적으로 작동하는 수신 거부 링크가 필요합니다. Listmonk 템플릿에서 이는 {{ UnsubscribeURL }}이며, 캠페인 본문은 {{ template "content" . }}이 위치한 곳에 삽입됩니다. 이 태그는 템플릿당 정확히 한 번만 나타나야 합니다. 수신 거부 링크가 없는 캠페인은 수신 거부 대신 스팸 신고를 유발하며, 스팸 신고는 수주에 걸쳐 쌓아온 발신 평판을 가장 빠르게 잃는 지름길입니다.

백업과 복구 시 필요한 요소

서버 외부로 반드시 반출해야 할 데이터는 데이터베이스 덤프와 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을 실행한 다음 서비스를 시작하십시오. 스키마 마이그레이션은 앞으로만 진행되므로, 생성해 둔 덤프가 유일한 복구 수단입니다.

FAQ

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

먼저 journalctl -u listmonk -n 50 --no-pager 명령어로 저널을 확인하십시오. 거의 모든 시작 실패 원인은 [db] 블록의 한 줄에 나타납니다.

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

pq: database "listmonk" does not exist[db]database 값이 실제 데이터베이스를 가리키지 않음을 의미합니다. sudo -u postgres psql -l 명령어를 사용하면 서버에 실제로 존재하는 데이터베이스 목록을 확인할 수 있으며, 여기서 실수로 잘못 입력한 철자를 찾을 수 있습니다.

--install 과정에서 발생하는 permission denied은 해당 역할이 연결은 가능하지만 데이터베이스의 소유자가 아니어서 테이블을 생성할 수 없음을 의미합니다. sudo -u postgres psql -c "ALTER DATABASE listmonk OWNER TO listmonk;"으로 권한을 수정하고 설치를 다시 실행하십시오.

서비스가 전혀 시작되지 않고 저널에 설정 파일 이름이 언급되는 경우. listmonk 사용자로 실행 중인 프로세스는 모드 600으로 설정된 root:root 상태의 config.toml 파일을 열 수 없습니다. stat -c '%U:%G %a' /etc/listmonk/config.toml 명령어를 실행했을 때 root:listmonk 640가 출력되어야 하며, 해당 파일의 상위 디렉터리는 root:listmonk 750 권한이어야 합니다.

패널은 작동하지만 메일이 도착하지 않는 경우. 이는 시작 문제가 아닙니다. 먼저 설정(Settings)과 SMTP를 확인한 다음, 관리자 패널 내 캠페인 로그를 확인하십시오. 각 시도에 대해 메일 서버가 반환한 오류가 기록되어 있습니다.

FAQ

Listmonk를 사용하려면 자체 메일 서버가 필요한가요?

아니요. Listmonk는 메일 서버가 아닙니다. 메일을 수신하여 발송해 줄 서버의 SMTP 자격 증명이 필요하며, 이는 트랜잭션 릴레이 서비스이거나 직접 운영하는 메일 서버일 수 있습니다. 해당 자격 증명은 config.toml이 아닌 관리자 패널의 Settings 및 SMTP 항목에서 설정하십시오. 메일 설정은 데이터베이스에 저장되기 때문입니다. 대부분의 VPS 제공업체는 신규 계정에서 25번 포트의 아웃바운드 통신을 차단하므로, STARTTLS를 사용하는 587번 포트나 암시적 TLS를 사용하는 465번 포트를 사용하십시오.

캠페인 내 링크가 localhost:9000을 가리키는 이유는 무엇인가요?

루트 URL 설정이 설치 기본값인 http://localhost:9000로 유지되고 있기 때문입니다. Listmonk는 캠페인 발송 시점에 해당 값을 수신 거부 링크와 미디어 URL에 삽입합니다. 관리자 패널에서 Settings를 열고 루트 URL을 실제 HTTPS 주소로 변경한 뒤 저장하십시오. 이미 발송된 메시지는 수정할 수 없으므로, 실제 메일링 리스트에 발송하기 전에 테스트 캠페인을 스스로에게 보내 수신 거부 링크를 확인하십시오.

--install을 다시 실행하면 구독자 정보가 삭제되나요?

네. --install는 최초 설치 도구이며 기존 Listmonk 스키마를 삭제합니다. 또한 --yes은 경고 메시지를 생략하게 만듭니다. 두 번 이상 실행될 가능성이 있는 스크립트에서는 테이블이 이미 존재할 경우 아무 작업도 수행하지 않는 --install --idempotent --yes을 사용하십시오. 새 릴리스의 스키마 변경 사항을 적용하려면 서비스를 중지하고 pg_dump를 수행한 다음 --upgrade을 실행하십시오.

Listmonk에서 listmonk 사용자에 대한 비밀번호 인증이 실패했다고 나오는 이유는 무엇인가요?

/etc/listmonk/config.toml[db] 블록에 있는 비밀번호가 동일한 이름의 Postgres 역할 비밀번호와 일치하지 않기 때문입니다. 저널 로그에는 pq: password authentication failed for user "listmonk"이 기록되며, pq은 서버의 거부 응답을 전달하는 Postgres 드라이버의 메시지입니다. 이는 설정 파일이 정상적으로 발견되고 읽혔음을 의미합니다. sudo -u postgres psql -c "ALTER USER listmonk WITH PASSWORD 'new-password';"을 사용하여 해당 역할의 비밀번호를 재설정하고, 동일한 문자열을 설정 파일에 입력한 뒤 sudo systemctl restart listmonk를 실행하십시오.