Last updated on

블로그 만들기


개요

목적

기간

2025. 10. 30. ~ (진행 중)

구조

정보

도메인: shinem.dev (cloudflare에서 구매, $12.20/연)

구축 목표


환경

개발 환경

기술 스택

Frontend(Head)

Backend(Headless CMS)

시스템 구조 (보완 및 데이터 흐름 추가 필요)

Headless CMS 기반 Static Site Architecture

Ghost에서 블로그를 작성하고, Astro에서 API로 콘텐츠를 받아와 정적 페이지 생성(SSG) 및 저장 후 배포

이후 추가할 내용


Node 버전 관리

개요

사용 목적: 다른 프로젝트들에서 Node v24를 사용하고 있었는데, Ghost에서는 v22만을 지원해 두 버전의 Node를 하나의 macOS에서 동시에 사용해야 함.

사용 환경: macOS (Apple Silicon, zsh)

설치

  1. nvm 설치: brew install nvm (homebrew 설치 필요)
  2. ~/.nvm 디렉토리 생성: mkdir ~/.nvm (nvm(Node Version Manager)의 설정·캐시·Node 버전들이 저장될 폴더)
  3. .zshrc (사용하는 터미널 설정 파일)에 아래 내용 추가:
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && . "/opt/homebrew/opt/nvm/nvm.sh"
  1. ~/.zshrc 변경 사항 적용: source ~/.zshrc
  2. 필요한 버전의 Node.js 모두 설치: nvm install 22 nvm install 24

버전 전환 방법

이제 프로젝트별로 다양한 Node 버전을 편리하게 사용할 수 있다!

다른 방안

Git Repository 구성

두 개의 템플릿을 사용해 Astro 설치 및 사용

개요

Astro를 사용한 블로그 Head를 구축

기존에는 Template을 1개만 사용할 수 있지만, Template (blog, statlight)를 모두 활용한 웹 페이지 구축

개발 환경

패키지 관리: pnpm

Integration: react, typescript, tailwind, mdx, seo, sitemap, rss, partytown, vercel

Template: minimal로 create하고, blog 및 starlight 추가

