📚 1인 인프라 구축기 #4

Umami 셀프호스팅 — Docker로 GA4 대안 애널리틱스 구축하고 AdBlock 우회까지

Oracle ARM 서버에 Umami를 Docker로 올리고, 4개 블로그에 트래커를 심으면서 만난 삽질들. TRACKER_SCRIPT_NAME이 안 먹는 문제부터 AdBlock 우회, WP mu-plugin 자동 매핑까지 실전 트러블슈팅.

📚 1인 인프라 구축기 시리즈 (4편)

Umami 셀프호스팅 — Docker로 GA4 대안 애널리틱스 구축하고 AdBlock 우회까지

💡 Tip. 바쁜 현대인들을 위한 본문 요약

  • **Umami**는 GA4의 경량 대안으로, Docker 한 방이면 셀프호스팅 가능하다
  • TRACKER_SCRIPT_NAME 환경변수가 공식 이미지에서 실제로 동작하지 않는다 — Nginx 리라이트로 우회
  • 트래커 스크립트를 /script.js 그대로 두면 AdBlock에 100% 차단된다
  • WordPress 멀티사이트에선 mu-plugin으로 website-id를 자동 매핑하는 게 정답
  • Astro 정적 사이트는 BaseLayout에 직접 삽입하면 끝

![IMG: Umami 대시보드 화면 — 깔끔한 웹 애널리틱스 인터페이스]

GA4를 쓰고 있었다. 잘 돌아간다. 근데 문제가 있다.

내 데이터가 Google 서버에 있다. GDPR 배너를 달아야 하고, 유럽 방문자는 쿠키 동의 전까지 추적이 안 된다. 무엇보다 — GA4 UI가 너무 복잡하다. 페이지뷰 하나 보려고 탐색 보고서를 만들어야 하는 건 뭔가 잘못됐다.

Umami는 다르다. 쿠키 없음, GDPR 걱정 없음, 대시보드가 깔끔함. Docker 이미지 하나면 내 서버에서 돌아간다. Oracle ARM 서버에 여유 자원이 있으니 올려보기로 했다.

같은 서버 구축 과정이 궁금하다면 Oracle ARM + Docker로 WordPress 4사이트 운영하기도 참고해보자.


🔍 증상: 트래커 스크립트가 차단된다

이게 뭐지? 하며 버그를 발견한 순간

![IMG: AdBlock이 스크립트를 차단하는 브라우저 개발자 도구 화면]

블로그 4개에 Umami를 달았다. Docker Compose로 올리는 건 쉬웠다. 문제는 그 다음부터였다.

# docker-compose.yml (발췌)
umami:
  image: ghcr.io/umami-software/umami
  environment:
    DATABASE_URL: postgresql://umami:password@umami-db:5432/umami
    TRACKER_SCRIPT_NAME: bee
  depends_on:
    - umami-db

umami-db:
  image: postgres:16-alpine
  volumes:
    - umami-db-data:/var/lib/postgresql/data

TRACKER_SCRIPT_NAME=bee를 설정했다. 공식 문서에 “트래커 스크립트명을 바꿔서 AdBlock을 우회할 수 있다”고 적혀 있었으니까.

브라우저를 열고 https://analytics.example.com/bee.js를 요청했다. 404.

/script.js는 잘 된다. 하지만 /script.js라는 경로는 uBlock Origin이 즉시 차단한다. 모든 AdBlock 필터 목록에 /script.js가 포함되어 있다.

⚠️ 주의: /script.js 경로는 EasyList를 포함한 주요 AdBlock 필터에 명시적으로 등재되어 있다. 경로를 바꾸지 않으면 데스크탑 방문자 기준 최대 40%가 추적되지 않는다.

실제 차단율이 얼마나 될까

AdBlock 사용자 비율을 고려하면 이건 심각한 문제다.

브라우저AdBlock 사용률 (추정)/script.js 차단
Chrome (데스크탑)30~40%✅ 차단
Firefox50%+✅ 차단
Safari20~30%✅ 차단 (1Blocker 등)
모바일 Chrome5~10%대부분 통과

방문자의 30% 이상이 추적 불가 상태였다. 이건 그냥 넘길 수 없는 숫자다.


🔎 원인: 환경변수가 씹히는 이유

로그 한 줄에서 근본 원인을 찾은 순간

![IMG: Docker 컨테이너 내부에서 빌드된 파일을 확인하는 터미널 화면]

