← ~/notes · 3 min read

이 블로그는 어떻게 호스팅되는가 — Astro + nginx + rsync + Cloudflare Tunnel

Vercel/Netlify 안 쓰고 집 서버에 블로그를 굴린다. 무료, 빠름, 직접 통제. 빌드 → 배포가 한 줄.

목차
  1. 구성
  2. Astro — 정적 사이트 생성기
  3. 콘텐츠 — 마크다운 + frontmatter
  4. 배포 — ./deploy.sh 한 줄
  5. nginx Docker — 단순함의 극치
  6. Cloudflare Tunnel — 도메인 라우팅
  7. 비용
  8. 단점도 있다
  9. 만들면서 알게 된 것들
  10. 다음 글 후보

이 블로그는 Vercel이나 Netlify 같은 호스팅 서비스를 쓰지 않는다. 집에 있는 Proxmox VM 한 대에서 nginx Docker 컨테이너로 정적 파일을 서빙하고, 외부 노출은 Cloudflare Tunnel로 한다.

비싸지 않고, 빠르고, 통제권이 100% 내 손에 있다.


구성

Mac (개발/배포)
  │
  │ npm run build  →  dist/ 정적 파일
  │ rsync          →  vm2:8081 nginx Docker 볼륨
  ▼
vm2.lab-it.net (Docker 서버)
  │
  │ nginx 컨테이너 (포트 8081)
  │ /usr/share/nginx/html ← rsync 대상
  ▼
Cloudflare Tunnel (cloudflared 데몬)
  │
  │ ingress: blog.hyeonbin.net → http://vm2:8081
  ▼
Cloudflare 엣지 (TLS, CDN, Transform Rules)
  │
  ▼
방문자 브라우저

Astro — 정적 사이트 생성기

Astro는 빌드 결과가 순수 HTML/CSS/JS 다. 서버 런타임 필요 없음. JS는 정말 필요한 곳에만 들어간다 (이 블로그는 테마 토글 스크립트 한 개뿐).

package.json이 정말 작다:

{
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "deploy": "./deploy.sh"
  },
  "dependencies": {
    "astro": "^5.1.0"
  }
}

의존성 1개. 31개 페이지 빌드에 943ms.


콘텐츠 — 마크다운 + frontmatter

글은 src/content/posts/*.md에 마크다운으로 쓴다. Astro의 Content Collections가 frontmatter를 검증한다.

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const posts = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    description: z.string().optional(),
    draft: z.boolean().default(false)
  })
});

export const collections = { posts };

새 글은 vim src/content/posts/2026-05-09-foo.md → 빌드 → 배포. 단 세 단계.


배포 — ./deploy.sh 한 줄

#!/usr/bin/env bash
set -e
npm run build
rsync -av --delete dist/ docker@vm2.lab-it.net:/path/to/nginx/html/
echo "✓ deployed: https://blog.hyeonbin.net"

rsync --delete로 삭제된 파일도 동기화. 6초면 끝.

vm2의 nginx 컨테이너는 마운트된 볼륨을 직접 서빙하므로 컨테이너 재시작 불필요. 파일 갱신 즉시 반영.


nginx Docker — 단순함의 극치

# docker-compose.yml (vm2)
services:
  blog-nginx:
    image: nginx:alpine
    volumes:
      - /home/docker/blog:/usr/share/nginx/html:ro
      - ./blog.conf:/etc/nginx/conf.d/default.conf:ro
    ports:
      - "127.0.0.1:8081:80"
    restart: unless-stopped

8081은 localhost에만 바인딩 — 외부에서 직접 못 닿음. Cloudflare Tunnel만 들어옴.

blog.conf도 30줄 안 된다 — 정적 파일 서빙 + gzip + try_files.


Cloudflare Tunnel — 도메인 라우팅

전 글에서 SSH 라우팅 설명한 그 터널을 그대로 쓴다. ingress에 한 줄만 추가.

ingress:
  - hostname: blog.hyeonbin.net
    service: http://172.30.1.120:8081   # vm2:8081
  - hostname: ssh.lab-it.net
    service: ssh://localhost:22
  # ... 다른 호스트들
  - service: http_status:404

DNS 레코드 자동 생성:

cloudflared tunnel route dns proxmox-ssh blog.hyeonbin.net

끝. blog.hyeonbin.net이 자동으로 https로 잡힌다 (Cloudflare가 인증서 자동 발급).


비용

항목비용
도메인 (hyeonbin.net)연 ~12,000원
호스팅 (집 서버)0원 (어차피 켜둠)
Cloudflare (Tunnel + DNS + CDN)0원 (Free 플랜)
인증서0원 (Cloudflare 자동)
합계연 12,000원

Vercel Pro($20/월), Netlify Pro($19/월)와 비교하면 24~25배 차이. 트래픽 대역폭? Cloudflare CDN이 캐시하므로 origin 트래픽은 거의 0.


단점도 있다

집 인터넷이 끊기면 사이트가 죽는다. Cloudflare가 일정 시간 캐시해주긴 하지만 영원하지 않음. 단 — KT 인터넷이 끊긴 적이 1년에 두세 시간이라 신경 안 씀.

서버 재부팅이 필요한 경우 다운타임. systemd로 자동 시작 걸어둬도 부팅 1분은 죽음. 정적 사이트라 사용자 경험엔 큰 영향 없음.

스케일은 안 됨. 트래픽 1만/일 넘어가면 집 인터넷이 못 버틸 수 있음. 그쯤 되면 Cloudflare Pages로 옮기면 됨.


만들면서 알게 된 것들

의존성 줄이기. 처음엔 RSS, sitemap, 자동 글 생성 CLI, 댓글 시스템… 다 박았다가 다 뺐다. 결국 의존성은 astro 하나, 스크립트는 dev/build/preview/deploy 넷.

서버 런타임 vs 정적. 블로그는 사용자 인터랙션이 거의 없다. SSR 필요 없다. 정적이 빠르고 안전하고 단순하다.

rsync는 전설. 30년 된 도구가 여전히 최선의 배포 도구다. 빌드 산출물 동기화엔 비교 대상이 없다.

Cloudflare Tunnel은 홈랩 인프라의 게임 체인저. 공유기 포트 안 열고 도메인 기반 라우팅이 되는 순간 셀프호스팅의 가장 큰 장애물이 사라진다.


다음 글 후보


블로그를 직접 굴리면 인프라 자체가 글감이 된다. 이 글이 그 증거.