카카오톡 같은 크로스플랫폼 메신저 ‘딩동(DingDong)’ 만들기 프로젝트 3탄!
이번 글에서는 우리 집 유그린 NAS(UGOS Pro)의 Docker 환경 위에 메신저의 핵심 데이터베이스인 PostgreSQL 16과 스마트폰 보안 통신(HTTPS)의 대문 역할을 할 Caddy 웹서버를 세팅하는 과정을 다룹니다.
특히, 집에서 이미 시놀로지 등 다른 서버가 443 포트를 선점하고 있을 때 해결하는 실전 노하우까지 완벽하게 정리했습니다.
1. Caddy 서버를 왜 설정해야 할까요?
처음 홈서버로 앱을 개발할 때 가장 많이 하는 질문입니다.
“백엔드 API 서버(NestJS)만 띄우면 되지, 왜 앞에 Caddy 같은 웹서버를 또 두나요?”
크게 3가지 이유 때문입니다:
- 모바일 앱의 필수 조건, 자동 HTTPS (SSL):
- 스마트폰 OS(Android, iOS)는 보안상
https://암호화가 안 된 일반http://주소와는 통신을 거부합니다. - Nginx는 무료 인증서(Let’s Encrypt)를 받으려면 Certbot 봇을 따로 깔고 주기적으로 갱신 스크립트를 돌려야 하지만, Caddy는 도메인만 적어두면 알아서 SSL 인증서를 발급받고 평생 자동으로 갱신해 줍니다.
- 스마트폰 OS(Android, iOS)는 보안상
- 역방향 프록시 (교통정리):
- 집 공유기에서 외부로 열 수 있는 HTTPS 포트는 기본
443번 하나입니다. - 하지만 NAS 안에는 API 서버(
3000), 통화 서버(7880) 등 여러 서비스가 돌아갑니다. Caddy가 앞에서 주소별로 트래픽을 교통정리해 줍니다.
- 집 공유기에서 외부로 열 수 있는 HTTPS 포트는 기본
- 내부망 보안 (방패 역할):
- 데이터베이스(PostgreSQL)나 내부 API 포트를 외부에 직접 노출하지 않고, Caddy 뒤의 폐쇄된 도커 가상 네트워크에 안전하게 격리합니다.
2. 홈서버 실전 문제: “공유기 443 포트를 이미 시놀로지가 쓰고 있다면?”
집에서 홈랩이나 NAS를 운영하다 보면 흔히 겪는 난관입니다.
“공유기의 외부 443 포트가 이미 기존 시놀로지 NAS로 포트포워딩되어 있는데, 유그린 NAS의 Caddy는 어떻게 써야 하죠?”
해결책: 시놀로지에서 유그린 Caddy로 전달하는 “프록시 연계 체인”
시놀로지의 443 포트포워딩을 건드릴 필요가 전혀 없습니다!
시놀로지는 문지기 역할만 하고, 메신저 도메인(api.내도메인)으로 들어온 요청을 유그린의 Caddy로 넘겨주는 방식입니다.
flowchart LR
A["스마트폰 앱 (443 포트)"] --> B["공유기 (443)"]
B --> C["시놀로지 NAS (기존 역방향 프록시)"]
C -->|"api.내도메인 요청 전달"| D["유그린 NAS Caddy (HTTP 8080)"]
D --> E["NestJS API (내부 포트 3000)"]
D --> F["LiveKit 통화 (내부 포트 7880)"]
linkStyle default stroke-width:3px,stroke:#333333이렇게 구성했을 때의 장점:
- 포트 번호 없는 깔끔한 URL: 앱 사용자들은 주소 뒤에 지저분한 포트 번호(
:8443)를 붙이지 않고https://api.mydomain.com그대로 접속할 수 있습니다. - Caddy의 장점 100% 활용: 유그린 NAS 내부에서는 Caddy가 WebSocket 프록시, 보안 헤더, 대용량 파일 전송 제한 설정을 완벽하게 처리합니다.
- 독자/일반 사용자 배려: 시놀로지가 없는 분들은 그냥 공유기에서
443 -> 유그린 Caddy로 바로 연결하면 똑같이 작동합니다.
3. 폴더 구조 및 사전 준비
유그린 NAS에 SSH로 접속하거나 UGOS 파일 관리자에서 Docker 전용 볼륨에 프로젝트 폴더를 만듭니다.
💡 저장소 배치 꿀팁:
- PostgreSQL DB와 Docker 설정: 응답 속도와 하드디스크 절전(Sleep) 방지를 위해 NVMe SSD 볼륨(
/volume1)에 둡니다.- 사진/동영상/파일 저장소: 용량이 크므로 대용량 HDD 볼륨(
/volume2)에 마운트합니다.
/volume1/docker/dingdong/ # SSD 볼륨
├─ docker-compose.yml # 컨테이너 오케스트레이션 설정
├─ .env # DB 비밀번호 및 환경변수 (보안 주의!)
├─ caddy/
│ ├─ Caddyfile # Caddy 라우팅 및 리버스 프록시 설정
│ └─ data/ # 인증서 영구 저장 폴더
└─ postgres/
└─ data/ # PostgreSQL 실제 데이터 영구 저장소
4. 설정 파일 작성
4.1 환경변수 파일 (.env)
비밀번호는 본인만의 강력한 비밀번호로 변경합니다.
# PostgreSQL 설정 POSTGRES_DB=dingdong POSTGRES_USER=dingdong POSTGRES_PASSWORD=my_super_secret_password_1234! # 도메인 설정 (본인 도메인으로 변경) DOMAIN_API=api.myfamily.com DOMAIN_RTC=rtc.myfamily.com
4.2 Caddy 설정 파일 (caddy/Caddyfile)
Caddy의 설정 문법은 Nginx보다 훨씬 직관적이고 간단합니다.
⚠️ 시놀로지 뒤에 둘 때 가장 중요한 핵심!
Caddy는 도메인만 적으면 Let’s Encrypt를 통해 외부 80/443 포트로 SSL 발급을 시도합니다.
하지만 외부 443 포트는 시놀로지가 이미 잡고 있고 HTTPS 암호화도 시놀로지가 끝마쳐서 넘겨주기 때문에, Caddy는 인증서를 발급받으려다 무한 오류를 뿜게 됩니다.
따라서 반드시 도메인 앞에http://를 명시하여, Caddy가 불필요한 SSL 인증서 발급을 하지 않고 내부 HTTP(8080 포트)로만 트래픽을 넘겨받도록 해야 합니다.
# API 서버 및 WebSocket 리버스 프록시 (앞에 http:// 필수!)
http://{$DOMAIN_API} {
# 대용량 사진/파일 전송을 위한 요청 본문 크기 제한 해제 (310MB)
request_body {
max_size 310MB
}
# 압축 전송 활성화 (모바일 데이터 절약)
encode zstd gzip
# 백엔드 API 컨테이너로 전달 (WebSocket도 Caddy가 알아서 처리함)
reverse_proxy api:3000
# 보안 헤더 설정
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
-Server
}
}
# LiveKit 통화 서버 시그널링 프록시
http://{$DOMAIN_RTC} {
reverse_proxy host.docker.internal:7880
}
4.3 통합 실행 파일 (docker-compose.yml) 만들기
위에서 준비한 Caddy와 PostgreSQL 컨테이너를 한 번에 묶어서 띄우는 메인 설계도 파일입니다.
어디에, 어떻게 만드나요?
- 프로젝트 루트 폴더인
/volume1/docker/dingdong/위치에docker-compose.yml이라는 이름으로 새 파일을 만듭니다.- 방법 A (유그린 UGOS 웹 GUI): 파일 관리자 ➡️
/volume1/docker/dingdong이동 ➡️ 우클릭 [새 파일 만들기] ➡️ 파일명docker-compose.yml지정 후 내용 붙여넣기 - 방법 B (터미널 nano):
cd /volume1/docker/dingdong && nano docker-compose.yml실행 후 붙여넣고 저장(Ctrl + O➡️Enter, 종료Ctrl + X)
- 방법 A (유그린 UGOS 웹 GUI): 파일 관리자 ➡️
파일 내용 (docker-compose.yml):
version: '3.8'
services:
# 1. Caddy 웹서버 (리버스 프록시)
caddy:
image: caddy:2-alpine
container_name: dingdong-caddy
restart: unless-stopped
env_file: .env # Caddyfile의 {$DOMAIN_API} 변수를 읽기 위해 필수!
ports:
# 시놀로지 뒤에 둘 경우 호스트 포트 8080(HTTP)을 Caddy의 80으로 매핑
# (시놀로지에서 http://유그린IP:8080 으로 넘겨줍니다)
- "8080:80"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
networks:
- app-network
# 2. PostgreSQL 16 데이터베이스
postgres:
image: postgres:16-alpine
container_name: dingdong-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
# [중요 개발 팁] 개발 PC(내 Mac)에서 Prisma로 스키마를 밀어 넣고
# DBeaver 등으로 접속해 보려면 로컬 네트워크 포트를 열어두어야 합니다.
# (공유기에서 5432 포트포워딩만 하지 않으면 집 내부망에서만 접속되므로 안전합니다)
- "5432:5432"
volumes:
- ./postgres/data:/var/lib/postgresql/data
networks:
- app-network
networks:
app-network:
name: dingdong-network
4.4 이 파일로 무엇을 하나요? (동작 원리)
- 이
docker-compose.yml파일이 저장된 위치(/volume1/docker/dingdong/)에서docker compose up -d명령어를 한 번만 실행하면:- Caddy와 PostgreSQL 이미지를 인터넷에서 자동으로 다운로드받습니다.
- 외부와 단절된 내부 비밀 네트워크(
dingdong-internal)를 생성합니다. - 두 개의 컨테이너를 서로 안전하게 연결하며 백그라운드에서 동시에 실행시켜 줍니다.
- 나중에 서버를 끄고 싶을 때는
docker compose down, 다시 켤 때는docker compose up -d명령어 하나로 모든 관리가 끝납니다.
5. 컨테이너 실행 및 동작 확인
설정 파일 작성이 끝났으면 유그린 NAS 터미널에서 실행합니다:
# 1. 백그라운드 실행 docker compose up -d # 2. 컨테이너 정상 구동 여부 확인 docker compose ps # 3. 로그 실시간 확인 (에러가 없는지 체크) docker compose logs -f
PostgreSQL 연결 테스트
PostgreSQL이 정상적으로 DB를 만들었는지 컨테이너 안으로 들어가 확인해 봅니다:
docker compose exec postgres psql -U dingdong -d dingdong -c "\l"
명령어 실행 후 dingdong 데이터베이스 목록이 보이면 DB 준비 완료입니다!
6. 시놀로지 역방향 프록시와 연동하기 (443 포트 연계)
이제 기존에 443 포트를 잡고 있던 시놀로지 NAS의 DSM 제어판으로 이동하여 **2개의 규칙(api, rtc)**을 생성합니다.
6.1 API 및 실시간 채팅 규칙 (api.myfamily.com)
- 시놀로지 DSM ➡️ 제어판 ➡️ 로그인 포털 ➡️ [고급] 탭 ➡️ [역방향 프록시] 클릭.
- [생성] 버튼 클릭 후 다음과 같이 규칙을 입력합니다:
- 소스(외부 요청):
- 프로토콜:
HTTPS - 호스트 이름:
api.myfamily.com - 포트:
443 - HSTS 활성화 체크
- 프로토콜:
- 대상(유그린 Caddy로 전달):
- 프로토콜:
HTTP(시놀로지가 외부 HTTPS를 복호화해서 넘겨주므로 HTTP 선택) - 호스트 이름:
유그린NAS_내부_IP(예:192.168.0.50) - 포트:
8080(Caddy가 수신하는 HTTP 포트)
- 프로토콜:
- 소스(외부 요청):
- [사용자 지정 머리글] 탭 ➡️ [생성] ➡️ [WebSocket] 클릭:
Upgrade,Connection헤더가 자동 추가되어 실시간 채팅 통신이 원활하게 통과됩니다.
- 저장을 누릅니다.
6.2 보이스톡 통화 시그널링 규칙 (rtc.myfamily.com)
보이스톡(WebRTC) 연결을 위한 시그널링 트래픽도 Caddy가 받아서 LiveKit으로 넘겨주므로 똑같이 등록해 줍니다.
- 역방향 프록시 화면에서 다시 [생성] 클릭.
- 다음과 같이 입력합니다:
- 소스(외부 요청):
- 프로토콜:
HTTPS - 호스트 이름:
rtc.myfamily.com - 포트:
443 - HSTS 활성화 체크
- 프로토콜:
- 대상(유그린 Caddy로 전달):
- 프로토콜:
HTTP - 호스트 이름:
유그린NAS_내부_IP(예:192.168.0.50) - 포트:
8080(Caddy가 호스트 이름에 따라 LiveKit의 7880으로 자동 라우팅)
- 프로토콜:
- 소스(외부 요청):
- [사용자 지정 머리글] 탭 ➡️ [생성] ➡️ [WebSocket] 클릭 (WebRTC 시그널링도 WebSocket 기반이므로 필수!)
- 저장을 누릅니다.
연동 결과
이제 시놀로지가 외부 443 포트로 들어온 두 가지 도메인을 모두 받아 유그린의 Caddy(8080)로 전달하고, Caddy가 내부 목적지에 맞춰 정확히 배달합니다:
https://api.myfamily.com➡️ 시놀로지 ➡️ 유그린 Caddy ➡️ NestJS API (내부 3000)https://rtc.myfamily.com➡️ 시놀로지 ➡️ 유그린 Caddy ➡️ LiveKit 음성서버 (내부 7880)
7. 다음 편 예고
이제 메신저의 심장인 PostgreSQL 데이터베이스와 든든한 방패인 Caddy 리버스 프록시 인프라가 완벽하게 준비되었습니다.
다음 **[개발일지 #04]**에서는 NestJS 백엔드 프로젝트를 시작하고, Prisma ORM을 사용해 실제 데이터베이스 스키마(회원, 채팅방, 메시지 테이블)를 생성하고 마이그레이션하는 과정을 다루겠습니다!
📂 Part 1. 기획 & 아키텍처 설계
- ✅ [개발일지 #01 | 기획] 유그린 NAS와 Flutter로 만드는 나만의 프라이빗 메신저 ‘딩동’ (기획 & 아키텍처 편)
📂 Part 2. 홈서버 & 인프라 구축 (UGREEN + Synology)
- ✅ [개발일지 #02 | 인프라] Mac에서 Flutter & VS Code 개발 환경 구축하기 (초보자 완벽 가이드)
- ✅ [개발일지 #03 | 인프라] 유그린 NAS에 Caddy & PostgreSQL 구축하기 (홈서버 443 포트 충돌 해결 꿀팁 포함)
📂 Part 3. 백엔드(API) & 실시간 통화 서버
- ✅ [개발일지 #04 | 백엔드] 유그린 NAS에 Go(Golang) 초경량 메신저 백엔드 서버 구축하기
- ⏳ [#05 | 백엔드] JWT 기반 가족 인증 및 실시간 WebSocket 채팅 구현 (예정)
- ⏳ [#06 | 통화] LiveKit 연동으로 무료 음성/영상 통화 서버 구축하기 (예정)
📂 Part 4. 모바일 앱 개발 (iOS / Android)
- ⏳ [#07 | 앱-공통] 모바일 앱 프로젝트 생성 및 로그인/채팅방 UI 구현 (예정)
- ⏳ [#08 | 앱-공통] 300MB 대용량 원본 사진/영상 전송 및 미디어 처리 (예정)
- ⏳ [#09 | 앱-푸시] 앱이 꺼져 있어도 울리는 푸시 알림(FCM/APNs) 연동 (예정)
📂 Part 5. 정식 배포 & 운영
- ⏳ [#10 | 배포] 백엔드 서버 도커 이미지 빌드 및 유그린 NAS 정식 배포 (예정)
- ⏳ [#11 | 운영] DB 자동 백업 스크립트 구성 및 가족 실사용 후기 (예정)