새로운 VPS 프로덕션 설정 (초보자용)
개요
이 스킬은 깨끗한 Ubuntu 서버를 프로덕션 환경으로 준비하는 과정을 단계별로 안내하며, 이후에는 최대한 자동으로 관리될 수 있도록 합니다. 대상 독자는 시스템 관리 경험이 없는 사람입니다. 따라서:
- 전문가의 방식이 아닌 안전한 기본값을 사용합니다. NOPASSWD-sudo나 편의를 위한 보안 검사 비활성화는 없습니다. 전문가의 삶을 편하게 하는 것은 초보자에게는 위험 요소입니다.
- 자동화를 통한 자율성. 보안 업데이트, 백업 및 모니터링은 자동으로 작동해야 합니다. 사용자는 알림에만 대응하면 됩니다.
- 각 단계를 확인합니다. 변경 후에는 결과를 확인하고, 그 다음에야 다음 단계로 넘어갑니다.
대상 OS: Ubuntu 22.04 / 24.04 LTS. 다른 배포판의 명령은 다릅니다.
변경 전 필수 규칙
- 설정 파일 백업:
sudo cp /path/config /path/config.bak.$(date +%F-%H%M) - 상태 확인:
sudo systemctl status <service>/sudo docker ps - 변경 후 결과 확인 (로그, 상태, 테스트 연결).
- 현재 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를 읽으십시오 — 단계별로 설명되어 있습니다.
절대 위반해서는 안 되는 핵심 원칙:
- 백업은 오프사이트여야 합니다. 동일한 서버(또는 디스크)의 복사본은 서버/디스크 장애 시 도움이 되지 않습니다. 최소 — 외부 스토리지(S3 호환: Backblaze B2, rsync.net 등).
- 암호화 비밀번호는 서버 외부에 보관하십시오 (비밀번호 관리자). 비밀번호를 분실하면 모든 백업을 영원히 읽을 수 없습니다. 이것이 백업을 잃는 가장 흔한 방법입니다.
- 복원 확인 없는 백업은 백업이 아닙니다. 적어도 한 번은 테스트 복원을 수행하십시오 (backups.md의 '교육용 복원' 참조).
- 자동화: systemd 타이머를 통한 일일 실행 + 오래된 스냅샷 로테이션.
백업할 항목
개별 경로가 아닌 카테고리로 생각하십시오. 그래야 잊지 않습니다.
| 카테고리 | 경로 예 | 이유 |
|---|---|---|
| 시스템 설정 | /etc 전체 | nginx, ssh, ufw, fail2ban, systemd 유닛 — 서버를 '있던 그대로' 복원하려면 |
| 애플리케이션 데이터 | /home/<USERNAME>/app/data, /var/lib/<service> | 재설치할 수 없는 것: 사용자 콘텐츠, 업로드 |
| DB 덤프 | /var/backups/*.sql.gz | restic 전에 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. 모니터
…
[잘림]