TRACKER_SCRIPT_NAME 환경변수를 파고들었다.

Umami Docker 이미지의 소스를 확인하면, 이 환경변수는 빌드 타임에 적용되어야 한다. 공식 Docker 이미지는 이미 빌드된 상태로 배포된다. 런타임에 환경변수를 넣어도 이미 번들된 파일명은 바뀌지 않는다.

# 실제 동작
ghcr.io/umami-software/umami 이미지
  └─ 빌드 시 TRACKER_SCRIPT_NAME=script (기본값)
  └─ /script.js 라우트로 고정
  └─ 런타임 env로 변경 불가 ❌

📌 핵심: TRACKER_SCRIPT_NAME은 빌드 타임 변수다. 공식 이미지를 그대로 쓰는 한, 런타임에 아무리 설정해도 효과가 없다.

직접 확인하는 방법

컨테이너에 접속해서 확인해봤다.

docker compose exec umami sh
ls /app/.next/static/chunks/
# → script.js 관련 번들이 하드코딩되어 있음

공식 이미지를 직접 빌드하면 해결되긴 한다. 하지만 ARM 환경에서 Next.js 빌드는 시간이 상당히 걸린다. 더 간단한 방법이 있다.


✅ 해결 1: Nginx 리라이트로 AdBlock 우회

5시간 삽질 끝에 드디어 버그 잡았을 때

Umami 앞에 Nginx 리버스 프록시가 있으니, 거기서 경로를 바꿔주면 된다.

# nginx/default.conf — analytics 서브도메인 블록

