feat(ntfy): 푸시 알림 서버 Phase 1 인프라 구축

- docker-compose.yml에 ntfy 서비스 추가 (포트 30750)
- ntfy/etc/server.yml 서버 설정 (인증 deny-all, 72h 캐시)
- ntfy/README.md 운영 매뉴얼

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-17 10:24:09 +09:00
parent 184cdd6aa8
commit e50ff3fb63
3 changed files with 211 additions and 0 deletions

View File

@@ -481,6 +481,25 @@ services:
networks: networks:
- tk-network - tk-network
# =================================================================
# ntfy — 푸시 알림 서버
# =================================================================
ntfy:
image: binwiederhier/ntfy
container_name: tk-ntfy
restart: unless-stopped
command: serve
ports:
- "30750:80"
environment:
- TZ=Asia/Seoul
volumes:
- ./ntfy/etc:/etc/ntfy
- ntfy_cache:/var/cache/ntfy
networks:
- tk-network
# ================================================================= # =================================================================
# AI Service — 맥미니로 이전됨 (~/docker/tk-ai-service/) # AI Service — 맥미니로 이전됨 (~/docker/tk-ai-service/)
# ================================================================= # =================================================================
@@ -544,6 +563,7 @@ services:
- tksafety-web - tksafety-web
- tksupport-web - tksupport-web
- tkeg-web - tkeg-web
- ntfy
networks: networks:
- tk-network - tk-network
@@ -563,6 +583,7 @@ volumes:
name: tkqc-package_uploads name: tkqc-package_uploads
tkeg_postgres_data: tkeg_postgres_data:
tkeg_uploads: tkeg_uploads:
ntfy_cache:
networks: networks:
tk-network: tk-network:
driver: bridge driver: bridge

167
ntfy/README.md Normal file
View File

