Community코딩 & 개발github.com

igor-batrakov/new-vps-setup

초보자를 위한 새로운 Ubuntu VPS 프로덕션 설정에 대한 단계별 안전 가이드 (에이전트 스킬 + 체크리스트): 하드닝, 자동 업데이트, UFW, fail2ban, Docker, nginx+HTTPS, restic 자동 백업, 모니터링.

new-vps-setup란 무엇인가요?

이 스킬은 시스템 관리 경험이 없는 개발자나 사용자가 새로운 Ubuntu VPS를 프로덕션 환경으로 안전하게 설정할 수 있도록 단계별로 안내합니다. 보안 전문가가 아닌 초보자를 위해 설계되어, 안전한 기본값, 최대한의 자동화, 그리고 각 단계 후 결과 확인을 강조합니다. 이 스킬은 Ubuntu 22.04/24.04 LTS를 대상으로 하며, SSH 하드닝, UFW 방화벽, fail2ban brute-force 보호, Docker 설치 및 보안, nginx 리버스 프록시 및 Let's Encrypt HTTPS, restic을 사용한 오프사이트 자동 백업, 그리고 모니터링 설정을 다룹니다. 사용자는 이 가이드를 따라 VPS를 안전하게 구성하고, 자동 업데이트와 백업을 통해 장기적으로 유지보수 부담을 최소화할 수 있습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add igor-batrakov/new-vps-setup

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

새로운 VPS 프로덕션 설정 (초보자용)

개요

이 스킬은 깨끗한 Ubuntu 서버를 프로덕션 환경으로 준비하는 과정을 단계별로 안내하며, 이후에는 최대한 자동으로 관리될 수 있도록 합니다. 대상 독자는 시스템 관리 경험이 없는 사람입니다. 따라서:

  • 전문가의 방식이 아닌 안전한 기본값을 사용합니다. NOPASSWD-sudo나 편의를 위한 보안 검사 비활성화는 없습니다. 전문가의 삶을 편하게 하는 것은 초보자에게는 위험 요소입니다.
  • 자동화를 통한 자율성. 보안 업데이트, 백업 및 모니터링은 자동으로 작동해야 합니다. 사용자는 알림에만 대응하면 됩니다.
  • 각 단계를 확인합니다. 변경 후에는 결과를 확인하고, 그 다음에야 다음 단계로 넘어갑니다.

대상 OS: Ubuntu 22.04 / 24.04 LTS. 다른 배포판의 명령은 다릅니다.

변경 전 필수 규칙

  1. 설정 파일 백업: sudo cp /path/config /path/config.bak.$(date +%F-%H%M)
  2. 상태 확인: sudo systemctl status <service> / sudo docker ps
  3. 변경 후 결과 확인 (로그, 상태, 테스트 연결).
  4. 현재 SSH 세션을 절대 닫지 말고, 새 세션에서 접속과 sudo가 작동하는지 확인하세요. 원격 서버에서 SSH 연결을 잃으면 제공업체 콘솔(VNC)에 접속해야 합니다.

프로덕션 보안 원칙 (모든 단계에서 준수):

  • 비밀 (API 키, 토큰, 비밀번호)은 .env 또는 환경 변수에만 저장하고, 코드에 절대 저장하지 않습니다.
  • .env는 반드시 .gitignore에 추가합니다 (그렇지 않으면 첫 커밋 시 git에 비밀이 유출됩니다).
  • 데이터베이스는 인터넷에 노출되지 않습니다. localhost 또는 개인 네트워크/VPN을 통해서만 접근합니다.
  • 서버에 디버그 모드가 없습니다: DEBUG=False, NODE_ENV=production.
  • 애플리케이션은 root로 실행되지 않습니다. 별도의 사용자/컨테이너를 사용합니다.
  • 불필요한 열린 포트가 없습니다 (관리 페이지, 대시보드, 메트릭은 닫혀 있거나 신뢰할 수 있는 IP 뒤에 있어야 함).
  • 편의를 위해 보안 검사를 비활성화하지 마십시오 (verify=False, allowAll은 로컬에서만).
  • SSH는 키를 통해서만, 비밀번호로는 안 됩니다.

0. 진단 — 현재 상태 확인

가장 먼저 실행하세요. 호스팅 업체는 종종 부분적으로 설정된 서버(cloud-init, ufw 등)를 제공합니다.

# 시스템
lsb_release -a && uname -r
free -h && df -h && nproc

