바이브 코딩 일지 #3: Apache 멀티 도메인 호스팅 + Cloudflare SSL 삽질기

서버 하나에 사이트 세 개를 올리면서 겪은 기록이다. Apache VirtualHost, Cloudflare 무한 리다이렉트 루프, Next.js basePath 함정, 그리고 결국 서브도메인으로 전환하기까지.

1. 서버 하나, 사이트 셋 — Apache VirtualHost

AWS Lightsail 서버 한 대에 이런 구조를 만들어야 했다:

  • my-domain.com/ → 정적 랜딩 페이지 (HTML/CSS)
  • my-domain.com/blog → WordPress (PHP)
  • my-domain.com/asset → Next.js 웹앱 (Node.js, PM2)

건물 하나에 1층 카페, 2층 사무실, 3층 헬스장 같은 구조다.

Apache VirtualHost로 경로별 라우팅을 설정했다:

<VirtualHost *:80>
  ServerName my-domain.com
  DocumentRoot "/home/user/htdocs/landing"
  Alias /blog /home/user/htdocs/wordpress
  ProxyPass        /asset http://127.0.0.1:3000/asset
  ProxyPassReverse /asset http://127.0.0.1:3000/asset
</VirtualHost>
  • ServerName: 이 VirtualHost가 응답할 도메인
  • DocumentRoot: 기본 경로(/)의 파일 위치
  • Alias: URL을 서버 다른 폴더에 매핑 (WordPress처럼 PHP가 직접 처리하는 경우)
  • ProxyPass: 다른 포트 앱으로 리버스 프록시

설정 후 반드시 문법 검사를 먼저 한다:

sudo apachectl configtest  # 오타/문법 에러 확인
sudo apachectl graceful    # 서비스 중단 없이 반영

configtest 안 하고 바로 재시작하면 Apache가 아예 멈출 수 있다.

2. 무한 리다이렉트 — Cloudflare SSL 함정

설정 마치고 접속했더니 ERR_TOO_MANY_REDIRECTS. 무한 리다이렉트다.

원인: Cloudflare SSL 모드가 Flexible이었다. Flexible이면:

  • 사용자 → Cloudflare: HTTPS
  • Cloudflare → 내 서버: HTTP

여기에 Apache가 “HTTP면 HTTPS로 가!”라고 리다이렉트를 걸어놨으니:

  1. 사용자가 https 접속
  2. Cloudflare가 http로 서버에 전달
  3. Apache가 https로 리다이렉트
  4. Cloudflare가 다시 http로 전달
  5. 무한 반복

해결: Apache에서 HTTPS 리다이렉트 줄을 삭제했다. Cloudflare가 이미 사용자와의 HTTPS를 처리하고 있으니까.

모드 사용자↔CF CF↔서버 서버 인증서
Flexible HTTPS HTTP 불필요
Full HTTPS HTTPS 자체서명 OK
Full Strict HTTPS HTTPS 유효한 CA 필수

Cloudflare 공식에서도 Flexible 대신 Full 이상을 권장한다. Flexible은 서버 구간이 암호화 안 되어서 보안에도 좋지 않다.

3. Next.js basePath 함정 3개

my-domain.com/asset에서 Next.js를 띄우려면 basePath 설정이 필요하다.

// next.config.mjs
const nextConfig = {
  basePath: '/asset',
  assetPrefix: '/asset',
}

그런데 여기에 함정이 세 개나 있었다.

함정 1: fetch()는 basePath를 자동 적용 안 한다

next/linknext/router는 자동으로 /asset을 붙여주는데, fetch()는 안 된다.

// 안 됨 — /api/accounts로 요청 → 404
const res = await fetch('/api/accounts')

// 이렇게 직접 붙여야 함
const BASE = process.env.NEXT_PUBLIC_BASEPATH ?? ''
const res = await fetch(BASE + '/api/accounts')

화면에 데이터가 안 나오면 Network 탭에서 API URL이 /asset/api/...인지 확인하자.

함정 2: rsync 슬래시 하나로 package.json 날림

# 슬래시 있음: 폴더 "안의 내용물"이 대상에 직접 복사
rsync -az .next/ user@server:/app/
# → /app/package.json이 .next/package.json으로 덮어써짐!

# 슬래시 없음: "폴더 자체"가 대상 안에 복사
rsync -az .next user@server:/app/
# → /app/.next/package.json (정상)

.next/ 안에 {"type": "commonjs"} package.json이 있는데, 슬래시 잘못 붙이면 앱의 진짜 package.json을 덮어쓴다. npm start가 원인 모를 에러로 실패한다.

함정 3: 서버의 next.config.mjs가 빌드와 다르면 404

로컬에서 basePath 설정하고 빌드한 후 .next만 서버에 올리면, npm start 때 서버의 next.config.mjs를 읽는다. 거기에 basePath가 없으면 라우팅이 어긋나 404.

# next.config.mjs도 같이 올려야 한다
rsync -az .next next.config.mjs package.json user@server:/app/

4. 결국 서브도메인으로 전환

basePath 함정을 세 개나 겪고 나니 “이렇게까지 할 필요가 있나?” 싶었다.

비교 경로 기반 (/asset) 서브도메인 (asset.)
Next.js 설정 basePath+assetPrefix 필요 불필요
API 호출 수동 prefix 그냥 /api/…
코드 복잡도 높음 낮음

서브도메인으로 전환하면서 basePath, assetPrefix, BASEPATH 환경변수 전부 제거. 코드가 깔끔해졌다.

전환 후 새로운 문제: asset.my-domain.com에 접속하면 블로그가 뜬다. Apache SSL vhost를 blog.my-domain.com에만 만들어뒀더니, 다른 도메인 HTTPS 요청이 전부 blog vhost로 간 거다. 모든 도메인에 port 443 vhost를 만들어서 해결.

5. 최종 구조

사용자 → Cloudflare (HTTPS, Full) → Apache :80/:443
  ├── my-domain.com        → 정적 HTML
  ├── blog.my-domain.com   → WordPress
  └── asset.my-domain.com  → Next.js (PM2)

경로 기반으로 시작했다가 basePath 삽질을 겪고 서브도메인으로 전환한 결과다.

디버깅할 때 쓰는 명령어:

sudo apachectl configtest    # 설정 문법 검사
sudo apachectl -S            # VirtualHost 매핑 확인
curl -skL -o /dev/null -w '%{http_code} %{url_effective}' https://my-domain.com/
pm2 list                     # 앱 상태
pm2 logs my-app --lines 30   # 로그

이번에 배운 것:
1. Cloudflare Flexible + 서버 HTTPS 리다이렉트 = 무한 루프. Full 이상으로.
2. basePath는 fetch에 자동 적용 안 된다.
3. rsync 슬래시 유무 — source/ = 내용물, source = 폴더 자체
4. Apache SSL vhost 누락하면 엉뚱한 사이트가 응답한다.
5. basePath 삽질이 반복되면 서브도메인 전환을 고려하자.