📚 목차
[Network] Docker 기반 NestJS 서버를 AWS EC2에 배포하기
프론트엔드 개발을 주로 해왔지만, 최근에는 화면 너머의 실행 환경까지 직접 다뤄볼 필요를 더 크게 느끼고 있다.
특히 AI Native 흐름이 빨라지면서, 단순히 코드를 얼마나 빠르게 작성하느냐보다 어떤 문제를 정의하고, 어떤 구조로 서비스 전체를 이해하며, 필요한 기술을 연결해낼 수 있느냐가 더 중요해졌다고 생각했다.
이런 변화 속에서 특정 영역만 깊게 아는 것만으로는 부족하다고 느꼈다.
프론트엔드 개발자라고 해서 UI와 상태 관리만 이해하는 데 그치기보다, 백엔드 API가 어떤 구조로 동작하는지, 애플리케이션이 어떤 환경에서 실행되는지, 배포 과정에서 네트워크와 보안 설정은 어떻게 연결되는지까지 직접 경험해보고 싶었다.
원래 백엔드도 꼭 한번 제대로 다뤄보고 싶다는 생각이 있었고, 이번 기회에 NestJS로 서버를 구성하고 Docker로 실행 환경을 컨테이너화한 뒤 EC2에 배포하는 흐름까지 스스로 밟아보기로 했다.
이번 글은 단순한 배포 기록이라기보다, NestJS 애플리케이션을 만들고 Docker와 EC2를 활용해 실제 실행 환경까지 다뤄보며 백엔드와 인프라의 기본 개념을 연결해본 과정을 정리한 글이다.
특히 EC2 인스턴스 생성, 키 페어, 보안 그룹, 포트 설정 같은 네트워크 요소들이 실제 배포 과정에서 어떤 의미를 가지는지도 함께 정리해보려 한다.
1. 전체 흐름 - NestJS 애플리케이션이 EC2에서 실행되기까지

전체적인 흐름은 다음과 같다.
먼저 로컬 환경에서 NestJS 애플리케이션을 작성하고 실행 가능한 상태로 만든다.
그 다음 Docker를 사용해 애플리케이션 실행에 필요한 환경을 이미지로 묶어, 로컬과 서버 간 실행 차이를 줄인다.
이후 AWS EC2 인스턴스를 생성하면서 어떤 운영체제를 사용할지, 어떤 인스턴스 성능을 선택할지, 어떤 방식으로 서버에 접속할지를 결정한다.
이 과정에서 키 페어를 생성해 SSH 접속 기반을 마련하고, 보안 그룹을 설정해 어떤 포트를 누구에게 열어둘지 정의한다.
이후에는 EC2 인스턴스에 접속해 Docker 이미지를 실행하고, NestJS 애플리케이션이 서버 환경에서 실제로 동작하도록 구성한다.
마지막으로 퍼블릭 IP와 인바운드 규칙을 통해 외부에서 해당 서버에 접근할 수 있는지 확인하게 된다.
즉, 이번 배포 과정은 단순히 코드를 올리는 작업이 아니라 애플리케이션, 실행 환경, 서버, 네트워크, 보안 설정이 하나의 서비스로 연결되는 과정이라고 볼 수 있다.
이 글도 그 순서에 맞춰 진행한다.
먼저 EC2 인스턴스를 생성하면서 서버가 배치되는 기본 네트워크 환경을 이해하고, 이어서 키 페어와 보안 그룹 설정을 통해 서버 접근 제어 방식을 정리한다.
그 다음 Docker를 이용해 NestJS 애플리케이션을 실행하고, 최종적으로 외부 요청이 어떤 경로로 서버에 도달하는지까지 연결해서 살펴볼 예정이다.
2. EC2 인스턴스 생성과 네트워크 환경
2-1. 인스턴스 이름과 AMI 선택: 서버의 운영체제를 고르는 단계

첫 화면에서는 인스턴스 이름을 정하고, 어떤 운영체제를 사용할지 선택했다.
여기서 중요한 건 단순히 "리눅스 하나 고르기"가 아니다.
AMI(Amazon Machine Image) 는 EC2 인스턴스를 어떤 환경으로 시작할지 정하는 템플릿이다.
즉, 이 단계는 "어떤 서버를 띄울 것인가"를 결정하는 단계다.
이번 실습에서는 Ubuntu를 선택했다.
Ubuntu는 커뮤니티에 자료가 풍부하고 가장 인기 있는 리눅스 배포판 중 하나라서, 문제 해결이나 설정 방법을 찾기가 쉽기 때문에 주로 선택한다.
네트워크 관점에서 보면 이 단계는 아직 "통신 설정" 자체는 아니지만, 이후 SSH 접속이나 패키지 설치, 서버 실행 방식에 영향을 주는 기반 환경 선택이라고 볼 수 있다.
2-2. 인스턴스 유형과 키 페어: 어떤 성능의 서버를 만들고, 어떻게 접속할 것인가?