@@ -0,0 +1,167 @@
# ntfy 푸시 알림 서버 — 운영 매뉴얼
## 개요
ntfy는 Web Push(VAPID)의 iOS 제한, 전송 보장 부재 등을 보완하는 푸시 알림 채널이다.
모바일 ntfy 앱으로 알림을 수신하고, 탭하면 딥링크로 해당 페이지로 이동한다.
- **Docker 서비스명**: `ntfy`
- **내부 URL**: `http://ntfy:80` (Docker 네트워크)
- **외부 URL**: `https://ntfy.technicalkorea.net` (Cloudflare Tunnel)
- **호스트 포트**: `30750`
- **설정 파일**: `ntfy/etc/server.yml`
- **데이터**: `ntfy_cache` Docker 볼륨 (`/var/cache/ntfy`)
## 초기 설정 (최초 1회)
### 1. Cloudflare Tunnel 설정
Zero Trust 대시보드 → Tunnels → Public Hostname 추가:
| Subdomain | Domain | Service |
|-----------|--------|---------|
| ntfy | technicalkorea.net | http://ntfy:80 |
### 2. 컨테이너 기동
```bash
docker compose up -d ntfy
```
### 3. 관리자 계정 + 토큰 발급
```bash
# 관리자 계정 생성 (비밀번호 입력 프롬프트)
docker exec -it tk-ntfy ntfy user add --role=admin admin
# API 토큰 발급
docker exec -it tk-ntfy ntfy token add admin
# 출력 예: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
```
### 4. .env에 토큰 설정
```env
NTFY_PUBLISH_TOKEN=<위에서 발급받은 토큰>
```
## 메시지 발행 (서버 → 사용자)
### curl로 테스트
```bash
# 기본 메시지
curl -H "Authorization: Bearer $NTFY_PUBLISH_TOKEN" \
-d "테스트 메시지입니다." \
http://localhost:30750/tkfactory-test
# 제목 + 딥링크 포함
curl -H "Authorization: Bearer $NTFY_PUBLISH_TOKEN" \
-H "Title: 설비수리 요청" \
-H "Tags: wrench" \
-H "Click: https://tkfb.technicalkorea.net/pages/admin/repair-management.html" \
-d "A동 CNC 설비 수리 요청이 접수되었습니다." \
http://localhost:30750/tkfactory-user-1
# 우선순위 높은 알림 (5=max, 3=default, 1=min)
curl -H "Authorization: Bearer $NTFY_PUBLISH_TOKEN" \
-H "Title: 긴급 안전 알림" \
-H "Priority: 5" \
-H "Tags: warning" \
-d "즉시 확인이 필요합니다." \
http://localhost:30750/tkfactory-user-1
```
### 토픽 네이밍 규칙
| 토픽 | 용도 |
|------|------|
| `tkfactory-user-{userId}` | 사용자별 개인 알림 (Phase 2에서 사용) |
| `tkfactory-test` | 테스트용 |
### 주요 헤더
| 헤더 | 설명 | 예시 |
|------|------|------|
| `Title` | 알림 제목 | `설비수리 요청` |
| `Click` | 탭 시 열릴 URL (딥링크) | `https://tkfb.technicalkorea.net/pages/work/tbm.html` |
| `Tags` | 이모지 태그 ([목록](https://docs.ntfy.sh/emojis/)) | `wrench`, `warning`, `white_check_mark` |
| `Priority` | 1(min) ~ 5(max) | `5` |
| `Authorization` | 인증 토큰 | `Bearer tk_...` |
## 사용자 관리
```bash
# 사용자 목록
docker exec tk-ntfy ntfy user list
# 일반 사용자 추가
docker exec -it tk-ntfy ntfy user add username
# 사용자 삭제
docker exec tk-ntfy ntfy user del username
# 특정 토픽 접근 권한 부여 (read-write / read-only / write-only)
docker exec tk-ntfy ntfy access username 'tkfactory-user-*' read-only
# 토큰 발급
docker exec tk-ntfy ntfy token add username
# 토큰 목록
docker exec tk-ntfy ntfy token list
# 토큰 삭제
docker exec tk-ntfy ntfy token remove username tk_...
```
## 모바일 앱 설정 (수신자용)
### Android
1. Play Store에서 **ntfy** 설치
2. 설정(⚙️) → **Default server**`https://ntfy.technicalkorea.net` 입력
3. 우측 상단 사용자 아이콘 → 로그인 (발급받은 계정/비밀번호 또는 토큰)
4. **+** → 토픽 `tkfactory-user-{본인userId}` 구독
### iOS
1. App Store에서 **ntfy** 설치
2. Settings → **Default server**`https://ntfy.technicalkorea.net` 입력
3. 로그인 후 토픽 구독 (Android와 동일)
## 서버 설정 (server.yml)
| 항목 | 현재 값 | 설명 |
|------|---------|------|
| `auth-default-access` | `deny-all` | 인증 없이 접근 불가 |
| `cache-duration` | `72h` | 메시지 보관 기간 (주말 포함 3일) |
| `visitor-request-limit-burst` | `60` | 버스트 요청 한도 |
| `visitor-request-limit-replenish` | `5s` | 요청 한도 보충 주기 |
설정 변경 후 컨테이너 재시작:
```bash
docker compose restart ntfy
```
## 트러블슈팅
### 401 Unauthorized
- 토큰이 맞는지 확인: `docker exec tk-ntfy ntfy token list`
- `auth-default-access: deny-all` 상태에서 토큰 없이 요청하면 발생
### 모바일 앱에서 알림이 안 옴
- Cloudflare Tunnel Public Hostname에 `ntfy.technicalkorea.net` 등록되었는지 확인
- 앱의 Default server URL이 `https://ntfy.technicalkorea.net`인지 확인
- 앱에서 로그인했는지, 토픽을 구독했는지 확인
- 폰 설정에서 ntfy 앱 알림 권한이 켜져 있는지 확인
### 컨테이너 로그 확인
```bash
docker logs tk-ntfy --tail 50
docker logs tk-ntfy -f # 실시간
```
## Phase 2 예정 사항 (참고)
- `system1-factory/api/models/notificationModel.js``sendPushToUsers()`에서 ntfy 발송 연동
- `push_subscriptions` 테이블에 `channel` 컬럼 추가 (`web_push` | `ntfy`)
- ntfy 구독 사용자에게는 Web Push 미발송 (중복 방지)
- notification-bell.js에서 ntfy 구독 토글 UI 추가

23
ntfy/etc/server.yml Normal file
View File

@@ -0,0 +1,23 @@
# ntfy server configuration
# Docs: https://docs.ntfy.sh/config/
# Base URL (Cloudflare Tunnel 경유)
base-url: "https://ntfy.technicalkorea.net"
# Listen on port 80 inside the container
listen-http: ":80"
# Cache — 72시간 (금요일 알림 → 월요일 확인 가능)
cache-duration: "72h"
cache-file: "/var/cache/ntfy/cache.db"
# Auth — 기본 접근 거부, 토큰 인증 필수
auth-default-access: "deny-all"
auth-file: "/var/cache/ntfy/user.db"
# Attachment (비활성화 — Phase 1에서는 텍스트 알림만)
# attachment-cache-dir: "/var/cache/ntfy/attachments"
# Rate limiting
visitor-request-limit-burst: 60
visitor-request-limit-replenish: "5s"