SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor

Docker로 ntfy 자체 호스팅 서버 구축 및 알림 설정 방법

Docker Compose를 사용하여 VPS에 ntfy 서버를 직접 구축하는 방법을 설명합니다. TLS 설정부터 사용자 인증, ACL 적용, cron 및 systemd OnFailure를 통한 알림 자동화까지 실무에 필요한 핵심 구성 과정을 단계별로 안내합니다.

자체 호스팅 ntfy 서버의 역할

자체 호스팅 ntfy 서버는 HTTP POST 요청을 휴대폰의 푸시 알림으로 변환합니다. curl를 사용하여 메시지를 게시하면 Android 앱, iOS 앱, 브라우저 탭 또는 HTTP 연결을 유지할 수 있는 모든 곳으로 메시지가 전달됩니다. 별도의 클라이언트 라이브러리를 설치하거나 메시지 브로커를 실행할 필요가 없습니다.

ntfy는 토픽(topic)을 통해 메시지를 식별합니다. 토픽은 https://ntfy.example.com/alerts과 같이 URL 경로에 포함된 이름이며, 누군가 해당 토픽으로 메시지를 게시하는 순간 생성됩니다. 기본 설치 상태에서는 해당 이름을 아는 누구나 토픽을 읽고 쓸 수 있습니다. 이 때문에 프로젝트 공식 문서에서는 토픽 이름을 비밀번호에 비유합니다. 이러한 모델은 공개 ntfy.sh 서비스에는 적합하지만, 백업 실패 알림과 같은 중요한 정보를 다루는 서버에는 적합하지 않습니다. 따라서 이 가이드에서는 첫 메시지를 보내기 전에 인증 기능을 활성화합니다.

시작하기 전에 필요한 것

Ubuntu 24.04 또는 Debian 13이 설치된 VPS, Docker Engine 및 Compose 플러그인, 도메인 이름, 그리고 매우 적은 양의 RAM이 필요합니다. ntfy.example.com을 서버의 공인 IP 주소로 가리키는 DNS(Domain Name System) A 레코드를 생성하고, 다른 작업을 시작하기 전에 해당 도메인이 올바르게 확인되는지 먼저 확인하십시오.

dig +short ntfy.example.com
sudo ufw allow 80,443/tcp
sudo ufw status

dig는 서버의 IP를 출력해야 합니다. 인증 기관(Certificate Authority)은 외부에서 도메인 이름을 확인하므로, 아무것도 출력되지 않으면 인증서 발급이 실패합니다. Let's Encrypt의 기반 프로토콜인 ACME(Automatic Certificate Management Environment)가 HTTP 챌린지를 위해 80번 포트를 사용하므로, 이 포트는 열려 있어야 합니다. ntfy 컨테이너 자체는 공인 포트를 직접 노출하지 않습니다.

ntfy 설정 파일 작성

Docker 이미지에는 설정 파일이 포함되어 있지 않으므로 직접 생성해야 합니다. 이 가이드의 이후 모든 명령어는 이 파일을 참조합니다. 먼저 컨테이너를 실행할 사용자 ID와 그룹 ID를 확인하십시오.

id -u
id -g
sudo install -d -o "$(id -u)" -g "$(id -g)" /etc/ntfy /var/cache/ntfy /var/lib/ntfy
sudo nano /etc/ntfy/server.yml
base-url: "https://ntfy.example.com"
listen-http: ":2586"
behind-proxy: true
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
enable-login: true
enable-signup: false

이 중 4개의 설정 항목이 핵심입니다. base-url은 반드시 정확한 공용 HTTPS 주소여야 합니다. ntfy는 이 주소를 기반으로 첨부 파일 링크와 웹 앱의 자체 요청을 생성하므로, 값이 잘못되면 웹 앱이 로드된 후 모든 동작이 실패하게 됩니다. listen-http: ":2586"는 컨테이너 내부의 모든 인터페이스에 바인딩되는데, 이는 위험해 보일 수 있으나 올바른 설정입니다. 컨테이너는 고유한 네트워크 네임스페이스를 가지므로, 내부에서 127.0.0.1에 바인딩하면 호스트에서 해당 포트에 접근할 수 없게 되어 Docker가 게시한 포트가 연결되지 않습니다. auth-default-access: "deny-all"은 전체 보안 정책을 결정합니다. 명시적인 권한 부여 없이는 누구의 읽기 및 쓰기 요청도 거부하기 때문입니다. behind-proxy: true은 ntfy가 X-Forwarded-For 헤더에서 클라이언트 주소를 가져오도록 지시합니다. 이를 통해 리버스 프록시를 하나의 매우 바쁜 클라이언트로 간주하지 않고 실제 방문자별로 속도 제한(rate limit)을 적용할 수 있습니다.

