빈 서버 한 대를 도메인과 자물쇠가 달린 사이트로 만드는 길을 산행 일지처럼 기록해 보세요. 서버를 처음 만져 봐도 괜찮아요. 위에서부터 한 칸씩 따라 하고, 확인까지 본 뒤에 체크하세요.
지금 내 고도
0m
정상 8,848m까지 8,848m
준비물
내 PC는?
고른 PC에 맞는 명령만 보여 드려요. 서버 안에서 치는 명령은 PC 종류와 상관없이 같습니다.
명령어 따라 하는 법
작업 위치 표시
"서버" 표시 명령은 캠프 2에서 SSH로 접속한 창에서 칩니다. 내 PC 창에 치면 안 돼요.
막혔을 때
빨간 글자(에러)가 나오면 다음 단계로 넘어가지 마세요. 에러 메시지를 통째로 복사해 검색하거나 AI에게 물어보면 대부분 풀립니다. 단, 비밀키 파일 내용 · .env 값 · 비밀번호는 절대 같이 붙여 넣지 마세요.
출발 전에 짐부터 챙깁니다
800m
0/5
사람들이 IP 숫자 대신 이름(예: myshop.kr)으로 들어오게 합니다. 마지막에 이 이름을 서버에 연결합니다.
가비아·호스팅케이알·Cloudflare 같은 도메인 등록 사이트에서 원하는 이름을 검색해 결제합니다. (.com·.kr 기준 1년 1~2만 원대)
구매 후 그 사이트의 "DNS 관리"(또는 "DNS 레코드") 메뉴를 한 번 찾아 둡니다. 캠프 5에서 씁니다.
✔ 확인 · DNS 관리 화면에 들어가서 레코드 목록이 보이면 OK
계정 비밀번호가 새도 휴대폰 인증 없이는 못 들어오게 합니다. 서버보다 계정 보안이 먼저입니다.
aws.amazon.com 에서 계정을 만듭니다. 결제 카드 등록이 필요합니다.
로그인 → 오른쪽 위 계정 이름 → "보안 자격 증명" → "MFA 디바이스 할당" → 휴대폰 인증 앱(Google Authenticator 등)으로 QR 코드를 찍어 등록합니다.
✔ 확인 · 로그아웃 후 다시 로그인할 때 인증 앱의 6자리 숫자를 물어보면 OK
실수로 비싼 서버를 켜 두거나 무료 범위를 넘으면 메일로 바로 알 수 있습니다.
AWS 콘솔 위쪽 검색창에 "Budgets" 입력 → "예산 생성" → "월별 비용 예산" 템플릿 → 금액(예: 10 USD)과 내 이메일 입력 → 생성.
✔ 확인 · Budgets 목록에 방금 만든 예산이 보이면 OK
SSH 키는 서버에 들어가는 "열쇠 파일"입니다. 비밀번호보다 훨씬 안전해서 이 열쇠로만 들어가게 만듭니다.
시작 메뉴에서 "PowerShell"을 검색해 엽니다. 아래 명령을 붙여 넣고 Enter. "passphrase"(열쇠 비밀번호)를 물으면 그냥 Enter 두 번 쳐도 됩니다.
mkdir -Force $HOME\.ssh; ssh-keygen -t ed25519 -f $HOME\.ssh\my-server -C "my-server"✔ 확인 · 아래 명령을 쳤을 때 "256 SHA256:... (ED25519)" 한 줄이 나오면 OK
ssh-keygen -lf $HOME\.ssh\my-server.pub파일이 두 개 생깁니다. my-server.pub(공개키)는 서버에 등록하는 자물쇠, my-server(비밀키)는 내 열쇠입니다. 비밀키는 절대 남에게 보내거나 채팅창·AI에 붙여 넣지 마세요.
앞으로 서버 IP, 앱 비밀번호(.env 값) 같은 걸 다룹니다. 메모장이나 채팅창에 흩어지지 않게 한곳에 모읍니다.
비밀번호 관리자(1Password, Bitwarden, 휴대폰 기본 비밀번호 앱 등)에 "내 서버" 항목을 하나 만들어 둡니다.
✔ 확인 · "내 서버" 항목이 만들어져 있으면 OK
AWS 화면에서 서버 한 대를 빌립니다
2,200m
0/3
인스턴스 = 인터넷에 늘 켜져 있는 빌린 컴퓨터입니다. 만들 때 내 공개키를 넣어 두면 내 열쇠로만 들어갈 수 있습니다.
먼저 공개키 내용을 복사합니다. 아래 명령 결과로 나온 "ssh-ed25519 AAAA... my-server" 한 줄 전체를 복사하세요.
Get-Content $HOME\.ssh\my-server.pubAWS 콘솔 오른쪽 위 지역을 "아시아 태평양(서울)"으로 바꿉니다.
검색창에 "EC2" → 왼쪽 메뉴 "네트워크 및 보안 → 키 페어" → "작업 → 키 페어 가져오기" → 이름 my-server, 내용 칸에 방금 복사한 한 줄 붙여 넣기 → 가져오기.
왼쪽 "인스턴스" → "인스턴스 시작". 이름: my-server / 이미지(AMI): Ubuntu Server 24.04 LTS / 인스턴스 유형: t3.micro 또는 t3.small (오른쪽 요금 표시 확인) / 키 페어: my-server.
"스토리지 구성"은 20GiB로 늘립니다. 아래 "네트워크 설정"은 다음 항목에서 같이 합니다.
✔ 확인 · 인스턴스를 시작한 뒤 목록에서 상태가 "실행 중"이면 OK
보안 그룹 = 서버 앞의 문지기 명단입니다. 22번 문(SSH)을 전 세계에 열어 두면 몇 분 안에 무작위 침입 시도가 시작됩니다.
인스턴스 시작 화면의 "네트워크 설정"에서: "다음에서 SSH 트래픽 허용" 체크 → 옆 선택칸을 "내 IP"로.
"인터넷에서 HTTPS 트래픽 허용", "인터넷에서 HTTP 트래픽 허용" 두 개도 체크합니다. 그다음 "인스턴스 시작".
✔ 확인 · 인스턴스 클릭 → "보안" 탭 → 인바운드 규칙이 22(내 IP 한 개), 80, 443 세 줄이면 OK
집·카페 등 장소가 바뀌면 내 IP가 달라져 SSH 접속이 안 됩니다. 그때는 보안 그룹 → 인바운드 규칙 편집 → 22번 규칙의 소스를 다시 "내 IP"로 바꾸면 됩니다.
그냥 두면 서버를 껐다 켤 때마다 주소(IP)가 바뀝니다. 고정 IP를 붙여야 도메인 연결이 안 끊깁니다.
EC2 왼쪽 "네트워크 및 보안 → 탄력적 IP" → "탄력적 IP 주소 할당" → 할당.
방금 생긴 IP 선택 → "작업 → 탄력적 IP 주소 연결" → 인스턴스 my-server 선택 → 연결.
이 IP 숫자(예: 3.35.xx.xx)를 보관소 "내 서버" 항목에 적어 둡니다. 아래 명령에서 <서버IP> 자리에 이 숫자를 넣습니다.
✔ 확인 · 인스턴스 상세 화면의 "퍼블릭 IPv4 주소"가 탄력적 IP와 같으면 OK
고정 IP는 서버에 붙어 있어도 시간당 요금이 조금 붙습니다(월 5천 원 안팎). 서버를 지울 때는 탄력적 IP도 "릴리스"해야 요금이 멈춥니다.
서버에 들어가서 문단속부터
3,900m
0/6
SSH 접속 = 내 PC 터미널로 서버를 원격 조종하는 것입니다. 이후 "서버" 표시가 붙은 명령은 전부 이렇게 접속한 창에서 칩니다.
<서버IP> 를 꺾쇠(<>)까지 지우고 내 고정 IP 숫자로 바꿔서 실행합니다.
ssh -i $HOME\.ssh\my-server ubuntu@<서버IP>처음에 "Are you sure you want to continue connecting (yes/no)?" 라고 물으면 yes 입력 후 Enter.
✔ 확인 · 줄 앞이 ubuntu@ip-172-... 처럼 바뀌면 서버 안에 들어온 것입니다. 나올 때는 exit.
서버 이미지가 만들어진 뒤에 나온 보안 패치를 받습니다.
몇 분 걸립니다. 보라색 설정 화면이 뜨면 Enter(기본값)로 넘기면 됩니다.
sudo apt update && sudo apt upgrade -y재부팅이 필요한지 확인합니다. "No such file" 이 나오면 재부팅 필요 없음. 파일 이름이 나오면 아래 재부팅 명령 → 1분 뒤 다시 접속.
ls /var/run/reboot-required(재부팅이 필요할 때만)
sudo reboot✔ 확인 · 다시 접속해서 업데이트 명령을 한 번 더 쳤을 때 "0 upgraded" 근처가 나오면 OK
기록(로그)에 찍히는 시간이 한국 시간이어야 나중에 헷갈리지 않습니다.
sudo timedatectl set-timezone Asia/Seoul✔ 확인 · Time zone 줄에 Asia/Seoul 이 보이면 OK
timedatectl열쇠(SSH 키) 없이 비밀번호 맞히기로 들어오는 길을 완전히 막습니다.
설정 파일을 하나 만듭니다. (한 줄 전체를 그대로 붙여 넣으세요)
printf "PasswordAuthentication no\nPermitRootLogin no\n" | sudo tee /etc/ssh/sshd_config.d/00-hardening.conf설정에 오타가 없는지 검사하고 적용합니다. 아무 메시지 없이 끝나면 정상입니다.
sudo sshd -t && sudo systemctl restart ssh지금 창은 그대로 두고, 내 PC에서 터미널을 하나 더 열어 처음 접속 명령으로 다시 접속해 봅니다. 들어가지면 성공입니다.
✔ 확인 · 두 줄 모두 끝이 no 이면 OK
sudo sshd -T | grep -E "passwordauthentication|permitrootlogin"새 창으로 접속이 되는 걸 확인하기 전에는 기존 창을 닫지 마세요. 실수가 있어도 기존 창에서 되돌릴 수 있습니다.
AWS 보안 그룹과 별개로 서버 안에도 문을 한 겹 더 답니다.
① SSH 문을 먼저 열어 둡니다. (이 순서가 제일 중요합니다)
sudo ufw allow OpenSSH② 웹(80·443) 문을 엽니다.
sudo ufw allow 80,443/tcp③ 방화벽을 켭니다. "Proceed with operation (y|n)?" 에 y 입력 후 Enter.
sudo ufw enable✔ 확인 · Status: active 와 함께 OpenSSH, 80,443 이 ALLOW 로 보이면 OK
sudo ufw status①을 건너뛰고 ③을 먼저 하면 그 순간 접속이 끊기고 다시 들어올 수 없습니다. 반드시 ① → ② → ③ 순서대로.
사람이 잊어도 중요한 보안 패치는 알아서 설치되게 합니다.
파란 화면이 뜨면 방향키로 <Yes> 를 고르고 Enter.
sudo apt install -y unattended-upgrades && sudo dpkg-reconfigure --priority=low unattended-upgrades✔ 확인 · enabled 가 나오면 OK
systemctl is-enabled unattended-upgrades서버 안에서 내 웹사이트를 켜 둡니다
5,600m
0/9
작은 서버는 메모리가 1GB 정도라 앱을 만드는(빌드) 도중에 멈추기 쉽습니다. 디스크 일부를 비상용 메모리로 씁니다.
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✔ 확인 · Swap 줄의 total 이 2.0Gi 근처면 OK
free -hNode.js = 자바스크립트로 만든 웹앱을 돌리는 엔진입니다. nvm 은 Node 버전을 쉽게 깔고 바꾸는 도구입니다.
① nvm 설치
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash② 방금 설치한 nvm 을 지금 창에 불러오기
source ~/.bashrc③ Node.js 최신 안정판(LTS) 설치
nvm install --lts✔ 확인 · v22.x.x 같은 버전 숫자가 두 줄 나오면 OK
node -v && npm -vGitHub에 있는 내 앱 코드를 서버의 ~/app 폴더로 내려받고, 앱에 필요한 부품(패키지)을 설치합니다.
【공개 저장소】 <계정>/<저장소> 를 내 GitHub 주소로 바꿔 실행합니다. (비공개 저장소면 이 줄 대신 아래 3단계)
git clone https://github.com/<계정>/<저장소>.git ~/app【비공개 저장소 1/3】 서버 전용 열쇠를 만들고 공개키를 화면에 띄웁니다. 나온 한 줄을 복사하세요.
ssh-keygen -t ed25519 -f ~/.ssh/deploy -N "" && cat ~/.ssh/deploy.pub【비공개 저장소 2/3】 GitHub 저장소 → Settings → Deploy keys → Add deploy key → 복사한 줄 붙여 넣기. "Allow write access" 는 체크하지 않습니다.
【비공개 저장소 3/3】 <계정>/<저장소> 를 바꿔 실행합니다. 처음에 yes/no 를 물으면 yes.
GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy" git clone git@github.com:<계정>/<저장소>.git ~/app && git -C ~/app config core.sshCommand "ssh -i ~/.ssh/deploy"【연습용 · 올릴 앱이 아직 없을 때】 위 대신 예제 Next.js 앱을 만듭니다. 질문이 나오면 Enter 로 기본값.
npx --yes create-next-app@latest ~/app --yes앱 부품 설치 (연습용 예제 앱은 이미 설치돼 있어 건너뛰어도 됩니다)
cd ~/app && npm ci✔ 확인 · 내 앱 파일 목록(package.json 등)이 보이면 OK
ls ~/app비공개 저장소를 쓰려고 내 PC의 비밀키(my-server)를 서버로 복사하지 마세요. 위처럼 서버 전용 열쇠를 따로 만듭니다.
.env 에는 DB 비밀번호·API 키 같은 값이 들어갑니다. GitHub에 올리지 않고 서버에서만 만듭니다. 내 앱에 .env 가 없으면 체크만 하고 넘어가세요.
nano 라는 메모장이 열립니다. 보관소에 있는 값을 한 줄씩 적습니다. 저장: Ctrl+O → Enter, 나가기: Ctrl+X.
nano ~/app/.env나만 읽을 수 있게 권한을 바꿉니다.
chmod 600 ~/app/.env✔ 확인 · 줄 맨 앞이 -rw------- 이면 OK
ls -l ~/app/.env빌드 = 내가 쓴 코드를 실제로 서비스할 수 있는 형태로 만드는 과정입니다. 몇 분 걸릴 수 있습니다.
cd ~/app && npm run build✔ 확인 · 빨간 Error 없이 끝나고 다시 입력줄이 나오면 OK
pm2 = 앱을 뒤에서 계속 켜 두고, 꺼지면 다시 켜 주는 관리인입니다.
npm install -g pm2✔ 확인 · 버전 숫자가 나오면 OK
pm2 -v접속 창을 닫아도, 서버가 재부팅돼도 앱이 계속 살아 있어야 합니다.
① 앱을 켭니다.
cd ~/app && pm2 start npm --name app -- start② 자동 시작 명령을 만들어 달라고 합니다. 결과 맨 끝에 "sudo env PATH=..." 로 시작하는 긴 한 줄이 나옵니다.
pm2 startup③ 방금 나온 "sudo env PATH=..." 한 줄을 통째로 복사해 붙여 넣고 Enter. (사람마다 내용이 달라서 여기에 적어 둘 수 없습니다)
④ 지금 켜진 앱 목록을 저장합니다.
pm2 save✔ 확인 · app 의 status 가 online 이면 OK
pm2 ls③을 빼먹으면 지금은 잘 돌아가도 서버가 재부팅되는 순간 사이트가 꺼집니다.
"명령이 에러 없이 끝남"과 "앱이 실제로 대답함"은 다릅니다. 바깥에 연결하기 전에 안에서 먼저 확인합니다.
✔ 확인 · 첫 줄에 HTTP/1.1 200 OK 가 보이면 OK
curl -I http://127.0.0.1:3000앱 기록 파일이 몇 달 쌓이면 디스크가 가득 차서 서버가 멈춥니다. 오래된 기록을 자동으로 정리합니다.
pm2 install pm2-logrotate✔ 확인 · 아래쪽 Module 목록에 pm2-logrotate 가 보이면 OK
pm2 lsNginx가 손님을 받아 앱에 넘깁니다
6,700m
0/4
Nginx = 웹 손님을 맞는 현관 담당입니다. 손님은 80·443번 문으로 들어오고, Nginx가 안쪽의 앱(3000번)에 전달합니다.
sudo apt install -y nginx✔ 확인 · active 가 나오면 OK
systemctl is-active nginx"이 도메인으로 온 손님은 3000번 앱에게 보내라"라고 적어 두는 파일입니다.
① 빈 설정 파일을 nano 로 엽니다.
sudo nano /etc/nginx/sites-available/app② 아래 내용을 복사해 nano 화면에 붙여 넣습니다. example.com 두 군데를 내 도메인으로 바꿉니다. 저장: Ctrl+O → Enter, 나가기: Ctrl+X.
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}✔ 확인 · 방금 붙여 넣은 내용이 그대로 나오고, example.com 이 내 도메인으로 바뀌어 있으면 OK
cat /etc/nginx/sites-available/app②의 내용은 터미널에 직접 붙여 넣는 명령이 아닙니다. 반드시 nano 화면 안에 붙여 넣으세요.
만든 설정은 "켜기" 목록(sites-enabled)에 연결해야 적용됩니다. Nginx 기본 환영 페이지는 끕니다.
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/ && sudo rm /etc/nginx/sites-enabled/default오타 검사 후 적용합니다.
sudo nginx -t && sudo systemctl reload nginx✔ 확인 · "syntax is ok" 와 "test is successful" 이 나오면 OK
sudo nginx -t처음으로 바깥에서 내 서버에 들어가 보는 순간입니다.
주소창에 http://내고정IP 를 입력합니다. (https 가 아니라 http)
✔ 확인 · "Welcome to nginx" 가 아니라 내 앱 화면이 보이면 OK
IP 숫자 대신 도메인으로
7,600m
0/3
A 레코드 = "이 이름은 이 IP로 가라"는 주소록 한 줄입니다. 맨 앞 이름(@)과 www 둘 다 만듭니다.
도메인 산 사이트 → DNS 관리 → 레코드 추가: 타입 A / 호스트(이름) @ / 값 내 고정 IP / TTL 300(또는 가장 짧은 값).
하나 더 추가: 타입 A / 호스트 www / 값 내 고정 IP.
✔ 확인 · DNS 관리 화면에 A 레코드 두 줄(@, www)이 내 IP로 보이면 OK
등록 사이트가 기본으로 넣어 둔 A 레코드(주차 페이지)나 AAAA 레코드가 남아 있으면 지우세요. 남아 있으면 캠프 6의 HTTPS 발급이 실패합니다.
주소록 변경이 인터넷 전체에 퍼지는 데 몇 분~몇 시간 걸립니다. 퍼지기 전에 다음 단계로 가면 실패합니다.
example.com 을 내 도메인으로 바꿔 실행합니다. www 도 똑같이 한 번 더 확인합니다.
✔ 확인 · Address 에 내 고정 IP 하나만 나오면 OK. 아직 다른 값이면 10분쯤 뒤 다시.
nslookup example.com 8.8.8.8Nginx 설정의 이름(server_name)과 DNS 가 둘 다 맞는지 한 번에 확인합니다.
주소창에 http://내도메인 입력. 안 되면 Nginx 설정 파일의 example.com 을 내 도메인으로 바꿨는지 다시 확인합니다.
✔ 확인 · 도메인 주소로 내 앱 화면이 보이면 OK
https 와 자동 갱신
8,400m
0/4
certbot = Let's Encrypt 의 무료 보안 인증서(https 자물쇠)를 받아 Nginx에 붙여 주는 도구입니다.
sudo apt install -y certbot python3-certbot-nginx✔ 확인 · 버전 숫자가 나오면 OK
certbot --version브라우저 주소창의 "주의 요함" 경고를 없애고 자물쇠를 답니다.
example.com 두 군데를 내 도메인으로 바꿔 실행합니다.
sudo certbot --nginx -d example.com -d www.example.com질문 순서: 이메일 입력 → 약관 동의 Y → 홍보 메일 수신 N. "Successfully deployed certificate" 가 나오면 성공입니다.
✔ 확인 · 내 도메인과 만료일(약 90일 뒤)이 보이면 OK
sudo certbot certificates누가 옛날 주소(http)로 들어와도 자물쇠 길(https)로 자동으로 옮겨 줍니다. certbot 이 보통 알아서 설정합니다.
브라우저 주소창에 http://내도메인 을 직접 입력합니다.
✔ 확인 · 주소가 https:// 로 바뀌고 자물쇠가 보이면 OK
인증서는 90일마다 갱신해야 합니다. 자동 갱신이 실패하면 3개월 뒤 갑자기 사이트가 "위험" 표시로 바뀝니다.
실제 갱신은 하지 않고 연습만 합니다.
sudo certbot renew --dry-run✔ 확인 · "Congratulations, all simulated renewals succeeded" 가 나오면 OK
남의 눈으로 마지막 점검
8,848m
0/6
내 PC는 예전 기록이 남아 있어 착각할 수 있습니다. 전혀 다른 인터넷에서 확인합니다.
휴대폰 와이파이를 끄고 브라우저에서 https://내도메인 접속.
✔ 확인 · 자물쇠와 내 앱 화면이 보이면 OK
눈으로는 안 보이는 인증서 문제까지 전문 도구로 확인합니다.
ssllabs.com/ssltest 에 내 도메인을 넣고 Submit. 2~3분 기다립니다.
✔ 확인 · 등급이 A 이상이면 OK
앱(3000번)이나 DB 문이 바깥으로 새어 열려 있으면 안 됩니다.
nmap.org 에서 nmap 을 설치한 뒤 실행합니다. 설치가 어렵다면 이 항목은 AWS 보안 그룹 인바운드 규칙이 세 줄뿐인지 다시 보는 것으로 대신해도 됩니다.
✔ 확인 · open 으로 나오는 번호가 22, 80, 443 뿐이면 OK
nmap -Pn example.com서버는 업데이트 등으로 가끔 재시작됩니다. 그때 사람이 없어도 사이트가 다시 켜져야 합니다.
접속이 끊기는 게 정상입니다.
sudo reboot✔ 확인 · 2분 뒤 브라우저로 https://내도메인 이 열리면 OK. 안 열리면 다시 접속해서 pm2 ls 로 app 이 online 인지 확인.
앞으로 앱을 고칠 때마다 이 한 줄을 씁니다. (내 PC에서 고친 코드를 GitHub에 먼저 올린 뒤 실행)
cd ~/app && git pull && npm ci && npm run build && pm2 restart app✔ 확인 · 고친 내용이 사이트에 보이면 OK
나중에 뭔가 망가져도 이 시점으로 돌아올 수 있는 저장 지점입니다.
EC2 → 인스턴스 → my-server 선택 → "작업 → 이미지 및 템플릿 → 이미지 생성" → 이름 입력 → 생성.
✔ 확인 · EC2 왼쪽 "이미지 → AMI" 목록에서 상태가 "사용 가능"이 되면 OK