개발 과정

  1. Astro로 프로젝트 초기화: 빈 프로젝트에 진입 후, 터미널에서 아래 명령 실행

    pnpm create astro@latest . --template minimal

    • —template minimal: 최소한의 템플릿 설치(추후 커스터마이징을 위함)
  2. Astro Integration 추가: 아래 명령 실행

    pnpm astro add react mdx sitemap partytown vercel

    • react/typescript: React 컴포넌트 사용을 위함
    • 추가 예정
  3. Astro Utility 추가

    pnpm add @astrojs/rss astro-seo

    • 추가 예정
  4. VSCode Extension 추가 (Astro)

    • Extensions 단축키: (Ctrl + Shift + x / Cmd + Shift + x)
  5. blog 템플릿 추가

    1. blog Example 로드: 아래 명령 실행

      pnpm dlx degit withastro/astro/examples/blog temp

      • pnpm dlx: 일회성 명령 실행 (≒ npx)
      • degit: git 기록은 제외하고 복사
      • 이후 인자들: 첫 번째 인자의 내용을 두 번째 인자 하위(프로젝트 루트/temp)에 복사

      (optional) .gitignoretemp/ 추가

    2. temp/public, temp/src 내용을 현재 프로젝트에 병합: 아래 명령 실행

      *rsync 명령은 macOS가 지원

      rsync -av --progress temp/public . : temp/public 내용을 프로젝트로 복사(병합)

      rsync -av --progress temp/src . : temp/src 내용을 프로젝트로 복사(병합)

    3. astro.config.mjs 파일의 defineConfig 인자 수정:

      • site 속성 추가(배포 시 사용할 주소, 추후 변경 가능. e.g. site: “https://shinem.dev”)

      • 현재 defineConfig 호출 관련부:

        export default defineConfig({
          site: "https://shinem.dev",
          integrations: [react(), mdx(), sitemap(), partytown()],
          adapter: vercel()
        });
        
    4. pnpm dev 명령으로 정상 동작 확인

    5. temp/ 삭제 (rm -rf temp)

  6. Starlight 템플릿 추가

    1. Starlight 라이브러리 추가: 아래 명령 실행

      pnpm astro add starlight

    2. Starlight Example: basics 로드: 아래 명령 실행

      pnpm dlx degit withastro/starlight/examples/basics temp

    3. temp/src 내용을 프로젝트에 병합: 아래 명령 실행

      *rsync 명령은 macOS가 지원

      rsync -av --progress --exclude "content.config.ts" temp/src . : temp/src 내용을 프로젝트로 복사(병합)

      • --exclude "content.config.ts" : content.config.ts 는 덮어쓰기 대상에서 제외
      1. content.config.ts 병합

      기존의 src/content.config.tstemp/src/content.config.ts 의 내용을 아래와 같이 병합

      • import 부분 병합: defineCollection 은 중복 import 중이므로 temp/의 import defineCollection 부분 제거 후 나머지를 병합

      • export 병합: collections 객체에서 blog, docs 속성을 모두 내보낼 수 있게 병합

      • 현재 collections 선언 관련부:

        export const collections = {
          blog,
          docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
        };
        
    4. astro.config.ts 병합

      기존의 astro.config.tstemp/astro.config.ts 내용을 병합

      • temp/astro.config.tsstarlight() 설정을 astro.config.tsstarlight() 자리로 복사

      • 현재 defineConfig 호출 관련부:

        export default defineConfig({
          site: "https://shinem.dev",
          integrations: [
            react(),
            mdx(),
            sitemap(),
            partytown(),
            starlight({
        			title: 'My Docs',
        			social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/withastro/starlight' }],
        			sidebar: [
        				{
        					label: 'Guides',
        					items: [
        						// Each item here is one entry in the navigation menu.
        						{ label: 'Example Guide', slug: 'guides/example' },
        					],
        				},
        				{
        					label: 'Reference',
        					autogenerate: { directory: 'reference' },
        				},
        			],
        		}),
          ],
          adapter: vercel(),
        });
        
    5. Starlight의 루트를 /docs 로 라우팅

      Starlight 자체 루트 변경 기능: 현재(25. 11. 3.) 지원하지 않음

      혹시 관련 업데이트가 되었다면 알려주시면 감사하겠습니다…

      해결 방안: 직접 하위 경로로 이동 및 링킹 변경

      1. 현재 …/docs 내부의 폴더/파일을 …/docs/docs 로 이동
      • 디렉토리 구조

        src/
        ├─ content/
        │  └─ docs/
        │     ├─ docs/
        │     │  ├─ index.mdX
        │     │  └─ guides/
        │     │  │  └─ example.md
        │     │  └─ reference/
        │     │     └─ index.md
        
      1. astro.config.mjsstarlight() 설정 - sidebar 속성의 slug/directory 변경: docs/ 하위로 변경 (e.g. a/b/cdocs/a/b/c)
      • intergrations 일부:

        starlight({
        			title: 'My Docs',
        
        		social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/withastro/starlight' }],
        		sidebar: [
        			{
        				label: 'Guides',
        				items: [
        					// Each item here is one entry in the navigation menu.
        					{ label: 'Example Guide', slug: 'docs/guides/example' },
        				],
        			},
        			{
        				label: 'Reference',
        				autogenerate: { directory: 'docs/reference' },
        			},
        		],
        	}),

      1. (optional) 내부 문서 링크들의 위치 변경
        • src/content/docs/docs/index.mdx 파일의 이미지(houston.webp) 경로 변경 (../를 앞에 추가)
        • hero - actions - 첫 번째 link를 docs/ 하위로 변경
    6. pnpm dev 명령으로 정상 동작 확인

    7. temp/ 삭제 (rm -rf temp)

Ghost CMS 구축

개요

Fly.io를 활용해 무료 CMS 서버 구축

Shinem의 글 작성/관리 플랫폼 및 백엔드로 활용 예정

개발 환경

패키지 관리: pnpm

배포 환경: Oracle Cloud Free Tier

개발 과정

  1. Oracle 계정 생성: 정보 입력 및 결제 수단 등록(Free Tier를 사용하면 결제되지 않음)

    Region을 한국 춘천(Chuncheon)으로 설정(25. 11. 04. 기준)

    이메일 인증 후 약 5~10분 대기 필요

  2. Compartment 생성: Identity → Compartments에서 Create Compartment 생성

    e.g. name: weblog-infra, description: Ghost blog server

  3. Instance 생성: Instances에서 Create Instance

    Basic Information

    name: shinem-cms

    Create in compartment: weblog-infra

    Placement: AD 1 (Chuncheon)

    Image & Shape

    • ARM
      • Image: Ubuntu - Canonical Ubuntu 22.04 Minimal aarch64
        ARM Shape에서 실행한다면 aarch64가 붙은 Image를 선택해야 함
        필자는 ARM Shape 메모리가 더 커서 이를 시도했다가, Chuncheon Region의 Out of capacity 오류로 인스턴스 생성을 실패했다. 다른 방안으로 아래의 Others를 참고. 운이 좋다면 가능할 수도
      • Shape: Ampere(Arm-based processor): VM.Standard.A1.Flex (OGPU: 1, Memory: 6GB)
        (Always Free-eligible 태그가 붙은 Shape을 사용해야 한다!)
    • Others
      • Image: Ubuntu - Canonical Ubuntu 22.04 Minimal
      • Shape: Specialty and previous generation: VM.Standard.E2.1.Micro (OGPU: 1, Memory: 1GB)
        (Always Free-eligible 태그가 붙은 Shape을 사용해야 한다!)

    Security

    Shielded Instance: On (optional)


    Networking

    VNIC name: shinem-cms-vnic

    Primary network

    Primary network: Create new vcn

    New virtual cloud network name: vcn-shinem-cms

    Create in compartment: weblog-infra

    Subnet

    Create new public subnet: check

    New subnet name: subnet-shinem

    Create in compartment: weblog-infra

    CIDR block: 10.0.0.0/24

    Add SSH Keys: Generate a key pair for me

    Download private key, public key

    ~/.ssh/에 보관 추천

    private key (.key 파일)권한 변경: 소유자만 읽기/쓰기 가능:chmod 600 ~/.ssh/ssh-key-2025-11-03.key

    • private key를 분실할 경우 해당 인스턴스에 접속 불가

    Boot volume

    Specify a custom boot volume size and performance setting: Off (기본 46.6GB 사용, optional)

    Use in-transit encryption: On

    Encrypt this volume with a key that you manage: Off (optional)

    Block volumes: 추가 안 함.

  4. Instance 생성: Create 클릭

    만약 Out of capacity for shape VM.Standard.A1.Flex 등의 용량 부족 오류가 있다면: 다른 Image/Shape (3. - Basic Informations - Image & Shape - Others (not ARM))으로 시도

  5. Public subnet 연결: Instances → Compartment 선택 → shinem-cms → Networking → Connect public subnet to internet (Connect 버튼 클릭) → Create → Close → Public IP 주소 할당 완료

  6. Public IP 확인: Instance shinem-cms → Detail → Public IP Address (외부 노출 자제)

  7. SSH 연결: 터미널에서 아래 명령 실행

    ssh -i ~/.ssh/ssh_key_name.key ubuntu@Public_IP → yes 입력 → 접속 완료 (프롬프트가 ubuntu@shinem-cms로 변경됨)

  8. Ghost CMS 설치

    1. 시스템 업데이트 & 도구 설치: 아래 명령 실행

      시스템 업데이트: sudo apt update && sudo apt upgrade -y

      • 최신 커널로 변경하라는 안내가 있다면(Newer kernel available …): q / None 등을 사용해 나감 → sudo reboot → 잠시 후 다시 SSH 연결

      도구 설치: sudo apt install -y curl unzip vim ufw python3 python3-setuptools build-essential (오래 걸림)

    2. swap 메모리 생성: RAM이 너무 작아 2GB의 추가 메모리 생성

      (만약 aarch64 (RAM 6GB)라면 건너뛰어도 된다.)

      sudo fallocate -l 2G /swapfile : 2GB의 빈 파일 생성

      sudo chmod 600 /swapfile : root만 접근할 수 있게 변경

      sudo mkswap /swapfile : /swapfile 을 스왑 영역으로 초기화

      sudo swapon /swapfile : /swapfile 을 활성화

      echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab : /etc/fstab (부팅 시의 마운트 설정 파일)에 /swapfile non swap sw 0 0 을 추가: 부팅할 때 /swapfile 을 swap으로 자동 마운트

      결과 확인: free -h

    3. Node 22 & Ghost-CLI 설치

      curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - : Node 설치용 스크립트 다운로드 및 저장소 등록

      sudo apt install -y nodejs : Node 설치

      sudo npm install -g ghost-cli : Ghost-CLI 설치 (오래 걸림)

      설치 확인: ghost version

    4. Ghost 설치:

      sudo mkdir -p /var/www/ghost : Ghost 표준 디렉토리 (/var/www/ghost) 생성

      sudo chown ubuntu:ubuntu /var/www/ghost : 소유자를 ubuntu로 설정

      cd /var/www/ghost : 해당 디렉토리로 이동

      ghost install local : Ghost 설치 (오래 걸림)

      → 자동으로 ghost start 명령이 실행되어, localhost:2368 에서 Ghost가 실행 중이라는 문구가 나오면 성공이다!

      성공 여부 확인 명령: ghost ls

      • ghost start 명령은 Ghost가 설치된 위치(/var/www/ghost 등)에서 실행해야 한다!

      • 실패 기록

        나는 pnpm을 사용하려고 했다가 sqlite3를 사용하는 부분에서 충돌이 있어 다시 npm으로 시도한다. npm으로 설치하는 것을 추천한다.

        실패 원인: npm 대신 pnpm을 사용하려고 npm i -g pnpm 을 사용해 pnpm을 설치하고, 나머지 installing을 pnpm으로 진행함.

        • 설치 로그 확인 필요: Starting Ghost 실패: cd versions/6.6.0 (25. 11. 04. 기준) → pnpm add sqlite3cd /var/www/ghostghost start

        • 여전히 같은 문제가 발생한다면: sudo apt updatesudo apt install -y python3 python3-setuptools build-essential (약 10~20분 소요) → versions/6.6.0/ 으로 이동 → 아래 명령 실행

          ~~pnpm remove sqlite3 : sqlite3 제거~~

          ~~pnpm add sqlite3 : sqlite3 다시 추가/빌드~~ → 실패

        얻은 교훈: 항상 pnpm이 좋은 것이 아니다. npm을 쓰라고 하면 npm을 사용해야겠다. 그런데 다시 추가/빌드하는 부분만 npm을 사용해도 되는가? (pnpm과 npm을 섞어 써도 되는가?)에 대한 궁금증은 있다.

  9. DNS 설정: Cloudflare에서 DNS 추가

    Type: A

    Name: cms (서브도메인 설정: cms라면 cms.shinem.dev와 같이 접속)

    Content: Public IP (도메인 확인 방법: ssh 연결에서 curl ifconfig.me

    Proxy status: DNS only (인증서 발급을 위해 임시 Off, 발급 후 다시 켤 것을 추천)

    TTL: 1 hr

  10. Security Rules 변경

    Oracle VCN → vcn-shinem-dns → Security → Network Security Groups → ig-quick-action-NSG → Security Rules → Add rules

    • Add Ingress Rule: (HTTP 통신을 위함)
      • Stateless: Off
      • Source Type: CIDR
      • Source CIDR: 0.0.0.0/0
      • IP Protocol: TCP
      • Source Port Range: All
      • Destination Port Range: 80
      • Description: Allows HTTP traffic (Let's Encrypt challenge & redirect)
    • Add Ingress Rule: (HTTPS 통신을 위함)
      • Stateless: Off
      • Source Type: CIDR
      • Source CIDR: 0.0.0.0/0
      • IP Protocol: TCP
      • Source Port Range: All
      • Destination Port Range: 443
      • Description: Allows HTTPS traffic
    1. nginx 설치: 아래 명령 실행

      sudo apt install -y nginx
      sudo systemctl enable nginx
      sudo systemctl start nginx
      

      새 파일 생성: sudo vim /etc/nginx/sites-available/ghost.conf

    2. 방화벽 설정 (서버 내부, VCN)

      • 서버 내부 설정: ufw 대신 iptables 사용
        1. Ghost가 사용하는 포트 Open: sudo ufw allow 2368
        2. SSH를 위한 포트 Open: sudo ufw allow 22 (22번 포트가 안 열리면 방화벽 활성화 시 ssh 사용 불가)
        3. HTTP/HTTPS를 위한 포트 Open: sudo ufw allow 80 , sudo ufw allow 443
        4. 방화벽 활성화 및 설정 적용: sudo ufw enablesudo ufw reload
        5. 설정 확인: sudo ufw status → 2368, 22번 포트의 Open 확인
      1. ufw 비활성화

        sudo systemctl stop ufw

        sudo systemctl disable ufw

      2. iptables 설정: 아래 명령 실행

        sudo iptables -I INPUT 5 -i ens3 -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT : HTTP(80) 설정

        sudo iptables -I INPUT 5 -i ens3 -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT : HTTPS(443) 설정

        sudo netfilter-persistent save : iptables 규칙을 영구적으로 저장

      VCN 설정

      Oracle Cloud Console → Networking → VCN → VCN 선택 (vcn-shinem-cms) → Security → Default Security List for vcn-shinem-cms → Security rules:

      • Add Ingress Rule: (HTTP 통신을 위함)
        • Stateless: Off
        • Source Type: CIDR
        • Source CIDR: 0.0.0.0/0
        • IP Protocol: TCP
        • Source Port Range: All
        • Destination Port Range: 80
        • Description: Allows HTTP traffic (Let's Encrypt challenge & redirect)
      • Add Ingress Rule: (HTTPS 통신을 위함)
        • Stateless: Off
        • Source Type: CIDR
        • Source CIDR: 0.0.0.0/0
        • IP Protocol: TCP
        • Source Port Range: All
        • Destination Port Range: 443
        • Description: Allows HTTPS traffic
    3. nginx 설정

      1. ghost.conf 파일 설정: sudo vi /etc/nginx/sites-available/ghost.conf

        아래 내용 입력 후 저장 (입력 모드: i, 저장: 명령 모드에서 :wq)

        server {
            listen 80;
            listen [::]:80;
        
        server_name cms.shinem.dev;
        
        location / {
            proxy_pass http://127.0.0.1:2368;
            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;
        }
        
        location ~ /.well-known/acme-challenge {
            root /var/www/ghost/system/nginx-root/;
            allow all;
        }

        }

      2. 심볼릭 링크 생성: sudo ln -sf /etc/nginx/sites-available/ghost.conf /etc/nginx/sites-enabled/ghost.conf

      3. 설정 확인: sudo nginx -t → 마지막 문구가 ...nginx.conf test is successful 라면 성공

      4. nginx 재시작: sudo systemctl restart nginx

    4. HTTPS 인증서 발급: 아래 명령 실행

      sudo apt install -y certbot python3-certbot-nginx : 인증서 발급에 필요한 패키지 설치

      sudo certbot --nginx -d [cms.shinem.dev](http://cms.shinem.dev) : SSL 인증서 발급, Nginx 설정 수정, 80 → 443 리다이렉트, 자동 갱신(cron)
      → 이메일 입력 → ToS 동의 → 이메일로 소식 받기: 비동의 추천

    5. Ghost 설정

      /var/www/ghost/config.development.json 을 에디터로 열어 "url"을 사용 중인 url로 수정한다.

      • config.development.json 예시

        {
          "url": "https://cms.shinem.dev",
          "server": {
          ...
        
    6. Cloudflare 설정 (optional)

      Cloudflare → DNS → Records → Proxy status: Proxied 선택

    완성! 이제 https://cms.shinem.dev 에서 Ghost에 접속할 수 있다. Admin 설정은 https://cms.shinem.dev/ghost 에서 하면 된다. 필자는 설정에서 페이지 잠금을 걸어 두었다.

    • 배포 시: ghost start --production 명령을 사용

Astro Blog 개발

Ghost API 연동

  1. Ghost Integration 추가: Ghost Admin 설정 → Advanced → Add custom integration

    Content API key를 복사해 둘 것