# 나는 누구인가, 권한은?
whoami && id

# SSH 설정 (파일뿐만 아니라 유효한 값)
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication|pubkeyauthentication|^port '

# 방화벽
sudo ufw status verbose

# 포트 리스닝
sudo ss -tlnp

# 자동 업데이트 (활성화되어 있나?)
systemctl status unattended-upgrades 2>/dev/null | head -5
cat /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null

# Docker / fail2ban
sudo docker ps 2>/dev/null
sudo fail2ban-client status 2>/dev/null

진단 후 사용자에게 발견된 내용을 알리고 확인 없이 설정을 시작하지 마십시오.


1. 초기 설정 및 보안

섹션 0을 확인하세요. 일부는 이미 완료되었을 수 있습니다. 두 번 하지 마십시오.

1.1 시스템 업데이트

sudo apt update && sudo apt upgrade -y
sudo apt autoremove -y
# 커널이 업데이트된 경우 재부팅이 필요합니다 (출력 내용 참조). 신중하게 재부팅하십시오:
# sudo reboot   # 약 30-60초 후에 다시 연결하십시오.

1.2 별도의 sudo 사용자 (root로 작업하지 않음)

sudo adduser <USERNAME>            # 비밀번호 설정 — 강력한 비밀번호를 선택하고 비밀번호 관리자에 저장하십시오.
sudo usermod -aG sudo <USERNAME>

초보자의 경우 sudo는 비밀번호를 요구하도록 유지합니다. 이는 의도된 것입니다. NOPASSWD-sudo는 키 또는 세션이 손상될 경우 마지막 방어선을 제거합니다. sudo 비밀번호는 SSH 키를 분실할 경우 대비한 fallback으로도 필요합니다 (1.3 참조).

1.3 SSH 키 (키로 비밀번호 없이 로그인)

키 유형은 ed25519입니다 (RSA 아님). 이름은 <server>_<device>, passphrase를 사용합니다.

로컬 머신에서:

ssh-keygen -t ed25519 -f ~/.ssh/<server>_<device> -C "<server>_<device>"
ssh-copy-id -i ~/.ssh/<server>_<device>.pub <USERNAME>@<SERVER_IP>

별도의 세션에서 키 로그인을 확인하십시오 (기존 세션을 닫지 마십시오!):

ssh -i ~/.ssh/<server>_<device> -o IdentitiesOnly=yes <USERNAME>@<SERVER_IP> 'whoami; sudo whoami'

로컬 머신의 ~/.ssh/config에 편리한 별칭:

Host <alias>
    HostName <SERVER_IP>
    User <USERNAME>
    IdentityFile ~/.ssh/<server>_<device>
    IdentitiesOnly yes

1.4 SSH 하드닝

/etc/ssh/sshd_config.d/99-hardening.conf 생성:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowTcpForwarding local
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers <USERNAME>

AllowTcpForwarding local은 로컬 포워딩(ssh -L — 도메인 설정 전에 127.0.0.1의 서비스를 확인)을 허용하고 원격 포워딩(-R)을 금지합니다. 포워딩이 전혀 필요하지 않으면 no로 설정하십시오.

적용 및 확인:

sudo sshd -t                                              # 구문 확인?
sudo systemctl restart ssh
sudo sshd -T | grep -iE 'permitroot|passwordauth'         # 유효 값 = no

주의사항 1 — cloud-init이 하드닝을 덮어씁니다. /etc/ssh/sshd_config.d/50-cloud-init.conf 파일은 종종 PasswordAuthentication yes를 포함하며 sshd_config.d/에서는 '첫 번째 일치가 우선'입니다. sudo sshd -T (유효 값)로 확인하고, 파일만 확인하지 마십시오. 덮어쓰는 경우 50-cloud-init.conf에서 해당 줄을 수정/제거하십시오.

주의사항 2 — Ubuntu 24.04는 ssh.socket을 사용합니다. sshd_config의 Port는 무시됩니다. SSH 포트를 변경하려면: sudo systemctl edit ssh.socket → ListenStream=<PORT> 추가 (기본값 제거). 제공업체 콘솔을 통해 확인하십시오 — 여기서 실수하면 접근이 차단됩니다.

1.5 자동 보안 업데이트 ⭐ (자율성의 핵심)

이것은 '서버가 스스로를 관리하게 하는' 가장 중요한 단계입니다. 이것이 없으면 패키지의 취약점이 누적됩니다.