다음 단계에서는 인스턴스 유형과 키 페어를 선택했다.
인스턴스 유형
t3.micro는 프리 티어에서 자주 사용하는 작은 인스턴스다.
학습용이나 가벼운 백엔드 실습에는 충분하다.
이건 네트워크 설정이라기보다 서버 자원 설정에 가깝지만, 실제로는 응답 속도나 처리 가능한 요청량에도 영향을 줄 수 있다.
즉, 서버의 CPU/메모리 성능은 결국 네트워크를 통해 들어오는 요청을 얼마나 잘 처리할 수 있는지와 연결된다.
키 페어
여기서 훨씬 중요한 건 키 페어다.
EC2는 비밀번호 대신 공개키/개인키 기반 SSH 인증을 많이 사용한다.
즉, "이 서버에 접속할 수 있는 사람"을 단순 패스워드가 아니라 키를 가진 사용자로 인증하는 방식이다.
- AWS에는 공개키 정보가 등록됨
- 내 로컬 컴퓨터에는 개인키(.pem)를 보관함
- 이후 SSH 접속 시 이 개인키를 사용해 인증함
이때 개인키는 절대 잃어버리면 안 된다.
네트워크 관점에서 보면, 인바운드 규칙이 "누가 들어올 수 있는지"를 정한다면 키 페어는 "들어오려는 사용자가 진짜 허용된 사용자인지"를 검증하는 수단이다.
즉,
- 보안 그룹: 네트워크 차원의 출입문
- 키 페어: 출입문을 통과한 뒤 신원을 확인하는 열쇠
2-3. 네트워크 설정

다음 화면에서는 네트워크 설정을 확인했다.
- VPC 선택
- 서브넷 선택
- 퍼블릭 IP 자동 할당 활성화
- 보안 그룹 생성
이 단계부터 진짜로 네트워크 설정이 눈에 들어오기 시작했다.
VPC
VPC(Virtual Private Cloud)는 AWS 안에 만드는 내 전용 가상 네트워크라고 이해하면 된다.
로컬 환경에서 하나의 사설망을 구성하듯, AWS 안에서도 내 인스턴스들이 속할 네트워크 공간을 나누는 개념이다.
즉, EC2는 인터넷 어딘가에 바로 “툭” 떠 있는 게 아니라,
먼저 VPC라는 네트워크 공간 안에 배치된다.
서브넷
서브넷은 VPC를 더 잘게 나눈 네트워크 구역이다.
실무에서는 퍼블릭 서브넷, 프라이빗 서브넷을 나눠서 사용하고,
웹 서버는 퍼블릭 서브넷에, DB는 프라이빗 서브넷에 두는 식으로 설계하기도 한다.
이번 화면에서는 기본 서브넷 설정을 사용했다.
퍼블릭 IP 자동 할당
이 부분이 특히 중요했다.
퍼블릭 IP를 활성화하면 이 인스턴스는 인터넷에서 접근 가능한 공인 IP를 받게 된다.
즉, 내 로컬 컴퓨터에서 SSH로 접속하거나, 브라우저에서 서버에 요청을 보내려면 보통 이 퍼블릭 IP가 필요하다.
반대로 퍼블릭 IP가 없으면 외부 인터넷에서는 직접 접근할 수 없고,
같은 VPC 내부 자원이나 배스천 호스트 등을 통해서만 접근하게 된다.
정리하면,
- VPC: 인스턴스가 속한 가상 네트워크
- 서브넷: 그 네트워크 안의 더 작은 구역
- 퍼블릭 IP: 인터넷에서 직접 접근 가능한 주소
인바운드 규칙 상세 설정
인스턴스 유형: t3.micro

