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로, 다음과 같은 특성이 있습니다:

  1. 정적 파일 서빙 제한: 루트(/) 경로에 임의 파일을 둘 수 없음
  2. 테마 assets 폴더: /assets/ 경로만 정적 파일 서빙
  3. 라우팅 제한: 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 설정 추가

  1. Proxy Hosts 메뉴 클릭
  2. 블로그 도메인 (예: blog.example.dev) 찾아서 Edit (점 3개 메뉴)
  3. Advanced 탭 선택
  4. 기존 설정 맨 아래에 다음 추가:
# ads.txt for Google AdSense
location = /ads.txt {
    alias /data/static/ads.txt;
}
  1. 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.devblog.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 셀프 호스팅 시리즈의 일부입니다.

시리즈 목차:

  1. Oracle Cloud에서 무료로 Ghost 블로그 운영하기
  2. Ghost에서 Google AdSense ads.txt 설정하기 ← 현재 글
  3. Ghost 뉴스레터를 위한 Mailgun 연동 (예정)