본문으로 이동
·11분 읽기

진짜 작동하는 VPS 설정: Node.js, PM2, Nginx, 그리고 무중단 배포

제가 프로덕션에서 사용하는 정확한 VPS 배포 설정 — Ubuntu 보안 강화, PM2 클러스터 모드, Nginx 리버스 프록시, SSL, 그리고 한 번도 실패하지 않은 배포 스크립트. 이론 없이, 작동하는 것만.

공유:X / TwitterLinkedIn

이 블로그는 월 $10 VPS에서 돌아갑니다. Vercel도 아니고, AWS도 아니고, 6명의 팀이 관리하는 Kubernetes 클러스터도 아닙니다. Nginx, PM2, 그리고 30초 이내에 배포하는 bash 스크립트가 있는 단일 Ubuntu 박스입니다.

저는 다른 경로를 시도해 봤습니다. Vercel을 사용했습니다 (크론 작업, 영구 WebSocket, 또는 그냥 제어가 필요하기 전까지는 훌륭합니다). AWS를 사용했습니다 (하루의 절반을 IAM 정책에 쓰는 것을 즐긴다면 훌륭합니다). 결국 항상 VPS로 돌아옵니다.

하지만 문제가 있습니다: 인터넷의 모든 "VPS에 배포" 튜토리얼은 해피 패스에서 멈춥니다. Node.js를 설치하고 node server.js를 실행하는 법을 보여주고 프로덕션이라고 부릅니다. 그러면 서버가 SSH 무차별 대입 공격을 받고, 프로세스 매니저를 설정하지 않아 새벽 3시에 프로세스가 죽고, SSL 인증서는 3개월 전에 만료되었습니다.

이것이 제가 있었으면 했던 가이드입니다. 여기 있는 모든 것은 실전 검증되었습니다 — 이 정확한 설정이 여러분이 지금 읽고 있는 페이지를 서빙합니다.

코드가 아닌 보안부터 시작하세요#

Node.js를 생각하기도 전에 서버를 잠그세요. 새로운 VPS 인스턴스는 타깃입니다. 자동화된 봇이 프로비저닝 후 수분 내에 SSH 포트를 때리기 시작합니다.

root가 아닌 사용자 생성#

bash
adduser deploy
usermod -aG sudo deploy

SSH 키 인증 설정#

로컬 머신에서:

bash
ssh-copy-id deploy@your-server-ip

그 다음 비밀번호 인증을 완전히 비활성화합니다:

bash
sudo nano /etc/ssh/sshd_config
bash
PasswordAuthentication no
PermitRootLogin no
bash
sudo systemctl restart sshd

이것을 건너뛰면 며칠 내에 인증 로그에서 수천 건의 실패한 로그인 시도를 보게 됩니다. 이것은 편집증이 아닙니다 — 공개 인터넷에서는 일상입니다.

UFW 방화벽#

bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

이게 전부입니다. 규칙 4개. SSH와 웹 트래픽만 통과합니다.

Fail2Ban#

bash
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

/etc/fail2ban/jail.local을 편집합니다:

ini
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
bash
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

SSH 실패 3번이면 1시간 차단입니다. Fail2Ban이 하루에 수백 개의 IP를 차단하는 것을 봤습니다. 작동합니다.

자동 보안 업데이트#

bash
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

서버가 이제 보안 패치를 자동으로 설치합니다. 잊어버릴 일이 하나 줄었습니다.

Node.js: apt 말고 NVM을 사용하세요#

모든 튜토리얼에서 이것을 봅니다: sudo apt install nodejs. 하지 마세요.

Ubuntu의 패키지 저장소는 오래된 Node.js 버전을 제공합니다. NodeSource PPA조차 뒤처집니다. 그리고 다른 프로젝트를 위해 Node 20과 Node 22 사이를 전환해야 할 때 막힙니다.

NVM이 이것을 해결합니다:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*

확인합니다:

bash
node -v  # v22.x.x 또는 현재 LTS 버전
npm -v

비자명한 팁: NVM으로 글로벌 패키지를 설치하면 (PM2 같은) 그 Node 버전에 묶입니다. nvm use로 버전을 전환하면 글로벌 패키지가 사라집니다. 서버에서는 기본값을 설정하고 그대로 유지하세요:

bash
nvm alias default 22

이것에 정확히 한 번 당했습니다. 한 번이면 충분했습니다.

PM2: 제 값을 하는 프로세스 매니저#