상세 설정 화면에서는 인바운드 보안 그룹 규칙을 직접 편집했다.
설정한 규칙은 다음과 같았다.
SSH
- 유형: ssh
- 프로토콜: TCP
- 포트 범위: 22
- 소스: 내 IP 혹은 0.0.0.0/0 (Github Actions 접근을 위해)
이 규칙의 의미는,
내 현재 공인 IP에서만 22번 포트로 SSH 접속을 허용한다.
여기서 /32는 정확히 한 개의 IP 주소만 허용한다는 뜻이다.
즉, 관리자 접속 대상을 최소 범위로 제한한 셈이다.
참고로 SSH의 역할은 서버에 원격으로 접속해서 명령어를 실행할 수 있게 해주는 것이다.
HTTP
- 유형: HTTP
- 프로토콜: TCP
- 포트 범위: 80
- 소스: 0.0.0.0/0
이 규칙은,
전 세계 모든 IP가 이 서버의 80번 포트로 접근할 수 있다.
즉, 웹 서버로서 외부에서 HTTP 요청을 받을 수 있도록 허용한 것이다.
HTTPS
- 유형: HTTPS
- 프로토콜: TCP
- 포트 범위: 443
- 소스: 0.0.0.0/0
이 규칙도 HTTP와 마찬가지로,
전 세계 모든 IP가 이 서버의 443번 포트로 접근할 수 있다
즉, 보안이 적용된 웹 서버로서 외부에서 HTTPS 요청을 받을 수 있도록 허용한 것이다.
나중에 SSL 인증서를 적용해서 HTTPS로 서비스할 때 이 포트를 사용하게 될 것이다.
사용자 지정 TCP
- 유형: 사용자 지정 TCP
- 프로토콜: TCP
- 포트 범위: 3000
- 소스: 0.0.0.0/0
이건 내가 실행할 백엔드 애플리케이션이 3000번 포트에서 뜰 것을 고려한 설정이다.
NestJS 개발 서버가 기본적으로 3000번 포트를 많이 사용하기 때문에, 외부 브라우저나 클라이언트에서 접속하려면 해당 포트를 열어야 한다.
이 규칙은,
전 세계 모든 IP가 이 서버의 3000번 포트로 접근할 수 있다.
정리를 해보자면,
- 22번 포트: 서버 관리용 SSH
- 3000번 포트: 내가 띄울 애플리케이션 서버
- 내 IP만 허용: 관리자 접근 제한
- 0.0.0.0/0 허용: 전체 인터넷 공개
2-4. 키 페어 생성

키 페어 생성 팝업에서는 다음 항목을 선택할 수 있었다.
- 키 페어 이름: nestjs-key
- 키 페어 유형:
RSA - 프라이빗 키 파일 형식:
.pem
여기서 나는 일반적인 OpenSSH 환경에서 많이 사용하는 .pem 형식을 선택했다.
이 장면은 단순히 “파일 하나 다운로드”하는 단계처럼 보이지만, 사실은 서버 접속의 인증 모델을 이해하게 해주는 부분이다.
RSA와 ED25519
둘 다 SSH 키 알고리즘이다.
- RSA: 오래전부터 널리 쓰인 방식
- ED25519: 더 현대적이고 짧은 키 길이에서도 강력한 보안을 제공하는 방식
입문 단계에서는 RSA도 충분히 많이 쓰이지만, 환경에 따라 ED25519를 선택하기도 한다.
여기서 중요한 점은,
이 개인키 파일은 내 로컬에만 존재하는 인증 수단이다.
AWS가 대신 보관해주는 게 아니기 때문에 분실하면 접속이 곤란해질 수 있다.
즉, 네트워크 연결이 허용되어 있더라도,
올바른 개인키가 없으면 SSH 인증에 실패한다.
이걸 통해 “서버에 접속한다”는 말이 실제로는 두 단계를 거친다는 걸 이해했다.
- 보안 그룹이 네트워크 차원에서 접근 허용
- SSH 키가 사용자 인증 수행
2-5. 스토리지 구성