server {
    server_name analytics.example.com;

    # AdBlock 우회: /bee.js → /script.js
    location = /bee.js {
        proxy_pass http://umami:3000/script.js;
        proxy_set_header Host $host;
    }

    location / {
        proxy_pass http://umami:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

왜 bee.js인가

/bee.js 요청이 들어오면 Umami 내부의 /script.js로 프록시한다. 클라이언트 브라우저에서는 /bee.js만 보이므로 AdBlock 필터에 걸리지 않는다.

이름을 bee.js로 정한 이유가 있다. analytics, tracker, stats, collect 같은 단어는 AdBlock 필터에 이미 있을 가능성이 높다. 아무 의미 없는 단어가 안전하다. 꿀벌은 데이터를 모으니까 — 나름 의미도 있고. 🐝

💡 팁: 파일명 선정 기준은 하나다. “이 단어가 광고 차단 필터 제작자 눈에 의심스럽게 보일까?” 의심스러우면 바꾼다. bee, oak, lens 같은 중립적인 단어가 오래 간다.

적용 결과

# 리라이트 적용 후
curl -sI https://analytics.example.com/bee.js | head -5
# HTTP/2 200
# content-type: application/javascript

이제 블로그에 삽입하는 스크립트를 /bee.js로 바꾸면 된다.

<script defer src="https://analytics.example.com/bee.js"
        data-website-id="YOUR-WEBSITE-ID"></script>

🛠️ 해결 2: 멀티사이트 트래커 자동화

주말 전에 버그 해결 완료

![IMG: 여러 사이트의 트래픽 데이터가 나란히 보이는 애널리틱스 대시보드]

블로그가 4개다. 각각 다른 data-website-id를 넣어야 한다. 수동으로 관리하면 언젠가 반드시 섞인다.

Astro (정적 사이트) — 직접 삽입

정적 사이트니까 BaseLayout.astro에 직접 넣으면 끝이다. 빌드 타임에 고정되고, 이후엔 신경 쓸 게 없다.

<!-- src/layouts/BaseLayout.astro -->
<head>
  <script defer
    src="https://analytics.example.com/bee.js"
    data-website-id="ea933af2-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
  </script>
</head>

WordPress (3사이트) — mu-plugin 자동 매핑

WordPress에서는 functions.php를 건드리면 안 된다. 테마 업데이트 한 번에 날아간다. mu-plugin(Must-Use Plugin)을 쓰면 테마와 무관하게 항상 로드된다.

<?php
/**
 * Plugin Name: Umami Analytics Tracker
 * Description: 사이트별 Umami website-id 자동 매핑
 */

function umami_tracker_script() {
    $site_map = [
        'site-a.com' => 'aaaaaaaa-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
        'site-b.com' => 'bbbbbbbb-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
        'site-c.com' => 'cccccccc-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
    ];

    $host = $_SERVER['HTTP_HOST'] ?? '';
    $website_id = $site_map[$host] ?? '';

    if (!$website_id) return;

    echo sprintf(
        '<script defer src="https://analytics.example.com/bee.js" data-website-id="%s"></script>',
        esc_attr($website_id)
    );
}
add_action('wp_head', 'umami_tracker_script');

파일을 wp-content/mu-plugins/umami-tracker.php에 넣으면 활성화 없이 자동 로드된다. Docker 볼륨으로 마운트되어 있으니 호스트에서 파일만 넣으면 끝.

💡 팁: mu-plugins 폴더의 파일은 WordPress 관리자 화면에서 비활성화할 수 없다. 의도적인 강제 로드 구조다. 반대로 말하면, 실수로 비활성화될 일도 없다는 뜻이기도 하다.

# 3개 사이트에 동시 배포
for site in wp-content-a wp-content-b wp-content-c; do
  cp umami-tracker.php ~/wordpress/$site/mu-plugins/
done

🛡️ 예방: 셀프호스팅 애널리틱스 운영 체크리스트

원인 추적해보니 3개월 전 내 커밋

PostgreSQL 백업은 필수

Umami 데이터는 PostgreSQL에 저장된다. Docker named volume이라 컨테이너를 날려도 데이터는 살아있다. 하지만 정기 백업은 별도로 필수다. 볼륨 자체가 날아가는 상황도 있다.

# 주 1회 백업 스크립트
docker compose exec -T umami-db \
  pg_dump -U umami umami | gzip > \
  ~/backups/umami-$(date +%Y%m%d).sql.gz

업데이트 전 확인 사항

Umami 이미지를 업데이트할 때마다 꼭 확인할 것:

  1. **릴리스 노트**에서 breaking change 확인 (DB 마이그레이션 필요 여부)
  2. /bee.js 리라이트가 여전히 유효한지 확인 (내부 라우팅 변경 가능성)
  3. 환경변수 변경사항 확인 (TRACKER_SCRIPT_NAME이 런타임으로 바뀔 수도 있음)
  4. 업데이트 후 대시보드 접속 + 실시간 추적 동작 확인

AdBlock 우회 모니터링

# 월 1회: AdBlock 필터 목록에 bee.js가 추가됐는지 확인
curl -s "https://easylist.to/easylist/easylist.txt" | grep "bee.js"
# 결과 없으면 안전

⚠️ 주의: bee.js도 언젠가 AdBlock 필터에 추가될 수 있다. 그때는 파일명을 바꾸고 Nginx 리라이트만 수정하면 된다. Umami 자체는 건드릴 필요 없다 — 이게 Nginx 리라이트 방식의 가장 큰 장점이다. 대응 시간 5분이면 충분하다.


📋 정리: GA4 vs Umami 비교

드디어 모든 테스트 통과한 순간

항목GA4Umami (셀프호스팅)
비용무료무료 (서버 자원만)
데이터 소유권Google내 서버
쿠키사용 (동의 필요)미사용
GDPR배너 필수불필요
설치 난이도스크립트 1줄Docker + Nginx
대시보드복잡깔끔
실시간 추적지연 있음즉시
고급 분석✅ 퍼널/코호트❌ 기본 지표만
AdBlock 영향높음우회 가능

GA4를 완전히 걷어내진 않았다. Google Search Console 연동이나 퍼널 분석이 필요할 때는 GA4가 여전히 유용하다. 하지만 일상적인 트래픽 확인은 Umami가 압도적으로 편하다.

대시보드 열고 3초면 오늘 방문자 수가 보인다.

Cloudflare + Nginx 리버스 프록시 구성이 궁금하다면 Cloudflare Full Strict SSL + Nginx 리버스 프록시도 함께 읽어보자.


최종 아키텍처

[방문자 브라우저]

  ├─ /bee.js 요청 ──→ Cloudflare ──→ Nginx
  │                                    └─ rewrite → /script.js
  │                                    └─ proxy_pass → umami:3000

  └─ 이벤트 전송 ──→ Umami ──→ PostgreSQL (umami-db)
                                  └─ Docker named volume

Oracle ARM 무료 서버에 Umami를 올린 지 한 달. GA4 대시보드를 열어본 횟수가 눈에 띄게 줄었다.

간단한 걸 간단하게 만드는 도구가 좋은 도구다. 🚀