PM2는 "배포됨"과 "프로덕션 준비 완료" 사이의 차이입니다. 프로세스 관리, 클러스터링, 로그 로테이션, 크래시 시 자동 재시작, 스타트업 스크립트를 처리합니다. 무료로.

설치 및 설정#

bash
npm install -g pm2

이코시스템 설정#

CLI 플래그로 앱을 시작하지 마세요. ecosystem.config.js 파일을 사용하세요. 버전 관리되고, 재현 가능하며, 자체 문서화됩니다.

javascript
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "akousa",
      script: "node_modules/.bin/next",
      args: "start -p 3002",
      cwd: "/var/www/akousa.net",
      instances: 2,
      exec_mode: "cluster",
      max_memory_restart: "500M",
      env: {
        NODE_ENV: "production",
        PORT: 3002,
      },
      // 그레이스풀 셧다운
      kill_timeout: 5000,
      listen_timeout: 10000,
      wait_ready: false,
      // 로깅
      log_date_format: "YYYY-MM-DD HH:mm:ss Z",
      error_file: "/var/log/pm2/akousa-error.log",
      out_file: "/var/log/pm2/akousa-out.log",
      merge_logs: true,
      // 실패 시 자동 재시작
      autorestart: true,
      max_restarts: 10,
      min_uptime: "10s",
      // 프로덕션에서는 watch 하지 마세요
      watch: false,
    },
  ],
};

중요한 선택들을 설명하겠습니다:

instances: 2 ("max" 대신): 1-2 코어의 작은 VPS에서 "max"는 스마트하게 들리지만 빌드 중에 리소스를 놓고 싸우는 프로세스를 생성합니다. 2개 인스턴스는 여유를 남기면서 무중단 리로드를 제공합니다. 4+ 코어 머신에서는 물론 "max"를 사용하세요.

exec_mode: "cluster": 이것이 무중단 리로드를 가능하게 합니다. 클러스터 모드 없이 pm2 reload는 그냥 멋진 재시작일 뿐입니다. 클러스터 모드에서 PM2는 인스턴스를 하나씩 재시작합니다 — 앱이 완전히 오프라인이 되는 순간이 없습니다.

max_memory_restart: "500M": Next.js 앱에 메모리 누수가 있나요? PM2가 서버를 OOM 킬하기 전에 재시작합니다. 이것이 새벽 2시 알림에서 저를 여러 번 구해줬습니다.

kill_timeout: 5000: PM2가 강제 종료하기 전에 앱이 진행 중인 요청을 마칠 수 있도록 5초를 줍니다. 기본값(1600ms)은 데이터베이스 연결이 있는 앱에서는 너무 공격적입니다.

watch: false: 프로덕션에서 watch: true를 남겨두는 사람들을 봤습니다. 그러면 PM2가 로그 파일이 변경될 때마다 앱을 재시작합니다. 앱이 재시작 루프에 빠집니다. 하지 마세요.

스타트업 스크립트#

PM2가 리부트를 견디게 만드세요:

bash
pm2 startup systemd
# 출력되는 명령을 복사해서 실행
pm2 save

이것은 systemd 서비스를 생성합니다. 서버 리부트 후 앱이 자동으로 돌아옵니다. 테스트하세요 — 서버를 리부트하고 확인하세요. 가정하지 마세요.

로그 로테이션#

로그가 결국 디스크를 잡아먹습니다. 로테이션 모듈을 설치하세요:

bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

파일당 최대 50MB, 로테이션된 파일 7개 보관, 오래된 것은 압축. 이것 없이 적당한 트래픽의 앱에서 /var/log가 3주 만에 25GB 디스크를 채우는 것을 봤습니다.

Nginx: 생각보다 많은 것을 하는 리버스 프록시#

"Node.js를 직접 80 포트에 노출하면 안 되나요?"

Nginx가 Node.js가 사이클을 낭비하지 않아야 할 것들을 처리하기 때문입니다: SSL 종료, 정적 파일 서빙, gzip 압축, 요청 버퍼링, 연결 제한, 느린 클라이언트의 그레이스풀 처리. C로 작성되었고 이 목적에 맞게 만들어졌습니다.

설치#

bash
sudo apt install nginx -y

설정#

nginx
# /etc/nginx/sites-available/akousa.net
 
upstream node_app {
    server 127.0.0.1:3002;
    keepalive 64;
}
 