sudo apt install unattended-upgrades apt-listchanges -y
sudo dpkg-reconfigure -plow unattended-upgrades   # 'Yes' 선택 — 20auto-upgrades 생성

/etc/apt/apt.conf.d/20auto-upgrades 확인:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";

/etc/apt/apt.conf.d/50unattended-upgrades (주요 줄의 주석을 해제/설정):

// 보안 업데이트 설치 (기본적으로 활성화됨):
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// 업데이트에 커널 재부팅이 필요한 경우 자동 재부팅. 부하가 적은 시간대 선택:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

'자동 재부팅' 결정: true = 커널 업데이트 시 서버가 밤에 자체 재부팅합니다 (더 안전하지만 짧은 다운타임 가능). false = 커널이 업데이트되지만 재부팅은 직접 해야 합니다 (모니터링 필요). 초보자의 단일 프로덕션 서버에는 일반적으로 true + 야간 시간대가 더 좋습니다.

확인 — 설치 없이 건식 실행:

sudo unattended-upgrade --dry-run --debug 2>&1 | tail -20
systemctl list-timers 'apt-daily*'   # 타이머 활성화?

1.6 fail2ban (SSH 무차별 대입 공격 방어)

기본 설정으로 충분합니다. 처음에 recidive/nftables로 복잡하게 하지 마십시오.

sudo apt install fail2ban -y

/etc/fail2ban/jail.d/sshd.local:

[sshd]
enabled = true
bantime = 1h
findtime = 30m
maxretry = 4
# 자신을 차단하지 않도록: 고정 IP 추가 (확인: curl ifconfig.me)
ignoreip = 127.0.0.1/8 ::1 <YOUR_HOME_IP>
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

1.7 시간대 및 (소형 VPS의 경우) 스왑

sudo timedatectl set-timezone <Region/City>     # 예: Europe/Moscow 또는 UTC

# RAM이 적은 VPS(< 2GB)에는 스왑이 필요합니다. OOM이 서비스를 죽이는 것을 방지합니다:
free -h
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf && sudo sysctl --system

2. UFW 방화벽

원칙: 모든 것을 닫고, 필요한 것만 엽니다. 먼저 SSH를 허용하지 않으면 자신을 잠급니다.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'      # 중요: enable 전에!
sudo ufw enable
sudo ufw status verbose

웹 서버의 경우 HTTP/HTTPS 추가:

sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

관리 페이지/대시보드/메트릭은 신뢰할 수 있는 IP에만, '모두'에게는 안 됨:

sudo ufw allow from <TRUSTED_IP> to any port <ADMIN_PORT> proto tcp comment 'admin panel'

절대 sudo ufw reset을 사용하지 마십시오 — SSH를 포함한 모든 규칙이 재설정됩니다.


3. Docker (선택 사항 — 애플리케이션이 컨테이너에 있는 경우)

설치

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker <USERNAME>      # 그룹 적용을 위해 다시 로그인

docker 그룹의 멤버십은 사실상 root와 같습니다 (docker run -v /:/host를 통해). 단일 관리자에게는 허용되지만, 서버에 대한 전체 액세스를 신뢰하지 않는 사용자에게는 이 그룹을 부여하지 마십시오.

로그 로테이션 (필수 — 그렇지 않으면 로그가 디스크를 채웁니다)

/etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo systemctl restart docker

⚠️ Docker는 UFW를 우회합니다

-p 8080:8080으로 포워딩된 포트는 UFW에서 허용하지 않았더라도 인터넷에서 액세스할 수 있습니다. 안전한 옵션:

  • localhost에만 바인딩: -p 127.0.0.1:8080:8080 (nginx/SSH 터널을 통해서만 액세스 가능).
  • DB 포트를 외부로 전혀 포워딩하지 마십시오 — docker 네트워크 내에서 서비스 이름으로 통신합니다.

4. nginx + HTTPS (선택 사항 — 웹 애플리케이션과 도메인이 있는 경우)

애플리케이션 앞의 nginx 리버스 프록시 (애플리케이션은 127.0.0.1에서 수신, 외부로는 nginx만).

sudo apt install nginx -y

/etc/nginx/sites-available/<app>:

server {
    listen 80;
    server_name <DOMAIN>;
    location / {
        proxy_pass http://127.0.0.1:<APP_PORT>;
        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;
    }
}
sudo ln -s /etc/nginx/sites-available/<app> /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Let's Encrypt를 통한 무료 HTTPS (certbot이 인증서를 발급하고 자동 갱신을 설정합니다):

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d <DOMAIN>
# 자동 갱신은 이미 타이머로 설정되었습니다. 확인:
sudo certbot renew --dry-run

