서버 하나에 사이트 세 개를 올리면서 겪은 기록이다. 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로 가!”라고 리다이렉트를 걸어놨으니:
- 사용자가 https 접속
- Cloudflare가 http로 서버에 전달
- Apache가 https로 리다이렉트
- Cloudflare가 다시 http로 전달
- 무한 반복
해결: 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/link와 next/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 삽질이 반복되면 서브도메인 전환을 고려하자.


