n8n으로 블로그 자동화 구축하기: Ghost 연동부터 실전 워크플로우까지
Oracle Cloud 무료 티어에 n8n을 셀프호스팅하고, Ghost Admin API로 블로그 포스팅을 자동화하는 완전 가이드입니다.

1. 왜 n8n인가?
블로그를 운영하다 보면 반복 작업이 쌓입니다:
- 글 발행하고 SNS에 공유
- RSS 피드 모니터링해서 관심 주제 정리
- 주기적인 뉴스레터 발송
- 백업 확인 알림
Zapier나 Make(Integromat)도 좋지만, 월 수백 개 워크플로우를 무료로 돌리려면 셀프호스팅이 답입니다.
비용 비교 (월 기준):
┌─────────────────┬──────────┬──────────────┐
│ 서비스 │ 무료 한도 │ 유료 시작가 │
├─────────────────┼──────────┼──────────────┤
│ Zapier │ 100 태스크│ $19.99 │
│ Make │ 1,000 ops │ $9 │
│ n8n Cloud │ 무료 없음 │ €20 │
│ n8n Self-hosted │ 무제한 │ 서버비만 │
└─────────────────┴──────────┴──────────────┘
Oracle Cloud 무료 티어(4 OCPU, 24GB RAM)면 n8n + Ghost + DB 전부 돌리고도 남습니다.
2. 아키텍처 구성
┌─────────────────────────────────────────────────────────┐
│ Oracle Cloud VM │
│ (4 OCPU, 24GB RAM) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Ghost │ │ n8n │ │ PostgreSQL │ │
│ │ (블로그) │ │ (자동화) │ │ (n8n DB) │ │
│ │ :2368 │ │ :5678 │ │ :5432 │ │
│ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │
│ │ │ │
│ ┌──────┴──────────────────┴──────┐ │
│ │ Nginx Proxy Manager │ │
│ │ blog.example.dev → :2368 │ │
│ │ n8n.example.dev → :5678 │ │
│ └─────────────┬──────────────────┘ │
│ │ :443 │
└────────────────┼────────────────────────────────────────┘
│
┌───────┴───────┐
│ Cloudflare │
│ (SSL/CDN) │
└───────────────┘
3. n8n 설치 (Docker Compose)
3.1 디렉토리 구조
~/docker/n8n/
├── docker-compose.yml
├── .env
└── shared/ # 워크플로우 간 파일 공유
3.2 docker-compose.yml
volumes:
n8n_storage:
postgres_storage:
networks:
n8n:
services:
postgres:
image: postgres:16-alpine
hostname: postgres
container_name: n8n-postgres
networks: ['n8n']
restart: unless-stopped
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
volumes:
- postgres_storage:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: n8nio/n8n:latest
hostname: n8n
container_name: n8n
networks: ['n8n']
restart: unless-stopped
ports:
- 5678:5678
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_PERSONALIZATION_ENABLED=false
- N8N_ENCRYPTION_KEY
- N8N_USER_MANAGEMENT_JWT_SECRET
env_file:
- .env
volumes:
- n8n_storage:/home/node/.n8n
- ./shared:/data/shared
depends_on:
postgres:
condition: service_healthy
3.3 .env 파일
# 보안을 위해 랜덤 생성
POSTGRES_USER=n8n
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
POSTGRES_DB=n8n
N8N_ENCRYPTION_KEY=$(openssl rand -base64 32)
N8N_USER_MANAGEMENT_JWT_SECRET=$(openssl rand -base64 32)
3.4 실행
cd ~/docker/n8n
docker compose up -d
# 상태 확인
docker ps --filter 'name=n8n'
4. Nginx Proxy Manager 설정
4.1 Proxy Host 추가
| 항목 | 값 |
|---|---|
| Domain Names | n8n.example.dev |
| Scheme | http |
| Forward Hostname | 172.17.0.1 |
| Forward Port | 5678 |
| Block Common Exploits | ✅ |
| Websockets Support | ✅ (필수!) |
4.2 SSL 설정
| 항목 | 값 |
|---|---|
| SSL Certificate | *.example.dev |
| Force SSL | ✅ |
| HTTP/2 Support | ✅ |
4.3 NPM 옵션 설명
| 옵션 | 역할 | n8n에서 |
|---|---|---|
| Block Common Exploits | SQL Injection, XSS 등 기본 WAF 규칙 | ✅ 권장 |
| Websockets Support | WebSocket 프로토콜 프록시 (실시간 통신) | ✅ 필수 |
중요: Websockets Support를 켜지 않으면 n8n 에디터가 실시간 동기화되지 않습니다. 워크플로우 편집 시 연결 끊김/지연이 발생합니다.
5. Ghost API 연동
5.1 Ghost에서 Integration 생성
Ghost Admin → Settings → Integrations → Add custom integration
Name: n8n
생성 후 확인:
- Content API Key: 읽기 전용 (조회용)
- Admin API Key: 읽기/쓰기 (포스팅용) ← 이것 사용
- API URL: https://blog.example.dev
5.2 n8n Credential 추가
n8n → Credentials → Add Credential → Ghost
| 항목 | 값 |
|---|---|
| API URL | https://blog.example.dev |
| Admin API Key | 64자리:64자리 형태 |
5.3 API Key 차이점
| Key 종류 | 권한 | 용도 |
|---|---|---|
| Content API Key | 읽기 | 프론트엔드 글 조회 |
| Admin API Key | 읽기/쓰기 | 글 생성, 수정, 삭제 |
6. Webhook → Ghost 포스팅 워크플로우
n8n의 기본 Ghost 노드는 custom_excerpt, meta_title 등 SEO 필드를 지원하지 않습니다. HTTP Request 노드로 Ghost Admin API를 직접 호출하면 모든 필드를 제어할 수 있습니다.
6.1 워크플로우 구조
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ Webhook │───▶│ Code │───▶│ HTTP Request │
│ (트리거) │ │ (파싱/변환) │ │ (Ghost API) │
└─────────────┘ └─────────────┘ └──────────────┘
6.2 Webhook 노드 설정
| 설정 | 값 |
|---|---|
| HTTP Method | POST (GET 아님!) |
| Path | ghost-post |
| Authentication | None (또는 Header Auth) |
주의: GET으로 설정하면 Body를 받을 수 없습니다. 반드시 POST로 설정하세요.
6.3 요청 Body 형식
Markdown 블로그 초안 전체를 content로 전송합니다:
{
"title": "글 제목",
"content": "# 제목\n\n> 요약\n\n## 1. 본문...\n\n## 15. 블로그 글 작성 시 포인트 (내부용)\n\n**Ghost SEO 설정:**\n| Post URL | `my-post-slug` |\n...",
"status": "draft"
}
6.4 Code 노드: Markdown 파싱 + Ghost API Body 생성
블로그 초안에서 필요한 정보를 추출하고, Ghost API 형식으로 변환합니다:
const content = $input.first().json.body.content;
// 1. # 제목 추출
const titleMatch = content.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1] : '';
// 2. 본문 추출 (## 1. 부터 ~ 블로그 글 작성 시 포인트 전까지)
const bodyMatch = content.match(/## 1\. [\s\S]*?(?=## \d+\. 블로그 글 작성 시 포인트)/);
const body = bodyMatch ? bodyMatch[0].trim() : '';
// 3. Ghost SEO 설정 추출 (마크다운 표에서)
const postUrlMatch = content.match(/\| Post URL \| `([^`]+)` \|/);
const metaTitleMatch = content.match(/\| Meta title \| (.+?) \|/);
const metaDescMatch = content.match(/\| Meta description \| (.+?) \|/);
const excerptMatch = content.match(/\| Excerpt \| (.+?) \|/);
// 4. Ghost API Body 생성
const ghostBody = {
posts: [{
title,
mobiledoc: JSON.stringify({
version: "0.3.1",
markups: [],
atoms: [],
cards: [["markdown", { markdown: body }]],
sections: [[10, 0]]
}),
slug: postUrlMatch ? postUrlMatch[1] : '',
custom_excerpt: excerptMatch ? excerptMatch[1].trim() : '',
meta_title: metaTitleMatch ? metaTitleMatch[1].trim() : '',
meta_description: metaDescMatch ? metaDescMatch[1].trim() : '',
status: "draft"
}]
};
return { ghostBody };
6.5 Mobiledoc 형식 이해하기
Ghost는 Lexical(5.0+ 기본), HTML, Mobiledoc 세 가지 형식을 지원합니다. Ghost 5.0부터 Lexical이 기본 포맷이지만, Mobiledoc도 여전히 작동하며 HTML을 보내면 Ghost가 자동으로 Lexical로 변환합니다. Markdown을 직접 넣으려면 Mobiledoc의 markdown 카드를 사용합니다:
{
"version": "0.3.1",
"markups": [],
"atoms": [],
"cards": [["markdown", { "markdown": "## 본문 내용..." }]],
"sections": [[10, 0]]
}
sections: [[10, 0]]은 "0번 카드를 렌더링하라"는 의미입니다.
6.6 HTTP Request 노드 설정
| 설정 | 값 |
|---|---|
| Method | POST |
| URL | https://blog.example.dev/ghost/api/admin/posts/ |
| Authentication | Predefined Credential → Ghost Admin API |
| Body Content Type | JSON |
| Specify Body | Using JSON |
Body:
={{ $json.ghostBody }}
또는 Expression 모드에서 {{ $json.ghostBody }}
6.7 테스트
curl -X POST https://n8n.example.dev/webhook/ghost-post \
-H "Content-Type: application/json" \
-d '{
"title": "테스트",
"content": "# 테스트 제목\n\n## 1. 본문\n\n내용입니다.\n\n## 15. 블로그 글 작성 시 포인트 (내부용)\n\n**Ghost SEO 설정:**\n\n| 항목 | 값 |\n|------|-----|\n| Post URL | `test-post` |\n| Meta title | 테스트 메타 제목 |\n| Meta description | 테스트 메타 설명 |\n| Excerpt | 테스트 발췌문 |",
"status": "draft"
}'
7. 실전 워크플로우 예시
7.1 예약 발행 시스템
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Schedule │───▶│ Notion DB │───▶│ Ghost │
│ 매일 9AM │ │ 조회 │ │ Publish │
└─────────────┘ └─────────────┘ └─────────────┘
Notion DB에서 "발행일 = 오늘"인 글을 찾아 Ghost에 발행합니다.
7.2 RSS → 요약 → 저장
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ RSS Feed │───▶│ OpenAI │───▶│ Ghost │
│ (30분마다) │ │ 요약 │ │ Draft 저장 │
└─────────────┘ └─────────────┘ └─────────────┘
관심 블로그의 새 글을 AI로 요약해서 초안으로 저장합니다.
7.3 Ghost 발행 → SNS 공유
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Ghost │───▶│ Format │───▶│ Threads/X │
│ Webhook │ │ Message │ │ Post │
└─────────────┘ └─────────────┘ └─────────────┘
Ghost에서 글 발행 시 Webhook으로 n8n에 알리고, SNS에 자동 공유합니다.
7.4 GitHub 커밋 → 블로그 초안
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ GitHub │───▶│ OpenAI │───▶│ Ghost │
│ Webhook │ │ 글 생성 │ │ Draft 저장 │
└─────────────┘ └─────────────┘ └─────────────┘
의미 있는 커밋이 있으면 AI가 기술 블로그 초안을 작성합니다.
8. Ghost Webhook 설정 (역방향 연동)
Ghost에서 이벤트 발생 시 n8n으로 알림을 보내려면:
8.1 Ghost에서 Webhook 추가
Ghost Admin → Settings → Integrations → n8n → Add Webhook
| 이벤트 | Target URL |
|---|---|
| Post published | https://n8n.example.dev/webhook/ghost-published |
| Member created | https://n8n.example.dev/webhook/ghost-member |
8.2 n8n에서 수신
Webhook 노드:
- Path: ghost-published
- Method: POST
Ghost가 보내는 데이터:
{
"post": {
"current": {
"id": "...",
"title": "새 글 제목",
"url": "https://blog.example.dev/new-post/",
"excerpt": "요약..."
}
}
}
9. 보안 고려사항
9.1 Webhook 인증
공개 Webhook은 누구나 호출 가능합니다. 보호 방법:
방법 1: Header Auth
n8n Webhook 노드:
- Authentication: Header Auth
- Header Name: X-API-Key
- Header Value: (비밀 키)
방법 2: IP 화이트리스트 (NPM에서)
# Advanced 탭에 추가
allow 1.2.3.4; # Ghost 서버 IP
deny all;
9.2 API Key 관리
.env파일은 절대 Git에 커밋하지 않음- n8n Credentials는 암호화되어 저장됨
- 주기적으로 API Key 로테이션
10. 트러블슈팅
10.1 Webhook이 "not registered for POST" 에러
원인: Webhook 노드가 GET으로 설정됨
해결: HTTP Method를 POST로 변경
GET은 URL 파라미터로만 데이터 전달 가능합니다. JSON Body를 받으려면 POST 필수.
10.2 Ghost API 422 "should have required property 'posts'"
// ❌ 잘못된 요청
{
"title": "제목",
"mobiledoc": "..."
}
// ✅ 올바른 요청
{
"posts": [{
"title": "제목",
"mobiledoc": "..."
}]
}
Ghost Admin API는 항상 posts 배열로 감싸야 합니다.
10.3 Ghost API 422 "mobiledoc should be string,null"
원인: mobiledoc이 객체로 전달됨 (JSON 파싱됨)
해결: JSON.stringify()로 문자열화
n8n HTTP Request에서 {{ $json.mobiledoc }}을 따옴표 없이 넣으면 객체로 파싱됩니다. Code 노드에서 미리 문자열로 만들어야 합니다.
10.4 "JSON parameter needs to be valid JSON"
원인: mobiledoc 문자열 안의 따옴표가 JSON을 깨뜨림
해결: Code 노드에서 전체 ghostBody를 만들고 {{ $json.ghostBody }}로 전달
복잡한 문자열이 포함된 JSON은 n8n 표현식으로 조립하면 깨지기 쉽습니다. Code 노드에서 완성된 객체를 만들어 전달하세요.
10.5 정규식 매칭 실패 (body가 빈 문자열)
// ❌ 매칭 안 됨 (공백 누락)
/##\d+\. 블로그/
// ✅ 매칭 됨
/## \d+\. 블로그/
마크다운 헤더는 ## 뒤에 공백이 있습니다. 정규식에서 공백을 빠뜨리면 매칭되지 않습니다.
10.6 Websocket 연결 끊김
원인: NPM에서 Websockets Support 미활성화
해결: Proxy Host 설정에서 Websockets Support 체크
10.7 Ghost API 401 에러
원인: Admin API Key 잘못됨
해결: Ghost Integration에서 Key 재생성
11. 핵심 개념 정리
| 개념 | 설명 |
|---|---|
| Workflow | 노드들의 연결, 자동화 파이프라인 |
| Node | 하나의 작업 단위 (트리거, 액션) |
| Trigger | 워크플로우 시작점 (Webhook, Schedule, etc) |
| Credential | 외부 서비스 인증 정보 |
| Expression | {{ }} 형태로 데이터 참조 |
| Lexical | Ghost 5.0+ 기본 콘텐츠 포맷 (Mobiledoc 대체) |
| Mobiledoc | Ghost의 레거시 콘텐츠 포맷 (Markdown 카드 지원, 여전히 작동) |
12. 베스트 프랙티스
- [ ] Webhook에는 반드시 인증 추가
- [ ] 에러 처리 노드(Error Trigger) 설정
- [ ] 중요 워크플로우는 실행 로그 보관
- [ ] 테스트는 Draft로 먼저, 확인 후 Published로
- [ ] API Key는 환경변수로 관리
- [ ] 복잡한 워크플로우는 Sub-workflow로 분리
- [ ] Ghost 노드 대신 HTTP Request로 SEO 필드까지 제어
- [ ] JSON 조립은 Code 노드에서 완성된 객체로
13. FAQ
Q: n8n과 Zapier 중 뭐가 좋나요?
A: 소규모면 Zapier가 편합니다. 워크플로우가 많거나 비용을 줄이려면 n8n 셀프호스팅이 유리합니다.
Q: Ghost 무료 티어로 API 사용 가능한가요?
A: Ghost는 셀프호스팅이면 무료입니다. Ghost(Pro)도 모든 플랜에서 API 제공합니다.
Q: n8n이 죽으면 예약 작업은 어떻게 되나요?
A: 놓친 스케줄은 복구되지 않습니다. restart: unless-stopped로 자동 재시작 설정하세요.
Q: Markdown을 그대로 Ghost에 넣을 수 없나요?
A: Ghost API는 Lexical(5.0+ 기본), HTML, Mobiledoc을 받습니다. HTML을 보내면 Ghost가 자동 변환하고, Mobiledoc의 markdown 카드를 사용하면 Markdown을 그대로 저장할 수 있습니다.
Q: 왜 Ghost 노드 대신 HTTP Request를 쓰나요?
A: n8n Ghost 노드는 기본 필드만 지원합니다. custom_excerpt, meta_title, meta_description 등 SEO 필드를 쓰려면 HTTP Request로 직접 API를 호출해야 합니다.
Q: 워크플로우 백업은 어떻게 하나요?
A: n8n UI에서 Export하거나, PostgreSQL DB를 백업하세요.