flowoodz.
← blog
10분 읽기글 flowoodz

서버 한 대로 apex와 서브도메인 여섯 개 운영하기: Cloudflare + Coolify 구성

apex 도메인과 서브도메인 서비스들을 한 Coolify 서버에 얹으면서 겪은 리다이렉트·SSL·헬스체크 함정과 최종 구성.

flowoodz.com 하나에 서비스가 여러 개 붙어 있습니다. apex는 브랜드 허브, 나머지는 서브도메인에 각각 다른 앱이 돌아갑니다. 전부 집에 있는 서버 한 대의 Coolify 위에 있고, DNS와 프록시는 Cloudflare가 맡습니다.

이 구성 자체는 어렵지 않았습니다. 시간을 잡아먹은 건 apex 도메인을 나중에 용도 변경할 때 생긴 문제들이었습니다. 원래 apex는 대표 서비스로 301 리다이렉트를 걸어 두고 있었는데, 이걸 브랜드 허브로 바꾸면서 예상치 못한 곳이 깨졌습니다.

같은 실수를 반복하지 않기 위해 구성과 함정을 남깁니다.


최종 구조

Coolify 대시보드. 프로젝트가 automation, infra, web으로 나뉘어 있고 서버는 localhost 한 대입니다

flowoodz.com/           → 브랜드 허브 (Next.js)
flowoodz.com/ads.txt    → 200 (애드센스)
flowoodz.com/health     → 200 ok (Coolify / Uptime Kuma)
 
www.flowoodz.com        → 같은 브랜드 허브 앱
plan.flowoodz.com       → 주간 할일 보드
yt.flowoodz.com         → 유튜브 트렌드 도구
capybara.flowoodz.com   → 미니 게임
kuma.flowoodz.com       → Uptime Kuma
n8n.flowoodz.com        → n8n

서버 한 대의 Coolify 위에 apex 브랜드 허브와 여러 서브도메인 앱이 각각 컨테이너로 올라가 있는 구조도

핵심 원칙은 하나입니다. apex는 apex 앱만 담당하고, 서브도메인 앱에는 apex 도메인을 붙이지 않는다. 이걸 어기면 어느 컨테이너가 apex 요청을 받는지 모호해집니다.

DNS는 이렇게 둡니다.

타입 이름 Proxy
A @ Coolify 서버 공인 IP Proxied
CNAME www flowoodz.com Proxied
A/CNAME plan, yt, kuma, ... 각 서비스 Proxied

함정 1: Cloudflare 전역 리다이렉트가 ads.txt를 죽인다

가장 오래 헤맨 문제입니다.