server {
    listen 80;
    listen [::]:80;
    server_name akousa.net www.akousa.net;
 
    # 모든 HTTP를 HTTPS로 리다이렉트
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name akousa.net www.akousa.net;
 
    # SSL (Certbot이 관리 — 이 줄들은 자동으로 추가됨)
    ssl_certificate /etc/letsencrypt/live/akousa.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/akousa.net/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
 
    # 보안 헤더
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
 
    # Gzip 압축
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml
        application/wasm;
 
    # 프록시 설정
    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
 
        # 헤더
        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;
 
        # WebSocket 지원 (필요한 경우를 위해)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
 
        # 타임아웃 — 넉넉하지만 무한하지 않게
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
 
        # 버퍼링 — Nginx가 느린 클라이언트를 처리하게
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;
    }
 
    # Next.js 정적 에셋 — Nginx가 직접 서빙
    location /_next/static/ {
        alias /var/www/akousa.net/.next/static/;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }
 
    # 퍼블릭 정적 파일
    location /static/ {
        alias /var/www/akousa.net/public/static/;
        expires 30d;
        access_log off;
    }
 
    # dot 파일 접근 차단
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

활성화합니다:

bash
sudo ln -s /etc/nginx/sites-available/akousa.net /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

리로드 전에 항상 nginx -t를 실행하세요. 문법 검사를 건너뛰고 잘못된 설정을 푸시해서 사이트를 다운시킨 적이 있습니다. nginx -t 다섯 글자가 30분의 패닉 디버깅을 절약해줬을 것입니다.

대부분의 튜토리얼이 이 설정에서 놓치는 것들:

upstream 블록과 keepalive 64: Nginx가 매 요청마다 새 TCP 연결을 여는 대신 Node.js 백엔드와의 연결을 재사용합니다. 부하 상태에서 중요합니다.

proxy_buffering on: Nginx가 Node.js의 전체 응답을 메모리로 읽은 다음 클라이언트가 처리할 수 있는 속도로 전송합니다. 이것 없이는 3G 연결의 느린 클라이언트가 Node.js 워커를 묶어둡니다.

_next/static/을 직접 서빙: 이것들은 해시된 불변 에셋입니다. Nginx가 365일 캐시 헤더와 함께 디스크에서 서빙하게 하세요. Node.js 프로세스가 이것에 시간을 낭비하면 안 됩니다.

5분 만에 SSL#

Let's Encrypt가 SSL을 해결했습니다. 2026년에도 인증서에 돈을 내고 있다면, 그만하세요.

bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.net

Certbot이 이메일을 요청하고, 이용약관에 동의하고, 자동으로 Nginx 설정을 수정해 SSL 디렉티브를 포함합니다. 끝입니다.

자동 갱신 확인#

Certbot은 하루에 두 번 확인하고 만료 30일 전에 인증서를 갱신하는 systemd 타이머를 설치합니다:

bash
sudo systemctl list-timers | grep certbot

갱신이 작동하는지 테스트합니다:

bash
sudo certbot renew --dry-run

드라이런이 통과하면 SSL에 대해 다시 생각할 일이 없습니다. 실패하면 대개 80 포트가 차단되었거나 (UFW 규칙을 확인하세요) Nginx가 실행 중이지 않기 때문입니다.

저를 잡은 한 가지: Certbot을 실행하기 전에 Nginx를 설정한 경우, 먼저 HTTPS 리다이렉트 없이 80 포트에서 리슨하는 서버 블록이 있어야 합니다. Certbot은 HTTP-01 챌린지를 위해 80 포트에 도달해야 합니다. Certbot이 성공적으로 실행된 후에 리다이렉트를 추가하세요.

배포 스크립트#

이것은 프로덕션에 푸시할 때마다 실행되는 스크립트입니다. CI/CD 플랫폼도 없고, GitHub Actions도 없습니다. SSH와 bash뿐입니다.

bash
#!/bin/bash
# deploy.sh — 거의 무중단 배포
 
set -euo pipefail
 
APP_DIR="/var/www/akousa.net"
APP_NAME="akousa"
LOG_FILE="/var/log/deploy.log"
HEALTH_URL="http://localhost:3002"
MAX_RETRIES=10
RETRY_INTERVAL=3
 
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
 
log "=== 배포 시작 ==="
 
cd "$APP_DIR"
 
# 최신 코드 가져오기
log "최신 변경사항 가져오는 중..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
 
# 의존성 설치
log "의존성 설치 중..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
 
# 빌드
log "애플리케이션 빌드 중..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
 