enable-login: true를 사용하면 웹 앱과 휴대폰 앱에서 비밀번호로 로그인할 수 있습니다. enable-signup은 false로 유지하십시오. 개인 서버에서 누구나 계정을 생성할 수 있게 하는 것은 보안상 취약점을 열어두는 행위이기 때문입니다.

sudo chown "$(id -u):$(id -g)" /etc/ntfy/server.yml
sudo chmod 600 /etc/ntfy/server.yml

Docker Compose로 ntfy 실행하기

이 내용을 /opt/ntfy/compose.yaml에 넣고, 1000:1000를 위에서 출력된 두 숫자 id -uid -g로 바꿉니다.

services:
  ntfy:
    image: binwiederhier/ntfy:v2.27.0
    container_name: ntfy
    command: serve
    user: "1000:1000"
    environment:
      - TZ=UTC
    volumes:
      - /etc/ntfy:/etc/ntfy
      - /var/cache/ntfy:/var/cache/ntfy
      - /var/lib/ntfy:/var/lib/ntfy
    ports:
      - "127.0.0.1:2586:2586"
    restart: unless-stopped
cd /opt/ntfy
sudo docker compose up -d
sudo docker compose logs ntfy
curl -s http://127.0.0.1:2586/v1/health

정상적인 서버는 {"healthy":true}를 반환합니다. 해당 compose 파일에는 의도적인 세부 사항이 두 가지 있습니다. 이미지는 latest 대신 2026년 8월 기준 최신 릴리스인 v2.27.0으로 고정했습니다. latest을 사용하면 다음 docker compose pull가 서버 버전을 변경해 버리고, 사용자는 나중에 변경 로그를 보고서야 이를 알게 되기 때문입니다. 포트는 127.0.0.1:2586:2586으로 게시하여 컨테이너가 호스트의 루프백 주소에서만 접근 가능하도록 했습니다. 대신 2586:2586을 작성하면 Docker가 사용자의 방화벽 규칙보다 앞서 자체 방화벽 규칙을 삽입합니다. 즉, ufw status에서는 포트가 닫혀 있다고 표시되더라도 실제로는 인터넷에서 포트가 응답하게 됩니다.

curl이 Connection refused을 출력하면 컨테이너 로그를 읽어 보십시오. /var/lib/ntfy/user.db에서 권한 오류가 발생한다면 user: 줄이 해당 디렉터리의 소유자와 일치하지 않는다는 뜻이며, 이 경우 프로세스가 자체 데이터베이스를 생성하지 못하고 종료됩니다. VPS를 위한 Docker Compose 기초 가이드에서 볼륨 소유권과 재시작 정책을 더 자세히 다룹니다.

Caddy를 이용한 TLS 적용

Caddy는 인증서를 스스로 요청하고 갱신하므로, TLS(Transport Layer Security)를 가장 빠르게 적용할 수 있는 방법입니다.

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

/etc/caddy/Caddyfile의 내용을 다음 세 줄로 교체합니다.

ntfy.example.com {
    reverse_proxy 127.0.0.1:2586
}
sudo systemctl reload caddy
curl -s https://ntfy.example.com/v1/health

HTTPS를 통해 동일한 {"healthy":true}에 접속된다면 전체 경로가 정상적으로 작동하는 것입니다. Caddy에서 502 응답이 온다면 ntfy가 수신 대기 중이 아닐 가능성이 높으므로 sudo ss -lntp | grep 2586로 확인하십시오. 인증서 오류는 일반적으로 DNS 레코드가 잘못되었거나 80번 포트가 차단된 경우이며, sudo journalctl -u caddy -n 50에서 그 원인을 확인할 수 있습니다.