여기 스토리지 20GiB gp3는 디스크 용량이고, t3.micro의 **메모리(RAM)**랑 다른 개념이다.
쉽게 말하면,
| 항목 | 의미 | 부족하면 생기는 문제 |
|---|---|---|
| 스토리지 20GiB | 서버의 하드디스크/SSD 용량 | Docker 이미지, 로그, DB 파일 저장 공간 부족 |
| 메모리 RAM | 프로그램이 실행 중에 쓰는 작업 공간 | 서버/DB가 죽거나 OOM 발생 |
| CPU | 계산 처리 능력 | 요청 처리 느려짐 |
| IOPS 3000 | 디스크 읽기/쓰기 처리량 | DB 저장/조회가 느려질 수 있음 |
프리티어에서는 최대 30GiB까지만 무료로 사용할 수 있기 때문에, 20GiB로 설정했다.
3. EC2에 SSH로 접속해서 Docker를 설치
이제 인스턴스 생성까지 완료했으니,
실제 서버에 접속해서 Docker를 설치해보자.
1. 키 페어 파일 권한 설정 (로컬 터미널에서)
먼저 다운로드한 키 파일에 대한 권한을 변경해야 한다.
chmod 400 ~/desktop/<키파일이름>.pempem 키 파일을 내 계정만 읽을 수 있게 바꿨다.
2. SSH 접속
EC2 콘솔 → 인스턴스 클릭 → "연결" 버튼 → SSH 클라이언트 탭에 접속 명령어가 나온다.
복사해서 붙여넣으면 된다.
예시는 아래처럼 나온다.
ssh -i ~/Downloads/<키파일이름>.pem ec2-user@<퍼블릭IP>3. EC2 서버에 Docker 설치하기 (Ubuntu)
이제 서버에 접속했으니 Docker를 설치할 차례다.
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable docker
sudo usermod -aG docker $USER
newgrp docker
docker run hello-world
docker compose version-
- Ubuntu 패키지 목록을 최신 상태로 갱신한다.
-
- HTTPS 통신과 파일 다운로드에 필요한 기본 도구를 설치한다.
-
- Docker 저장소의 인증 키를 보관할 디렉터리를 생성한다.
-
- Docker 공식 저장소의 GPG 키를 내려받아 저장한다.
-
- apt가 Docker GPG 키 파일을 읽을 수 있도록 권한을 부여한다.
-
- 현재 Ubuntu 버전에 맞는 Docker 공식 저장소를 apt 소스 목록에 등록한다.
-
- 새로 추가한 Docker 저장소를 포함해 패키지 목록을 다시 갱신한다.
-
- Docker Engine, CLI, containerd, Buildx, Compose 플러그인을 함께 설치한다.
-
- 재부팅 후에도 Docker가 자동으로 시작되게 설정한다.
-
- 현재 사용자를 docker 그룹에 추가해 sudo 없이 Docker 명령을 쓸 수 있게 한다.
-
- 방금 추가한 그룹 권한을 현재 터미널 세션에 바로 반영한다.
-
- Docker가 정상 동작하는지 테스트 컨테이너로 확인한다.
-
- Docker Compose가 정상 설치되었는지 확인한다.
4. 백엔드 코드를 EC2로 올리기
sudo apt install -y git
git clone https://github.com/<username>/<repo>.git
cd <repo>-
- git 설치
-
- 깃허브 저장소 clone
-
- 그 안의 backend 폴더로 이동
이제 .env 파일 만들어야 한다.
EC2에는 .env가 없으니까 직접 생성한다.
nano .env
아래 내용 붙여넣고 비밀번호/시크릿은 강한 값으로 변경한다.
DB_HOST=db
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=강한비밀번호로변경
DB_DATABASE=nestjs_practice
JWT_SECRET=랜덤긴문자열로변경
JWT_REFRESH_SECRET=랜덤긴문자열로변경
저장은 Ctrl+O → Enter → Ctrl+X
이후, 아래 명령어를 통해 이미지를 빌드하고 컨테이너를 실행한다.
docker compose up -d --build
docker compose ps
docker compose logs-
- 이미지 빌드하고 MySQL + 앱 컨테이너를 백그라운드로 실행
-
- 컨테이너가 정상 실행 중인지 확인
-
- 실행 중 에러가 없는지 확인

위 이미지처럼 잘 배포된 것을 볼 수 있다!
Swap Memory 설정
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo swapon -s # 확인해당 명령어들은 EC2 인스턴스에 스왑 메모리를 설정하는 과정이다.
스왑 메모리는 실제 RAM이 부족할 때 디스크 공간을 임시 메모리로 사용하는 기능이다.
다만 디스크 I/O가 발생하기 때문에 RAM보다 속도가 훨씬 느려서, 스왑에 의존하는 상황이 지속되면 성능이 크게 저하될 수 있다.
따라서 스왑 메모리는 부족한 RAM을 보완하는 안전망으로 쓰되, 장기적으로는 RAM을 충분히 확보하는 방향으로 가는 것이 좋다.
-
- 2GB 크기의 스왑 파일을 생성한다.
-
- 스왑 파일의 권한을 600으로 설정해 보안을 강화한다.
-
- 스왑 파일을 스왑 영역으로 초기화한다.
-
- 스왑 파일을 활성화한다.
-
- 현재 활성화된 스왑 영역을 확인한다.
5. nginx 리버스 프록시 설정