if [ $? -ne 0 ]; then
    log "에러: 빌드 실패. 배포 중단."
    exit 1
fi
 
# PM2 리로드 (클러스터 모드에서 무중단)
log "PM2 리로드 중..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
 
# 재시도가 있는 헬스 체크
log "헬스 체크 실행 중..."
for i in $(seq 1 $MAX_RETRIES); do
    HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" = "200" ]; then
        log "헬스 체크 통과 (HTTP $HTTP_CODE)"
        log "=== 배포 성공적으로 완료 ==="
        exit 0
    fi
    log "헬스 체크 시도 $i/$MAX_RETRIES (HTTP $HTTP_CODE). ${RETRY_INTERVAL}초 후 재시도..."
    sleep $RETRY_INTERVAL
done
 
log "에러: $MAX_RETRIES번 시도 후 헬스 체크 실패"
log "이전 PM2 상태로 롤백 중..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1

실행 권한을 부여합니다:

bash
chmod +x deploy.sh

로컬 머신에서 배포합니다:

bash
ssh root@your-server-ip "bash /var/www/akousa.net/deploy.sh"

이 스크립트의 핵심 결정들:

set -euo pipefail: 어떤 에러에서든 스크립트가 즉시 종료됩니다. 이것 없이는 실패한 npm install이 조용히 빌드 단계로 넘어가고, 디버깅에 20분을 낭비하는 모호한 에러를 얻습니다.

빌드 전 rm -rf .next: Next.js에는 가끔 오래된 출력을 생성하는 빌드 캐시가 있습니다. 이것에 당한 적이 있습니다 — 소스 코드가 업데이트되었는데도 페이지가 오래된 콘텐츠를 보여줬습니다. 빌드 디렉토리를 삭제하면 빌드에 15초 정도 추가되지만 신선한 출력을 보장합니다.

pm2 restart 대신 pm2 reload: 이것이 무중단 부분입니다. 클러스터 모드에서 reload는 롤링 재시작을 수행합니다 — 업데이트된 코드로 새 인스턴스를 올리고, 준비될 때까지 기다린 다음, 이전 인스턴스를 그레이스풀하게 종료합니다. 어떤 시점에서도 제로 인스턴스 상태가 되지 않습니다.

재시도가 있는 헬스 체크: Next.js는 재시작 후 워밍업에 몇 초가 걸립니다. 스크립트가 최대 30초 (10번 재시도 x 3초) 동안 대기하며 앱이 HTTP 200으로 응답하는지 확인합니다. 응답하지 않으면 무언가 잘못된 것이고 사용자가 아닌 즉시 알아야 합니다.

실패 시 롤백: 모든 재시도 후에도 헬스 체크가 실패하면 스크립트가 PM2를 재시작합니다 (마지막 저장된 상태를 로드). 완벽한 롤백은 아니지만 서버를 깨진 상태로 두는 것보다 낫습니다.

새벽 2시에 문제가 생겼을 때#

이 정확한 설정에서 실제로 디버깅한 것들입니다:

"사이트가 다운됐어요"#

먼저 실행할 명령들:

bash
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.log

열 중 아홉은 pm2 logs가 무슨 일이 일어났는지 즉시 알려줍니다. 누락된 환경 변수, 실패한 데이터베이스 연결, 또는 처리되지 않은 프로미스 거부.

"메모리가 계속 증가해요"#

bash
pm2 monit

프로세스별 CPU와 메모리의 실시간 대시보드를 제공합니다. 메모리가 안정화되지 않고 꾸준히 올라가면 누수가 있는 것입니다. 이코시스템 설정의 max_memory_restart 설정이 안전망입니다 — PM2가 서버를 다운시키기 전에 프로세스를 재시작합니다.

더 깊은 조사를 위해:

bash
pm2 describe akousa

업타임, 재시작 횟수, 메모리 스냅샷을 보여줍니다. 지난 24시간 동안 47번 재시작을 봤다면, 그것이 힌트입니다.

"SSL 인증서가 만료됐어요"#

bash
sudo certbot certificates

모든 인증서와 만료일을 나열합니다. 자동 갱신이 실패했다면:

bash
sudo certbot renew --force-renewal
sudo systemctl reload nginx

"디스크 공간이 가득 찼어요"#