이미 nginx를 사용 중이라면 ntfy 문서에 명시된 프록시 설정인 proxy_http_version 1.1, proxy_buffering off, proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for을 복사하고, 읽기 및 전송 타임아웃을 최소 3분 이상으로 설정하십시오. 구독자는 수신 대기하는 동안 하나의 HTTP 연결을 계속 열어두는데, nginx는 기본적으로 60초가 지나면 유휴 업스트림 연결을 종료합니다. 이로 인해 구독자가 반복적으로 재연결을 시도하게 되며, 연결이 끊긴 사이에 전송된 메시지는 유실됩니다.

사용자 생성 및 토픽 권한 제한

인증 기능이 활성화되었으나 아직 아무도 접근할 수 없는 상태입니다. 이것이 의도한 바입니다. 본인을 위한 관리자 계정 하나와 스크립트용 머신 계정 하나를 생성하십시오. 이 명령어들은 컨테이너 내부에서 /etc/ntfy/server.yml를 읽어오며, 설정 파일이 볼륨 마운트로 연결된 이유가 바로 이것입니다.

sudo docker compose exec ntfy ntfy user add --role=admin admin
sudo docker compose exec ntfy ntfy user add robot
sudo docker compose exec ntfy ntfy user list

각 명령어는 비밀번호를 입력받습니다. 관리자는 접근 제어 목록을 무시하며 모든 토픽을 읽고 쓸 수 있으므로, 해당 계정은 본인과 휴대폰 앱용으로만 사용하십시오. robot는 권한을 부여하기 전까지는 아무런 접근 권한이 없는 일반 사용자입니다.

sudo docker compose exec ntfy ntfy access robot alerts write
sudo docker compose exec ntfy ntfy access robot "alerts_*" write
sudo docker compose exec ntfy ntfy access

ACL(접근 제어 목록) 항목은 사용자, 토픽, 권한으로 구성됩니다. 토픽은 고정된 이름이거나 *을 포함한 패턴일 수 있습니다. 예를 들어 alerts_*alerts_backupalerts_db를 모두 포함하므로 호스트마다 명령어를 따로 실행할 필요가 없습니다. write 권한은 게시 전용을 의미하므로, cron 작업에서 탈취된 토큰으로 구독하여 전송한 내용을 다시 읽어갈 수 없습니다. 특수 사용자명인 everyone은 인증되지 않은 방문자의 권한을 설정하며, ntfy access everyone status read와 같이 의도적으로 공개할 항목에만 사용해야 합니다.

스크립트는 비밀번호가 아닌 토큰을 사용해야 합니다.

sudo docker compose exec ntfy ntfy token add robot

이 명령어는 tk_으로 시작하는 토큰을 출력합니다. 토큰은 해당 사용자의 권한을 그대로 상속받으므로, 이 토큰은 alerts 토픽에 게시하는 작업만 수행할 수 있습니다. ntfy token list는 존재하는 토큰 목록을 보여주며, ntfy token remove은 사용자의 비밀번호를 변경하지 않고도 특정 토큰을 무효화합니다.

첫 메시지를 전송하여 잠금 기능이 작동하는지 확인하기

먼저 문이 닫혀 있는지 확인하는 것으로 시작합니다.

curl -s -o /dev/null -w '%{http_code}\n' -d "hello" https://ntfy.example.com/alerts

이 명령을 실행하면 403이 출력되며, 403이 올바른 응답입니다. auth-default-access: "deny-all"는 익명 게시를 거부하기 때문입니다. 이제 실제 메시지를 전송해 봅니다.

curl -H "Authorization: Bearer tk_REPLACE_WITH_YOUR_TOKEN" \
  -H "Title: Nightly backup finished" \
  -H "Priority: default" \
  -H "Tags: white_check_mark" \
  -d "42 GB copied in 11 minutes" \
  https://ntfy.example.com/alerts

서버는 저장된 메시지를 JSON 형식으로 응답하며, 이를 통해 메시지가 무시되지 않고 정상적으로 수락되었음을 알 수 있습니다. Title은 굵게 표시된 첫 번째 줄입니다. Priority은 1에서 5까지의 숫자나 min에서 urgent 사이의 이름으로 설정할 수 있으며, 휴대폰에서 알림음이 울릴지 여부를 결정합니다. Tags는 이름이 알려진 이모지 단축 코드와 일치할 경우 알림에서 이모지로 변환되며, 일치하지 않으면 일반 텍스트로 유지됩니다.

터미널에서 토픽을 모니터링하려면 스트림을 연결하십시오.

curl -s -u admin https://ntfy.example.com/alerts/raw