apex를 대표 서비스로 넘기던 시절, Cloudflare에 전역 Redirect Rule을 걸어 뒀습니다. flowoodz.com/*plan.flowoodz.com/* 형태입니다. 편했습니다. Coolify 앱을 건드릴 필요가 없었으니까요.

문제는 apex를 브랜드 허브로 바꾼 뒤였습니다. Coolify에 앱을 새로 붙이고 도메인을 등록했는데도 /가 계속 301로 넘어갔습니다. Coolify 설정을 몇 번이나 다시 확인했지만 거기엔 문제가 없었습니다. Cloudflare가 요청을 오리진까지 보내지 않고 엣지에서 먼저 리다이렉트하고 있었습니다.

더 고약한 건 /ads.txt였습니다. 애드센스는 flowoodz.com/ads.txt가 200으로 응답해야 하는데, 전역 리다이렉트는 경로를 가리지 않으니 이것도 301이 됐습니다. 브라우저로 /만 확인하고 있으면 알아채기 어렵습니다.

교훈: apex의 용도를 바꿀 때는 Coolify보다 Cloudflare의 Redirect Rule과 Page Rule을 먼저 확인해야 합니다. 오리진 설정을 아무리 고쳐도 엣지에서 잡히면 도달하지 않습니다.

증상별로 정리하면 이렇습니다.

증상 실제 원인
NXDOMAIN @ A 레코드 없음
/가 계속 301 Cloudflare 전역 Redirect Rule 잔존, 또는 옛 컨테이너가 살아 있음
/ads.txt가 301 위와 같은 원인. 경로 무관하게 전부 넘어감
SSL 오류 Coolify에 도메인 미등록, 또는 Cloudflare SSL 모드 불일치

함정 2: Proxied 상태에서 SSL 모드 불일치

Cloudflare에서 오렌지 구름(Proxied)을 켜면 트래픽이 Cloudflare를 거칩니다. 이때 Cloudflare와 오리진 사이의 암호화 모드가 따로 있습니다.

Coolify는 Let's Encrypt로 오리진 인증서를 발급합니다. 그래서 SSL/TLS 모드는 Full 또는 Full (Strict)이어야 합니다. Flexible로 두면 Cloudflare가 오리진에 평문으로 붙으려 하고, Coolify는 HTTPS로 리다이렉트하면서 무한 루프나 인증서 오류가 납니다.

  • SSL/TLS 모드: Full 이상
  • Always Use HTTPS: On

이 두 값은 DNS 편집 권한만 있는 API 토큰으로는 확인할 수 없습니다. 대시보드에서 직접 봐야 합니다. 자동화 스크립트로 DNS만 관리하고 있으면 이 항목이 사각지대가 됩니다.


함정 3: 헬스체크 경로의 소유 앱이 분명해야 합니다

앱마다 /health를 따로 두고, 해당 앱의 컨테이너와 외부 도메인을 각각 확인합니다. 구현과 프로브 실패 원인은 Next.js 배포 기록에, Docker 내부 검사와 Uptime Kuma의 역할 차이는 모니터링 기록에 남겼습니다.

이 글에서 중요한 건 여러 앱이 같은 서버에 있어도 검사 경로를 공유하지 않는다는 점입니다. Coolify가 정상인데 외부 모니터만 실패하면 DNS나 Cloudflare 구간을 먼저 보고, 한 앱의 /health만 실패하면 그 컨테이너부터 확인합니다.


함정 4: 배포 설정이 아니라 결과 HTML을 확인해야 합니다

2026년 8월 19일에 프로덕션 HTML을 다시 확인하다가 애드센스 스크립트가 통째로 빠져 있는 것을 찾았습니다. 클라이언트 ID는 로컬 .env.local에만 있었고 Coolify 빌드에는 전달되지 않았습니다. 설정 화면에 값을 적었다는 기억보다 실제로 배포된 HTML이 더 정확했습니다.

지금은 공개 값인 애드센스 클라이언트 ID를 코드에 직접 명시하고, 빌드 산출물의 <head>에서 스크립트를 확인합니다. NEXT_PUBLIC_*가 빌드 시점에 확정되는 원리와 두 번째 반려 대응은 애드센스 기록에 이어 적었습니다.


배포와 검증

Coolify는 GitHub App으로 저장소를 연결해 두면 push마다 자동 빌드합니다. private 저장소는 반드시 GitHub App으로 붙여야 합니다. Public Repository 방식으로 등록하면 git ls-remote가 실패합니다.

수동 배포가 필요하면 API를 씁니다.

curl -H "Authorization: Bearer $COOLIFY_API_KEY" \
  "https://coolify.example.com/api/v1/deploy?uuid=<APP_UUID>&force=true"

배포 후 검증은 항목별로 나눠서 봅니다. /만 확인하면 앞서 말한 ads.txt 같은 문제를 놓칩니다.

# apex가 리다이렉트가 아니라 200인지
curl -sSI https://example.com/ | head -5
 
# 헬스체크
curl -sS https://example.com/health
 
# 경로별 응답 (전역 리다이렉트 잔존 확인)
curl -sSI https://example.com/ads.txt | head -3
 
# www도 같은 앱을 타는지
curl -sSI https://www.example.com/ | head -3

-I로 헤더만 보면서 상태 코드가 200인지 301인지를 확인하는 게 핵심입니다. 브라우저는 리다이렉트를 자동으로 따라가서 최종 화면만 보여 주니까, 눈으로는 정상처럼 보입니다.


장애는 바깥 구간부터 확인합니다

서버 한 대에 여러 서비스를 얹는 구성은 비용 면에서 확실히 유리합니다. 앱을 하나 더 붙이는 한계비용이 거의 0입니다. 대신 레이어가 늘어난다는 점을 계속 의식해야 합니다.

요청이 Cloudflare DNS, Cloudflare 엣지, Coolify 프록시, 컨테이너 순서로 흐르며 각 구간에서 발생하는 대표 장애를 정리한 다이어그램

요청은 Cloudflare DNS → Cloudflare 엣지(리다이렉트·SSL) → Coolify 프록시 → 컨테이너 순서로 흐릅니다. 문제가 생기면 이 순서대로 의심하는 게 가장 빠릅니다. 내가 시간을 날린 이유는 항상 가장 안쪽(컨테이너 설정)부터 의심했기 때문입니다. 정작 원인은 대부분 가장 바깥쪽에 있었습니다.