Ghost 셀프 호스팅에서 Google AdSense ads.txt 설정하기 (NPM + Docker)
Ghost는 정적 파일 서빙이 제한적이라 ads.txt를 직접 올릴 수 없습니다. NPM의 Advanced 설정에 location 블록을 추가해서 해결하는 방법을 정리했습니다.
1. 문제 상황
Google AdSense에 블로그를 등록했는데, 대시보드에서 "Ads.txt 상태: 찾을 수 없음" 경고가 표시되었습니다.
사이트 URL: example.dev
승인 상태: 준비 중
Ads.txt 상태: 찾을 수 없음 ← 문제!
왜 문제인가요?
ads.txt가 없으면:
- 유럽(EEA), 영국, 스위스 방문자로부터 광고 수익 손실 가능
- AdSense 승인이 지연될 수 있음
- 광고 사기(Ad Fraud) 방지 기능 미적용
환경 구성
| 구성 요소 | 설명 |
|---|---|
| 블로그 플랫폼 | Ghost (Docker, 6-alpine) |
| 리버스 프록시 | Nginx Proxy Manager (Docker) |
| CDN/DNS | Cloudflare (Proxy ON) |
| 도메인 구조 | example.dev → 리다이렉트 → blog.example.dev |
문제는 Ghost가 정적 파일 서빙을 기본 지원하지 않는다는 점입니다. WordPress처럼 루트에 파일을 그냥 올리면 되는 것이 아닙니다.
2. 원인 분석
ads.txt란?
Authorized Digital Sellers의 약자로, 웹사이트 소유자가 "이 광고 네트워크만 내 사이트에서 광고를 판매할 수 있다"고 선언하는 텍스트 파일입니다.
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
| 필드 | 설명 |
|---|---|
google.com |
광고 시스템 도메인 |
pub-XXXX |
게시자 ID (AdSense에서 확인) |
DIRECT |
직접 계약 관계 |
f08c47fec0942fa0 |
Google의 TAG-ID (고정값) |
Ghost에서 ads.txt가 안 되는 이유
Ghost는 Node.js 기반 CMS로, 다음과 같은 특성이 있습니다:
- 정적 파일 서빙 제한: 루트(
/) 경로에 임의 파일을 둘 수 없음 - 테마 assets 폴더:
/assets/경로만 정적 파일 서빙 - 라우팅 제한:
routes.yaml로 커스텀 라우트 가능하지만 정적 파일은 불가
❌ /content/ads.txt → 접근 불가
❌ /assets/ads.txt → /assets/ 경로만 가능
❌ routes.yaml → 동적 라우팅만 지원
해결 방향
Ghost 앞단의 **리버스 프록시(NPM)**에서 /ads.txt 요청을 가로채서 직접 서빙합니다.
요청 흐름:
브라우저 → Cloudflare → NPM → Ghost
↓
/ads.txt 요청이면
↓
정적 파일 직접 반환
3. 해결 방법
3.1 ads.txt 파일 생성
먼저 서버에 ads.txt 파일을 생성합니다.
# 서버 접속
ssh -i ~/your-key ubuntu@YOUR_SERVER_IP
# NPM 데이터 디렉토리에 static 폴더 생성
mkdir -p ~/docker/nginx-proxy-manager/data/static
# ads.txt 파일 생성
echo "google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0" > ~/docker/nginx-proxy-manager/data/static/ads.txt
주의:
pub-XXXX부분은 본인의 AdSense 게시자 ID로 교체해야 합니다. AdSense → 계정 → 계정 정보에서 확인할 수 있습니다.
파일 생성 확인:
cat ~/docker/nginx-proxy-manager/data/static/ads.txt
# 출력: google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
3.2 NPM 관리 페이지 접속
Nginx Proxy Manager는 보안상 관리 포트(81)를 외부에 노출하지 않는 것이 좋습니다. SSH 터널을 사용합니다.
# 로컬에서 실행 (새 터미널)
ssh -i ~/your-key -L 8081:localhost:81 ubuntu@YOUR_SERVER_IP
# 브라우저에서 접속
open http://localhost:8081
3.3 NPM Advanced 설정 추가
- Proxy Hosts 메뉴 클릭
- 블로그 도메인 (예:
blog.example.dev) 찾아서 Edit (점 3개 메뉴) - Advanced 탭 선택
- 기존 설정 맨 아래에 다음 추가:
# ads.txt for Google AdSense
location = /ads.txt {
alias /data/static/ads.txt;
}
- Save 클릭
설정 상세 설명
location = /ads.txt { # ← 정확히 /ads.txt 경로만 매칭 (= 는 exact match)
alias /data/static/ads.txt; # ← NPM 컨테이너 내부 경로
}
| 디렉티브 | 설명 |
|---|---|
location = |
정확한 경로 매칭 (다른 요청에 영향 없음) |
alias |
파일 시스템의 실제 경로로 매핑 |
왜 /data/static/인가요?
호스트의 ~/docker/nginx-proxy-manager/data/가 컨테이너의 /data/로 마운트되어 있기 때문입니다.
# docker-compose.yml 예시
volumes:
- ./data:/data # ← 이 매핑 때문
3.4 설정 확인
# ads.txt 접근 테스트
curl -s https://blog.example.dev/ads.txt
정상 출력:
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
HTTP 헤더 확인:
curl -sI https://blog.example.dev/ads.txt | head -5
HTTP/2 200
content-type: text/plain
content-length: 59
4. 핵심 개념 정리
Ghost 정적 파일 서빙 방법 비교
| 방법 | 난이도 | Docker 호환 | 설명 |
|---|---|---|---|
| NPM location (권장) | 쉬움 | O | 리버스 프록시에서 처리 |
| nginx 직접 설정 | 중간 | X | 별도 nginx 필요 |
| Ghost 테마 수정 | 어려움 | O | routes.yaml + 커스텀 템플릿 |
| Cloudflare Workers | 중간 | O | 엣지에서 처리 |
NPM location 디렉티브 종류
location = /ads.txt { } # 정확히 /ads.txt만
location /api/ { } # /api/로 시작하는 모든 경로
location ~ \.php$ { } # 정규식: .php로 끝나는 경로
location ~* \.(jpg|png)$ { } # 정규식(대소문자 무시): 이미지 파일
alias vs root 차이
# alias: 경로를 완전히 대체
location /ads.txt {
alias /data/static/ads.txt;
}
# 요청: /ads.txt → 파일: /data/static/ads.txt ✓
# root: 경로를 붙임
location /ads.txt {
root /data/static;
}
# 요청: /ads.txt → 파일: /data/static/ads.txt ✓ (이 경우 동일)
단일 파일 서빙에는 alias가 더 명확합니다.
5. 베스트 프랙티스
ads.txt 설정 체크리스트
- [ ] AdSense 게시자 ID 정확히 확인 (
pub-뒤 16자리 숫자) - [ ] 파일 내용에 불필요한 공백/줄바꿈 없는지 확인
- [ ] HTTPS로 접근 가능한지 확인
- [ ] Content-Type이
text/plain인지 확인 - [ ] 리다이렉트 없이 200 응답인지 확인
여러 광고 네트워크 사용 시
# ads.txt에 여러 줄 추가
cat > ~/docker/nginx-proxy-manager/data/static/ads.txt << 'EOF'
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
google.com, pub-YYYYYYYYYYYYYYYY, RESELLER, f08c47fec0942fa0
adtech.com, 12345, DIRECT
EOF
도메인 리다이렉트 시 주의사항
만약 example.dev → blog.example.dev로 리다이렉트하는 경우:
방법 1: 리다이렉트 전 도메인에도 ads.txt 설정 (Cloudflare Page Rules 제외)
# Cloudflare Page Rules
1. example.dev/ads.txt → 리다이렉트 안 함 (별도 처리)
2. example.dev/* → blog.example.dev/$1 (기존 리다이렉트)
방법 2: 리다이렉트된 도메인에서만 서빙 (대부분 동작함)
AdSense 크롤러가 리다이렉트를 따라가서 blog.example.dev/ads.txt를 확인하므로, 대부분의 경우 방법 2로 충분합니다.
변경 후 확인 방법
AdSense 대시보드에서 ads.txt 상태가 업데이트되는 데 24~48시간이 걸립니다. 즉시 확인하려면:
# Google의 ads.txt 검증 도구 사용
open "https://adstxt.guru/"
사이트 URL을 입력하면 실시간으로 ads.txt 파싱 결과를 보여줍니다.
6. FAQ
Q: ads.txt 없으면 AdSense 승인이 안 되나요?
A: 승인은 가능하지만, GDPR 동의 메시지(CMP) 설정 화면에서 경고가 표시되고, 유럽 지역 방문자로부터의 수익이 제한될 수 있습니다.
Q: Ghost Pro(호스팅 버전)에서는 어떻게 하나요?
A: Ghost Pro는 자체적으로 ads.txt 설정 기능을 제공하지 않습니다. Ghost 지원팀에 문의하거나, 커스텀 도메인의 DNS 레벨에서 처리해야 합니다.
Q: NPM 대신 Traefik을 쓰는데 어떻게 하나요?
A: Traefik의 File Provider로 미들웨어를 설정하거나, 별도의 정적 파일 서버 컨테이너를 추가해야 합니다.
# traefik 동적 설정 예시
http:
routers:
ads-txt:
rule: "Path(`/ads.txt`)"
service: static-files
priority: 100
Q: ads.txt를 수정하면 바로 반영되나요?
A: NPM은 설정 저장 시 자동으로 nginx를 reload합니다. 파일 내용만 수정한 경우에도 즉시 반영됩니다 (nginx restart 불필요).
Q: 여러 도메인에 같은 ads.txt를 적용하려면?
A: 각 Proxy Host의 Advanced 설정에 동일한 location 블록을 추가하면 됩니다. 또는 NPM의 Custom Nginx Configuration을 사용해 전역 설정으로 관리할 수 있습니다.
7. 참고 자료
8. 관련 글
이 글은 Ghost 셀프 호스팅 시리즈의 일부입니다.
시리즈 목차:
- Oracle Cloud에서 무료로 Ghost 블로그 운영하기
- Ghost에서 Google AdSense ads.txt 설정하기 ← 현재 글
- Ghost 뉴스레터를 위한 Mailgun 연동 (예정)