curl은 비밀번호를 요구합니다. 각 메시지는 한 줄씩 도착하며, 간헐적으로 나타나는 빈 줄은 연결 유지를 위한 신호입니다. 브라우저에서 https://ntfy.example.com를 열고 동일한 계정으로 로그인하면 웹 앱 버전의 동일한 스트림을 확인할 수 있습니다.

단일 스크립트가 서버를 마비시키지 않도록 속도 제한 설정하기

기본적으로 각 방문자는 60개의 요청을 담을 수 있는 버킷을 할당받으며, 5초마다 1개의 요청이 다시 채워집니다. 이는 개인 서버로서는 넉넉한 수준이지만, 재시도 루프에 빠진 스크립트는 이 자원을 모두 소진할 수 있습니다. server.yml에 제한 설정을 추가하십시오.

visitor-request-limit-burst: 30
visitor-request-limit-replenish: "10s"
visitor-message-daily-limit: 500
sudo docker compose restart ntfy

제한을 초과한 방문자는 메시지를 받는 대신 HTTP 429 응답을 받게 됩니다. 제한은 방문자 주소별로 계산되며, 이것이 바로 behind-proxy: true 설정이 매우 중요한 이유입니다. 이 설정이 없으면 ntfy는 Caddy의 주소만 인식하게 되어 모든 클라이언트가 동일한 방문자로 간주됩니다. 결과적으로 하나의 스크립트가 버킷을 모두 소진하면 사용자의 휴대폰이나 다른 서버까지 영향을 받게 됩니다.

실패한 cron 작업에 대한 알림

명령줄에 토큰을 노출하지 마십시오. ps aux는 시스템의 모든 사용자에게 실행 중인 모든 프로세스의 전체 명령줄을 보여주므로, -H으로 전달된 토큰은 curl이 실행되는 동안 모든 로컬 계정에서 읽을 수 있습니다. curl 설정 파일을 사용하면 이를 방지할 수 있습니다.

sudo install -d -m 700 /etc/ntfy-alert
printf 'header = "Authorization: Bearer tk_REPLACE_WITH_YOUR_TOKEN"\n' | sudo tee /etc/ntfy-alert/curlrc
sudo chmod 600 /etc/ntfy-alert/curlrc

이제 작업을 래핑하십시오. 이 내용을 /usr/local/bin/backup-with-alert.sh로 저장하고 chmod 750 권한을 부여하십시오.

#!/bin/bash
out=$(/usr/local/bin/backup.sh 2>&1)
code=$?
if [ "$code" -ne 0 ]; then
  printf '%s' "$out" | tail -c 1000 | curl -K /etc/ntfy-alert/curlrc \
    -H "Title: backup.sh failed with exit $code" \
    -H "Priority: high" \
    -H "Tags: warning" \
    --data-binary @- \
    https://ntfy.example.com/alerts
fi
exit "$code"
17 3 * * * /usr/local/bin/backup-with-alert.sh >> /var/log/backup-alert.log 2>&1

$?은 명령 바로 다음 줄에서 캡처됩니다. 다음 명령이 실행되면 덮어쓰이기 때문입니다. ntfy는 최대 메시지 크기를 제한하며 알림은 로그 뷰어가 아니므로, 출력은 tail -c 1000를 거쳐야 합니다. 마지막의 exit "$code"는 원래의 상태 코드를 보존하므로, 이 작업을 감시하는 다른 프로세스도 여전히 실패를 인지할 수 있습니다. 스크립트가 /bin/false을 가리키도록 설정하여 한 번 실행해 보고 전체 과정을 테스트하십시오.

실행되지 않는 실패 분기는 아예 알림이 없는 것보다 나쁩니다. 침묵이 곧 성공을 의미한다고 오해할 수 있기 때문입니다. Cron은 작업에 거의 비어 있는 환경과 로그인 셸보다 훨씬 짧은 PATH을 제공합니다. 따라서 직접 실행할 때는 잘 작동하던 스크립트가 curl 줄에 도달하기도 전에 종료될 수 있습니다. cron 작업이 실행되지 않는 이유에 대한 가이드에서 이러한 환경적 함정을 다룹니다. 모든 경로를 절대 경로로 사용하고, 성공했다고 가정하기보다는 첫 번째 예약 실행 후 로그 파일을 직접 확인하십시오.

systemd 유닛 실패 시 알림 설정