Nginx는 클라이언트 요청을 먼저 받아 백엔드 애플리케이션으로 전달하는 리버스 프록시 역할을 한다.
Docker 환경에서 NestJS 애플리케이션이 3000번 포트로 실행되고 있다면, Nginx를 앞단에 두어 사용자는 80/443 포트로만 접근하게 만들 수 있다.
또한 HTTPS 적용 시 SSL 인증서를 Nginx에 설정하면, 클라이언트와 Nginx 사이의 통신은 HTTPS로 암호화되고, Nginx와 내부 백엔드 서버 간의 통신은 HTTP로 처리할 수 있다.
이를 통해 백엔드 서버의 포트를 직접 노출하지 않고, HTTPS 적용, 보안 관리, 로드 밸런싱, 요청 라우팅 등을 더 안정적으로 구성할 수 있다.
nginx 설치
일단 들어가기에 앞서, 본인은 가비아에서 도메인을 구매하였기에, 가비아 DNS 관리 페이지에서 A 레코드를 추가하여 하위 도메인 node.aivitaltrip가 EC2 인스턴스의 퍼블릭 IP를 가리키도록 설정했다.
이후, EC2 터미널에 접속하여 nginx를 설치했다.
sudo apt update
sudo apt install certbot -y
sudo certbot certonly --standalone -d node.aivitaltrip.com이후 이메일을 입력하고, 약관 동의에 'yes'를 입력하면 SSL 인증서가 발급된다.
docker-compose.yml 수정
docker-compose.yml 파일에서 nginx 서비스 설정을 추가했다.
app의 ports 설정은 제거하고, nginx 서비스는 아래와 같이 80/443 포트를 열어서 외부와 통신하도록 설정했다.
nginx:
image: nginx:latest
restart: always
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- app이후 아래 명령어를 통해 Nginx가 정상적으로 작동하는지 확인할 수 있다.
docker compose psapp, db, nginx가 모두 "Up" 상태일 경우 잘된 것이다.
마무리
이번 글에서는 NestJS 애플리케이션을 Docker로 컨테이너화하고 AWS EC2에 배포하는 전 과정을 정리했다.
각 단계에서 단순히 명령어를 실행하는 것을 넘어, 배포 흐름 속에서 등장하는 개념들이 실제로 어떤 역할을 하는지 이해하는 데 집중했다.
핵심 개념을 정리하면 이렇다.
- EC2 인스턴스: 내가 직접 제어하는 가상 서버. 운영체제, 성능, 저장소를 선택해 원하는 실행 환경을 구성한다.
- VPC와 서브넷: AWS 안에서 인스턴스가 속할 네트워크 공간을 정의한다. 인스턴스는 인터넷에 직접 노출되는 것이 아니라 먼저 이 네트워크 경계 안에 배치된다.
- 보안 그룹: 어떤 포트를 누구에게 허용할지 정하는 네트워크 차원의 방화벽이다. 22번은 SSH 접근, 80/443번은 웹 요청, 3000번은 애플리케이션 서버로 연결된다.
- 키 페어: 비밀번호 없이 서버에 접속할 수 있게 해주는 공개키 기반 인증 수단이다. 보안 그룹이 네트워크 차원의 출입문이라면, 키 페어는 그 문을 통과하기 위한 신원 확인 열쇠다.
- Docker: 애플리케이션 실행에 필요한 환경을 이미지로 묶어준다. 로컬에서 작동하던 환경을 EC2 서버 위에 그대로 재현할 수 있다는 게 핵심이다.
- Nginx 리버스 프록시: 외부 요청을 받아 내부 애플리케이션으로 전달하는 역할을 한다. 백엔드 포트를 직접 노출하지 않으면서도 SSL 적용, 요청 라우팅, 보안 강화를 한 곳에서 처리할 수 있다.
프론트엔드 개발자로서 이번 배포 경험은 단순한 기술 스택 추가가 아니었다.
API 요청이 어떤 경로로 서버에 도달하고, 서버가 어떤 환경에서 실행되며, 포트와 보안 그룹이 그 흐름을 어떻게 제어하는지를 직접 설정하면서 처음으로 서비스 전체를 하나의 흐름으로 바라보게 되었다.
다음 단계로는 GitHub Actions를 연결해 코드 변경이 자동으로 EC2에 배포되는 CI/CD 파이프라인을 구성해볼 계획이다.