온프레미스 Docker n8n 에 AWS 도메인 입히기 — IP 대신 주소로 접속하기
Summary
n8n 을 온프레미스 서버에 Docker 로 이미 띄워둔 상태였어요. 잘 도는데, 접속할 때마다 http://192.168.x.x:5678 처럼 IP 와 포트를 외워서 쳐야 하는 게 영 불편했습니다. 팀원한테 주소 알려줄 때도 민망하고, 무엇보다 HTTPS 가 안 붙으니 브라우저가 계속 “안전하지 않음” 경고를 띄웠어요.
그래서 이 IP 접속을 https://n8n.example.com 같은 깔끔한 도메인 주소로 바꿨습니다. 도메인은 AWS Route 53 에서 관리하고, 서버 앞단에는 Caddy 리버스 프록시를 하나 세워서 HTTPS 인증서까지 자동으로 받게 했어요. 서버 자체는 그대로 온프레미스에 있고, “이름표”만 AWS 로 붙이는 구성이에요.
💡 이 글에서 다루는 것
- 왜 IP 접속을 도메인으로 바꾸는 게 좋은가 (외우기·HTTPS·웹훅)
- 큰 그림: 브라우저 → Route 53 → 우리집 공인 IP → Caddy → n8n
- Route 53 에 도메인(호스팅 영역)과 A 레코드 만들기
- 공유기/방화벽에서 80·443 포트 포워딩
- Caddy 리버스 프록시를 docker compose 에 얹어 HTTPS 자동 발급
- n8n 환경변수(
N8N_HOST,WEBHOOK_URL) 를 도메인에 맞추기 — 안 하면 웹훅이 깨져요dig·curl로 확인하고 브라우저로 최종 점검- (옵션) 포트 80 을 못 열 때 — Route 53 DNS 챌린지
1. 왜 IP 로 들어가면 불편할까
http://192.168.x.x:5678 로도 n8n 은 잘 열려요. 그런데 이렇게 쓰면 걸리는 게 셋 있어요.
- 외우기 힘들다 — IP + 포트 조합은 사람이 기억하기 나쁘고, 내부 IP 가 바뀌면 링크가 통째로 죽어요.
- HTTPS 를 못 붙인다 — 인증서는 IP 가 아니라 도메인 이름에 발급돼요. IP 로 접속하는 한 브라우저 경고를 피할 수 없고, 로그인 쿠키도 안전하지 않은 채로 오갑니다.
- 웹훅·OAuth 콜백이 깨진다 — n8n 의 진짜 힘은 외부 서비스가 쏘는 웹훅과 OAuth 연동인데, 콜백 주소가
http://192.168.x.x:5678/...이면 외부에서 도달할 수도 없고 https 를 요구하는 서비스는 아예 등록을 거부해요.
도메인 하나 붙이면 이 셋이 한 번에 풀립니다. 그래서 하는 거예요.
✅ 목표:
http://192.168.x.x:5678→https://n8n.example.com(도메인은 예시로example.com을 씁니다. 본인 도메인으로 바꿔서 읽으세요.)
2. 큰 그림
먼저 흐름부터 그려볼게요. 요청이 어디를 거쳐 n8n 까지 도달하는지 알면 나머지 설정이 전부 이 그림의 한 조각이라는 게 보여요.
브라우저
│ https://n8n.example.com
▼
[ AWS Route 53 ] ← 도메인 이름을 공인 IP 로 해석 (A 레코드)
│ 1.2.x.x
▼
[ 공유기/방화벽 ] ← 80·443 포트를 서버로 포워딩
│
▼
[ Caddy (리버스 프록시) ] ← HTTPS 종단, 인증서 자동 발급
│ http://n8n:5678
▼
[ n8n 컨테이너 ] ← 온프레미스 Docker, 그대로
핵심은 서버(n8n)는 손대지 않는다는 거예요. 앞에 Caddy 라는 문지기를 하나 세우고, 그 문지기한테 도메인 이름표(Route 53)를 붙이는 구조입니다. 서버는 계속 온프레미스에 있고, 인터넷에서 오는 요청만 이 경로로 들어와요.
이제 위에서부터 하나씩 만들어 볼게요.
3. Route 53 에 도메인과 A 레코드 만들기
도메인이 이미 있고 Route 53 에서 관리하고 있다면 이 절은 A 레코드 하나만 추가하면 끝이에요. 아직 없다면 순서는 이렇습니다.
- Route 53 → 호스팅 영역(Hosted zone) 생성 →
example.com입력. - 만들어지면 NS 레코드 4개가 보여요. 도메인을 산 곳(가비아·Cloudflare·GoDaddy 등)의 네임서버를 이 4개로 바꿔주면, 이제부터
example.com의 DNS 는 Route 53 이 담당합니다.
그다음, n8n 이 뜬 우리집(사무실) 서버의 공인 IP 로 향하는 A 레코드를 하나 만들어요.
💡 공인 IP 는 서버에서
curl -s ifconfig.me로 확인하면 돼요. 내부 IP(192.168.x.x)가 아니라 바깥에서 보이는 IP 여야 합니다.
콘솔에서 만들어도 되고, CLI 라면 이렇게 한 방에 넣어요. 아래 change-batch JSON 을 record.json 으로 저장하고:
{
"Comment": "n8n subdomain to on-prem server",
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "n8n.example.com",
"Type": "A",
"TTL": 300,
"ResourceRecords": [{ "Value": "1.2.x.x" }]
}
}
]
}
호스팅 영역 ID 를 넣어 반영합니다.
aws route53 change-resource-record-sets \
--hosted-zone-id <HOSTED_ZONE_ID> \
--change-batch file://record.json
몇 분 뒤 이름이 IP 로 풀리는지 확인해요.
dig +short n8n.example.com
1.2.x.x 가 찍히면 이름표는 붙은 거예요. 아직 안 뜨면 TTL/전파를 조금 더 기다리면 됩니다.
⚠️ 집·사무실 회선은 공인 IP 가 고정이 아닌 경우가 많아요. IP 가 바뀌면 A 레코드도 갱신해야 하니, 고정 IP 를 받거나 뒤의 “포트 80 을 못 열 때” 절에서 다루는 방식처럼 IP 에 덜 의존하는 방법을 같이 고려하세요. (재부팅 때마다 바뀌는 환경이면 IP 변경 시 Route 53 API 로 A 레코드를 자동 갱신하는 작은 스크립트를 crontab 에 걸어두는 것도 방법이에요.)
4. 공유기/방화벽에서 포트 포워딩
Route 53 이 이름을 공인 IP 로 풀어줘도, 그 IP 로 들어온 요청이 서버까지 도달해야 의미가 있어요. 온프레미스는 보통 공유기(NAT) 뒤에 있으니, 바깥에서 온 웹 트래픽을 서버로 넘겨주는 포트 포워딩을 걸어줍니다.
| 외부 포트 | → 내부 대상 | 용도 |
|---|---|---|
80/tcp |
서버 192.168.x.x:80 |
HTTP → HTTPS 리다이렉트 + 인증서 발급 |
443/tcp |
서버 192.168.x.x:443 |
실제 HTTPS 접속 |
공유기 관리자 페이지의 포트 포워딩(또는 “가상 서버”) 메뉴에서 위 두 줄을 서버 내부 IP 로 넘겨주면 돼요. 서버에 OS 방화벽(ufw 등)이 켜져 있다면 거기서도 80·443 을 열어줍니다.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
🚨 5678 포트는 굳이 바깥으로 열지 마세요. 앞으로는 Caddy(80·443)만 인터넷에 노출하고, n8n 은 Docker 내부 네트워크에서만 Caddy 와 대화하게 둘 거예요. n8n 을 인터넷에 직접 노출하면 인증 이슈·스캔 트래픽에 그대로 노출됩니다.
5. Caddy 리버스 프록시 얹기
이제 문지기를 세울 차례예요. Caddy 를 고른 이유는 단순해요 — 도메인만 적어주면 Let’s Encrypt 인증서를 알아서 발급·갱신하고, HTTP→HTTPS 리다이렉트도 기본으로 해줘요. 설정 파일이 딱 몇 줄이에요.
기존에 n8n 만 돌리던 docker-compose.yml 에 Caddy 서비스를 하나 추가하고, n8n 은 호스트로 포트를 뿌리는 대신 같은 네트워크 안에서만 Caddy 와 통신하게 바꿉니다.
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
environment:
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- N8N_PORT=5678
- WEBHOOK_URL=https://n8n.example.com/
- N8N_EDITOR_BASE_URL=https://n8n.example.com/
- GENERIC_TIMEZONE=Asia/Seoul
- TZ=Asia/Seoul
volumes:
- n8n_data:/home/node/.n8n
# 예전엔 여기 ports 로 5678 을 뿌렸다면 이제 지워요.
# Caddy 만 n8n 을 바라보면 되니까 호스트로 노출할 필요가 없어요.
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
n8n_data:
caddy_data:
caddy_config:
그리고 같은 폴더에 Caddyfile 을 만들어요. 이게 전부예요.
n8n.example.com {
reverse_proxy n8n:5678
}
reverse_proxy n8n:5678 에서 n8n 은 compose 서비스 이름이에요. 같은 네트워크라 컨테이너 이름으로 바로 찾아가요. 올려봅니다.
docker compose up -d
Caddy 가 뜨면서 n8n.example.com 인증서를 자동으로 받아와요. 로그로 확인할 수 있어요.
docker compose logs -f caddy
certificate obtained successfully 비슷한 줄이 뜨면 인증서 발급까지 끝난 거예요.
⚠️ 인증서 발급(HTTP-01/TLS-ALPN 챌린지)은 80·443 이 바깥에서 서버까지 열려 있어야 성공해요. 4절의 포트 포워딩이 제대로 안 됐으면 여기서 막히니, 인증서 로그가 안 나오면 포워딩부터 다시 보세요.
6. n8n 환경변수를 도메인에 맞추기
5절 compose 에 이미 넣어뒀지만, 왜 넣는지가 중요해서 따로 짚을게요. 이걸 안 맞추면 화면은 열리는데 웹훅과 로그인 리다이렉트가 미묘하게 깨져요.
| 변수 | 값 | 왜 필요한가 |
|---|---|---|
N8N_HOST |
n8n.example.com |
n8n 이 자기 자신을 이 이름으로 인식 |
N8N_PROTOCOL |
https |
생성하는 링크를 https:// 로 |
WEBHOOK_URL |
https://n8n.example.com/ |
워크플로우가 안내하는 웹훅 주소의 기준 |
N8N_EDITOR_BASE_URL |
https://n8n.example.com/ |
에디터·공유 링크의 기준 주소 |
특히 WEBHOOK_URL 이 핵심이에요. 이걸 안 넣으면 n8n 은 웹훅 주소를 내부 IP·포트 기준(http://...:5678/...)으로 안내하는데, 그 주소는 외부 서비스가 도달할 수 없어요. WEBHOOK_URL 을 도메인으로 박아두면 워크플로우가 처음부터 올바른 공개 주소를 알려줘요.
💡 Caddy 가 HTTPS 를 대신 종단(terminate)하고 n8n 한테는 평문 HTTP 로 넘겨줘요. 그래서 n8n 입장에선 앞에 프록시가 있다는 걸 알아야 링크를
https://로 제대로 만들어요. 위N8N_PROTOCOL=https가 그 역할이에요.
값을 바꿨으면 n8n 만 다시 띄워요.
docker compose up -d n8n
7. 확인하기
이제 이름 → 인증서 → 실제 화면 순으로 점검해요.
이름이 우리집 IP 로 풀리는지:
dig +short n8n.example.com
HTTPS 응답과 인증서가 붙었는지:
curl -I https://n8n.example.com
HTTP/2 200 (혹은 로그인으로의 302)이 뜨고 인증서 경고가 없으면 성공이에요. 마지막으로 브라우저에서 https://n8n.example.com 을 열어봅니다.
- 주소창에 자물쇠(🔒)가 뜨고 인증서 경고가 없다
- n8n 로그인/에디터 화면이 정상으로 뜬다
- 워크플로우에서 만든 웹훅 주소가
https://n8n.example.com/webhook/...로 안내된다 - 이제 IP·포트를 외울 필요가 없다 🎉
8. (옵션) 포트 80 을 못 열 때 — Route 53 DNS 챌린지
ISP 가 80 번을 막아뒀거나, 인바운드 포트를 아예 열기 싫은 환경도 있어요. 이럴 땐 인증서 발급을 HTTP 챌린지 대신 DNS 챌린지로 바꾸면 포트 80 없이도 인증서를 받을 수 있어요. Route 53 을 이미 쓰고 있으니 궁합도 좋습니다.
방식은 이래요 — Caddy 가 인증서를 받을 때 Let’s Encrypt 가 요구하는 검증 값을 Route 53 에 TXT 레코드로 잠깐 올렸다 지우는 걸로 소유권을 증명해요. 그래서 Route 53 을 만질 수 있는 권한(IAM)이 필요하고, Route 53 DNS 플러그인이 포함된 Caddy 이미지를 써야 해요.
Caddyfile 에 DNS 제공자를 지정하고:
n8n.example.com {
reverse_proxy n8n:5678
tls {
dns route53
}
}
Caddy 컨테이너에 Route 53 을 조작할 IAM 자격증명을 환경변수로 넘겨줘요. 이 키에는 해당 호스팅 영역의 레코드 변경 권한만 최소로 주는 걸 추천드려요.
caddy:
image: <caddy-route53> # route53 DNS 플러그인이 포함된 이미지
environment:
- AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY_ID>
- AWS_SECRET_ACCESS_KEY=<AWS_SECRET_ACCESS_KEY>
- AWS_REGION=ap-northeast-2
🚨 IAM 키는 절대 깃/이미지에 박아 넣지 마세요.
.env나 시크릿으로 주입하고, 권한은 대상 호스팅 영역의route53:ChangeResourceRecordSets정도로 최소화하세요.
이렇게 하면 포트 80 을 열지 않고도 인증서를 자동 발급·갱신할 수 있어서, 인바운드를 443 만 열어두는 더 조용한 구성이 돼요.
마무리
정리하면, 서버는 온프레미스 Docker 그대로 두고 앞에 Caddy 문지기 + Route 53 이름표만 붙였어요. 이제 IP·포트를 외우는 대신 https://n8n.example.com 한 줄로 들어가고, HTTPS 도 자동으로 붙고, 웹훅·OAuth 연동도 도메인 기준으로 깔끔하게 돌아갑니다.
같은 패턴은 n8n 뿐 아니라 온프레미스에 띄운 다른 셀프호스팅 서비스(대시보드·문서·API)에도 그대로 재활용할 수 있어요. Caddyfile 에 블록 하나 더 추가하고 A 레코드 하나 더 파면 끝이에요.
일단 오늘은 여기까지…..
다음 글에서는 이 구조에 기본 인증·접근 제한을 얹어 조금 더 단단하게 조이는 이야기를 정리해볼게요.