Cron은 예약된 작업을 처리합니다. 장시간 실행되는 서비스에는 OnFailure=이 필요하며, systemd는 유닛이 failed 상태에 진입할 때마다 이를 실행합니다. 템플릿 유닛을 하나 생성하여 서버의 모든 서비스에 재사용하십시오. 이 파일을 /etc/systemd/system/ntfy-unit-failed@.service으로 저장합니다.

[Unit]
Description=Send an ntfy alert because %i failed

[Service]
Type=oneshot
ExecStart=/usr/local/bin/ntfy-unit-failed %i

그런 다음 /usr/local/bin/ntfy-unit-failed을 실행하고 모드를 750으로 설정합니다.

#!/bin/bash
unit="$1"
journalctl -u "$unit" -n 15 --no-pager -o cat | tail -c 1000 | curl -K /etc/ntfy-alert/curlrc \
  -H "Title: $unit failed on $(hostname -s)" \
  -H "Priority: urgent" \
  -H "Tags: rotating_light" \
  --data-binary @- \
  https://ntfy.example.com/alerts

패키지 업그레이드 시 수정 사항이 덮어쓰이지 않도록 드롭인(drop-in) 파일을 사용하여 서비스에 연결합니다.

sudo systemctl edit myapp.service
[Unit]
OnFailure=ntfy-unit-failed@%n.service

%n는 전체 유닛 이름으로 확장되므로 인스턴스는 ntfy-unit-failed@myapp.service이 되며, 템플릿 내부의 %imyapp.service를 스크립트의 첫 번째 인자로 전달합니다. 이 덕분에 하나의 템플릿으로 모든 유닛을 관리할 수 있습니다. 의도적으로 실패하도록 만든 /etc/systemd/system/ntfy-selftest.service 유닛을 저장하여 정상 작동하는지 확인하십시오.

[Unit]
Description=Deliberately failing unit
OnFailure=ntfy-unit-failed@%n.service

[Service]
Type=oneshot
ExecStart=/bin/false
sudo systemctl daemon-reload
sudo systemctl start ntfy-selftest.service

시작 명령이 0이 아닌 값으로 종료되고 Job for ntfy-selftest.service failed because the control process exited with error code이 출력되면, 약 1초 뒤에 휴대폰으로 알림이 올 것입니다. 테스트가 끝난 후에는 해당 테스트 유닛을 삭제하십시오.

한 가지 주의할 점이 있습니다. OnFailure=은 유닛이 failed 상태에 도달할 때만 실행되는데, Restart=always이 설정된 서비스는 systemd가 계속 재시작을 시도하기 때문에 이 상태에 도달하지 못할 수 있습니다. 유닛은 StartLimitIntervalSec 시간 동안 StartLimitBurst번 이상 재시작된 후에야 실패 상태가 됩니다. 알림을 받고 싶은 모든 서비스에 이 두 값을 설정하십시오. 그렇지 않으면 서비스가 충돌을 반복하는 동안에도 알림 없이 며칠씩 방치될 수 있습니다. 타이머는 위에서 언급한 cron 패턴을 대체하는 더 깔끔한 방식입니다. 타이머의 서비스 유닛은 기본적으로 OnFailure=을 제공받기 때문이며, VPS에서 systemd 서비스와 타이머를 사용하는 방법 가이드에서 이를 변환하는 과정을 상세히 다룹니다.

동일한 토픽에 업타임 모니터 연결하기

Uptime Kuma, 셀프 호스팅 상태 모니터는 ntfy 알림 유형을 기본으로 제공합니다. Settings, Notifications, Setup Notification 순으로 이동한 뒤 Ntfy를 선택하십시오. 서버 URL을 https://ntfy.example.com로, 토픽을 alerts로 설정하고 우선순위를 지정한 다음 robot 액세스 토큰을 붙여넣습니다. 잘못된 토픽 이름을 사용하면 write 권한이 해당 토픽을 포함하지 않을 경우 오류 메시지 없이 실패하므로, 저장하기 전에 반드시 테스트 알림을 보내 확인하십시오.

