이 블로그는 어떻게 호스팅되는가 — Astro + nginx + rsync + Cloudflare Tunnel
Vercel/Netlify 안 쓰고 집 서버에 블로그를 굴린다. 무료, 빠름, 직접 통제. 빌드 → 배포가 한 줄.
목차
이 블로그는 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/ [email protected]:/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은 홈랩 인프라의 게임 체인저. 공유기 포트 안 열고 도메인 기반 라우팅이 되는 순간 셀프호스팅의 가장 큰 장애물이 사라진다.
다음 글 후보
- Cloudflare Transform Rules로 빌드 시각 가리기 ← 이미 적음
- nginx Docker 컨테이너 conf 정리
- Astro 다크모드 토글 — JS 한 개로 끝내기
- 콘텐츠 컬렉션과 Zod 스키마
블로그를 직접 굴리면 인프라 자체가 글감이 된다. 이 글이 그 증거.