보호: /etc/nginx/nginx.conf에 server_tokens off; 추가 (nginx 버전 숨김) 및 보안 헤더 (Strict-Transport-Security, X-Content-Type-Options nosniff).


5. 자동 백업 (restic + 오프사이트)

전체 지침 (설치, 저장소, 스크립트, 타이머, 복원): references/backups.md를 읽으십시오 — 단계별로 설명되어 있습니다.

절대 위반해서는 안 되는 핵심 원칙:

  1. 백업은 오프사이트여야 합니다. 동일한 서버(또는 디스크)의 복사본은 서버/디스크 장애 시 도움이 되지 않습니다. 최소 — 외부 스토리지(S3 호환: Backblaze B2, rsync.net 등).
  2. 암호화 비밀번호는 서버 외부에 보관하십시오 (비밀번호 관리자). 비밀번호를 분실하면 모든 백업을 영원히 읽을 수 없습니다. 이것이 백업을 잃는 가장 흔한 방법입니다.
  3. 복원 확인 없는 백업은 백업이 아닙니다. 적어도 한 번은 테스트 복원을 수행하십시오 (backups.md의 '교육용 복원' 참조).
  4. 자동화: systemd 타이머를 통한 일일 실행 + 오래된 스냅샷 로테이션.

백업할 항목

개별 경로가 아닌 카테고리로 생각하십시오. 그래야 잊지 않습니다.

카테고리경로 예이유
시스템 설정/etc 전체nginx, ssh, ufw, fail2ban, systemd 유닛 — 서버를 '있던 그대로' 복원하려면
애플리케이션 데이터/home/<USERNAME>/app/data, /var/lib/<service>재설치할 수 없는 것: 사용자 콘텐츠, 업로드
DB 덤프/var/backups/*.sql.gzrestic 전에 pg_dump/.backup을 통한 덤프 (라이브 DB 파일은 손상될 수 있음)
비밀.env, /root/.config/*암호화되어 백업됨 (restic은 저장소를 암호화). 암호화 비밀번호는 서버 외부에

백업하지 말아야 할 항목 (재설치 가능하며 백업만 부풀림): node_modules, .venv/venv, Docker 이미지 (docker pull), 시스템 패키지 (apt), 캐시, /var/log, /tmp. 이러한 경로는 이미 스크립트의 --exclude에 있습니다 (backups.md 참조).

보존 기간 (retention) — 정책 선택

restic은 중복 제거된 스냅샷을 저장하므로 '더 많은 복사본'이 공간을 적게 차지합니다. 명령어: restic forget --prune --keep-daily N --keep-weekly N --keep-monthly N.

정책명령어대상근거
기본 (기본값)--keep-daily 7 --keep-weekly 4 --keep-monthly 6대부분의 프로덕션 서버최근 7일 중 아무 때나 롤백 가능 + 최대 6개월 기록. 최신 오류와 '언제 망가졌는지'를 모두 잡아냅니다. backups.md에 이미 설정되어 있습니다.
간편--keep-daily 7 --keep-weekly 4귀중한 누적 데이터가 없는 사이트/봇약 1개월 기록. 공간을 최소화하고 간단합니다. 오래된 데이터 손실이 중요하지 않은 경우 적합합니다.
장기--keep-daily 14 --keep-weekly 8 --keep-monthly 12중요한 데이터가 있는 DB, 금융, 법적 요구 사항1년 기록 + 2주 일일 기록. 공간은 더 많이 차지하지만, 즉시 발견되지 않는 '조용한' 데이터 손상으로부터 보호합니다.

초보자 권장: 기본 정책으로 시작하십시오 — 저렴한 스토리지 비용으로 두 시나리오(빠른 롤백 및 장기 기록)를 모두 충족합니다. forget의 숫자만 변경하면 언제든지 장기 정책으로 전환할 수 있습니다. 이전 스냅샷은 소급하여 손실되지 않습니다.

DB 관련 중요 사항: '라이브' DB 파일을 백업하면 보존 정책이 의미가 없습니다. 파일이 손상될 수 있습니다. 항상 먼저 덤프(pg_dump / SQLite .backup)를 만들고 restic이 덤프를 백업하도록 하십시오.


6. 모니터

…

[잘림]

관련 스킬