이 구성의 현실적인 한계는 다음과 같습니다. 동일한 VPS에서 실행 중인 모니터는 해당 VPS가 다운되었을 때 이를 알릴 수 없으며, ntfy는 ntfy 자체가 다운되었을 때 알림을 전달할 수 없습니다. 모니터는 다른 장비에서 실행하고, ntfy를 감시하는 모니터를 위해 이메일과 같은 두 번째 알림 채널을 추가하십시오. Uptime Kuma의 Push 모니터 유형은 다른 사각지대를 보완합니다. 크론 작업이 성공적으로 실행된 후 푸시 URL을 호출하도록 설정하면, 해당 호출이 중단될 때 Kuma가 경고를 보냅니다. 실패 분기는 작업이 실행될 때만 작동하므로, 아예 시작되지 않은 작업에 대해서는 알릴 수 없습니다.

자체 호스팅한 ntfy는 Android와 iPhone에서 작동합니까?

Android에서는 아무런 제약 없이 작동합니다. Google Play나 F-Droid에서 앱을 설치하고, 설정(Settings)을 열어 기본 서버를 https://ntfy.example.com로 지정한 뒤, 사용자 관리 화면에서 계정을 추가하고 alerts를 구독하십시오. 즉시 전송(Instant delivery) 기능은 포그라운드 서비스를 실행 상태로 유지하므로 휴대폰이 Doze 모드일 때도 메시지가 도착합니다. 이때 표시되는 상시 알림은 버그가 아니라 Android의 포그라운드 서비스 요구 사항입니다. F-Droid 빌드에는 Firebase 코드가 전혀 포함되어 있지 않으므로 모든 구독이 즉시 전송 방식을 사용합니다. ntfy는 Google 푸시 서비스의 개방형 대체제인 UnifiedPush 배포자 역할도 수행할 수 있으므로, UnifiedPush를 지원하는 다른 앱들도 귀하의 서버를 통해 메시지를 전달받을 수 있습니다.

iOS에서는 제거할 수 없는 한 가지 의존성이 존재합니다. Apple은 APNs(Apple push notification service)를 통해서만 백그라운드 상태의 앱을 깨울 수 있으며, 앱의 서명 자격 증명을 보유한 주체만이 알림을 보낼 수 있기 때문에 귀하의 서버가 앱에 직접 도달할 방법은 없습니다. ntfy는 릴레이를 통해 이 문제를 해결합니다. 귀하의 서버가 메시지 ID를 포함한 poll_request을 ntfy.sh로 보내면, ntfy.sh가 이를 Firebase와 APNs를 거쳐 전달하여 앱을 깨우고, 앱이 귀하의 서버에서 메시지 본문을 가져오는 방식입니다.

upstream-base-url: "https://ntfy.sh"

이 과정에서 발생하는 비용을 명확히 이해해야 합니다. 메시지 내용은 귀하의 서버에 남지만, 메시지가 도착했다는 사실과 해당 ID는 귀하가 직접 운영하지 않는 인프라를 거쳐 전달됩니다. 이 설정을 사용하지 않으면 iPhone의 자체 호스팅 서버 알림은 앱을 깨울 방법이 없기 때문에 늦게 도착하거나 아예 도착하지 않습니다. 릴레이를 제거하는 유일한 방법은 귀하의 Apple 개발자 계정과 APNs 키를 사용하여 iOS 앱을 직접 빌드하고 배포하는 것인데, 이는 매년 비용이 발생하며 업데이트가 있을 때마다 다시 빌드해야 함을 의미합니다. 만약 릴레이 방식이 귀하의 사용 환경에 적합하지 않다면, Android나 데스크톱 웹 앱에서 알림을 확인하십시오.

백업, 업그레이드 및 이미지 고정

두 경로 /etc/ntfy/server.yml/var/lib/ntfy/user.db는 재생성할 수 없습니다. 두 번째 경로에는 모든 사용자, 비밀번호 해시, ACL 항목 및 토큰이 포함되어 있으므로 개인 키처럼 취급해야 합니다.

sudo tar czf ntfy-backup.tgz -C / etc/ntfy var/lib/ntfy
sudo chmod 600 ntfy-backup.tgz

해당 파일을 서버 외부로 복사하십시오. cache.db에는 최근 메시지만 저장되며, 위에서 언급한 cache-duration를 기준으로 12시간 분량만 보관되므로 손실되어도 보호할 가치가 없습니다. 업그레이드는 compose 파일의 태그를 수정하고 이미지를 pull하여 수행합니다.

sudo docker compose pull
sudo docker compose up -d
curl -s https://ntfy.example.com/v1/health