bash
df -h
du -sh /var/log/*
pm2 flush

pm2 flush는 모든 PM2 로그 파일을 즉시 삭제합니다. 로그 로테이션을 설정하지 않았다면 (말했잖아요), 여기서 고통을 느낍니다.

매일 아침 실행하는 명령#

bash
ssh deploy@akousa.net "pm2 status && df -h / && uptime"

한 줄에 세 가지: 프로세스가 실행 중인지, 디스크가 괜찮은지, 서버에 과부하가 걸렸는지. 2초 걸립니다. 사용자보다 먼저 문제를 잡습니다.

대부분의 가이드가 말해주지 않는 것#

빌드 단계가 가장 큰 취약점입니다. 1GB RAM VPS에서 Next.js 앱의 npm run build는 800MB+ 메모리를 소비할 수 있습니다. 빌드 중에 PM2가 앱을 2개 인스턴스로 실행하고 있다면 OOM이 발생합니다. 해결책: 스왑 파일을 사용하거나 (최소 2GB), 빌드 중 앱을 정지하고 몇 초의 다운타임을 수용하세요. 저는 스왑을 사용합니다.

bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

설치 명령의 --legacy-peer-deps는 해결책이 아닌 코드 스멜입니다. 의존성 트리의 일부 패키지가 피어 의존성 범위를 업데이트하지 않았기 때문에 사용합니다. 몇 달마다 제거해 봅니다. 언젠가 될 겁니다. 그때까지는 출시합니다.

배포 스크립트를 처음부터 테스트하세요. 새로운 서버에서 저장소를 클론하고 모든 단계를 수동으로 실행하세요. 배포 스크립트에 숨어있는 "내 머신에서는 작동함" 이슈의 수는 당혹스럽습니다. 이것을 했을 때 제 스크립트에서 3가지 문제를 발견했습니다 — 누락된 글로벌 패키지, 잘못된 파일 권한, 그리고 이전 수동 설정 때문에만 존재했던 경로.

서버의 IP를 SSH 설정에 넣으세요. IP 주소 타이핑을 그만하세요:

bash
# ~/.ssh/config
Host akousa
    HostName 69.62.66.94
    User deploy
    IdentityFile ~/.ssh/id_ed25519

이제 ssh akousa만 있으면 됩니다. 작은 것들이 복리로 쌓입니다.

전체 체크리스트#

완료라고 부르기 전에:

  • sudo 접근 권한이 있는 root가 아닌 사용자
  • SSH 키 인증만, 비밀번호 인증 비활성화
  • 필요한 포트만 열린 UFW 활성화
  • SSH를 보호하는 Fail2Ban
  • 자동 보안 업그레이드 활성화
  • NVM으로 설치된 Node.js
  • 클러스터 모드로 앱을 실행하는 PM2
  • PM2 스타트업 스크립트 설정 (리부트 생존)
  • PM2 로그 로테이션 설치
  • 적절한 헤더의 Nginx 리버스 프록시
  • 자동 갱신이 있는 Let's Encrypt SSL
  • 헬스 체크가 있는 배포 스크립트
  • 스왑 파일 설정 (빌드 여유 공간용)
  • 테스트: 서버를 리부트하고 모든 것이 돌아오는지 확인

마지막 항목이 사람들이 건너뛰는 것입니다. 그런 사람이 되지 마세요. 서버를 리부트하고, 60초 기다리고, 앱이 살아있는지 확인하세요. 그렇지 않으면 스타트업 스크립트가 잘못 설정된 것이고 최악의 타이밍에 알게 됩니다.

이것이 "엔터프라이즈급"인가?#

아닙니다. 그게 핵심입니다.

이 설정은 월 $10 미만으로 이 블로그를 안정적으로 서빙합니다. 30초에 단일 명령으로 배포됩니다. 모든 부분을 이해합니다. 문제가 생기면 정확히 어디를 봐야 하는지 압니다.

Docker를 쓸 수 있을까요? 물론이죠. Kubernetes를 쓸 수 있을까요? 기술적으로는요. 스테이징 환경과 카나리 배포가 있는 완전한 CI/CD 파이프라인을 설정할 수 있을까요? 당연하죠.

하지만 최고의 인프라는 실제로 이해하고, 새벽 2시에 디버깅할 수 있으며, 프로젝트가 버는 것보다 비용이 적은 인프라라는 것을 배웠습니다. 개인 사이트, SaaS MVP, 또는 작은 스타트업에 — 이것이 바로 그 설정입니다.

먼저 출시하세요. 필요할 때 확장하세요. 그리고 항상, 항상, 새로운 서버에서 배포 스크립트를 테스트하세요.