먼저 릴리스 노트를 읽어보십시오. SQLite 데이터베이스는 시작 시 마이그레이션되므로, 스키마 변경 후 이전 태그로 롤백하는 것은 안전하지 않습니다. 새 버전이 하루 동안 정상적으로 실행될 때까지 방금 생성한 백업을 보관하십시오.

Gotify 및 Apprise

Gotify는 더 가벼운 선택지입니다. 웹 UI와 Android 앱을 포함한 단일 바이너리로 구성되며, 토픽 와일드카드를 지원하지 않고 공식 iOS 클라이언트가 없습니다. Android만 사용하는 개인 서버에 적합합니다. Apprise는 서버가 아니라 Python 라이브러리이자 명령줄 도구입니다. ntfy를 포함한 100개 이상의 서비스로 메시지를 동시에 전송하므로, 여러 곳에 알림을 보내야 하는 스크립트에 적합합니다. ntfy는 서버, HTTP API, 양대 모바일 플랫폼용 앱을 모두 제공합니다. 이것이 바로 임대 서버에서 알림을 보낼 때 ntfy가 주로 선택되는 이유입니다.

FAQ

ntfy 서버에 게시할 때 403 오류가 발생하는 이유는 무엇입니까?

server.yml 내의 auth-default-access: "deny-all" 설정으로 인해 익명 게시가 거부되며, 이는 의도된 동작입니다. -u user:pass 또는 -H "Authorization: Bearer tk_..."을 사용하여 자격 증명을 전송하십시오. 이미 토큰을 전송하고 있음에도 403 오류가 발생한다면, 해당 토큰의 사용자가 해당 토픽에 대한 일치하는 ACL 항목을 가지고 있지 않은 것입니다. ntfy access을 실행하여 전체 목록을 출력하십시오. write 권한은 구독을 허용하지 않으므로, 게시 권한이 있는 계정이라도 동일한 토픽을 읽으려 할 때는 거부될 수 있음을 유의하십시오.

자체 호스팅 ntfy 서버를 사용할 때 iPhone에서 알림이 작동합니까?

피할 수 없는 릴레이를 통해 작동합니다. Apple은 APNs(Apple push notification service)를 통해서만 앱을 깨우며, 앱 게시자만이 해당 서비스로 메시지를 보낼 수 있습니다. 따라서 ntfy는 메시지 ID가 포함된 poll_request를 ntfy.sh로 전달하고, ntfy.sh가 이를 기기로 릴레이합니다. server.yml에서 upstream-base-url: "https://ntfy.sh"을 설정하고 컨테이너를 재시작하십시오. 메시지 본문 자체는 여전히 귀하의 서버에서 가져옵니다. 이 설정이 없으면 iOS 알림이 지연되거나 아예 나타나지 않습니다.

cron 작업의 ntfy 알림이 도착하지 않는 이유는 무엇입니까?

먼저 curl 명령어를 단독으로 실행하여 토큰과 토픽이 올바른지 확인하십시오. 수동 실행 시에는 작동하지만 cron에서 작동하지 않는다면, 실패 원인은 알림 이전 단계에 있습니다. cron은 최소한의 환경과 짧은 PATH로 작업을 실행하므로, 명령어를 전체 경로 없이 호출하는 스크립트는 curl 라인에 도달하기 전에 종료될 수 있습니다. 절대 경로를 사용하고, 작업의 출력을 로그 파일로 리다이렉트한 뒤 다음 실행 후에 해당 파일을 확인하십시오. 전송 대신 429 응답이 온다면 속도 제한(rate limit)이 작동 중이며 스크립트가 너무 빠르게 재시도하고 있다는 의미입니다.

ntfy를 공용 인터넷에 노출해야 합니까?

모바일 앱이 이동통신망에서 서버에 접근해야 하므로, auth-default-access: "deny-all"과 토픽별 ACL을 적용한 공용 HTTPS 엔드포인트가 일반적인 구성입니다. 어떤 토픽도 everyone에게 읽기 권한을 주지 않는다면 안전합니다. 모든 구독자가 직접 관리하는 기기라면 VPN 전용 인스턴스도 합리적입니다. 하지만 전화기의 경우 터널이 연결되어 있을 때만 앱이 알림을 수신하므로, 전화기가 다시 연결될 때까지 알림이 대기하게 되어 적합하지 않습니다.