
✨Dockerfile
docker에서 이미지를 생성하기 위한 용도로 작성하는 파일로, 만들 이미지에 대한 정보를 기술해 둔 템플릿
프로젝트의 루트 디렉토리에 다음과 같이 Dockerfile을 만든다. 이때 멀티 스테이지 빌드(다단계 빌드) 방식을 사용하여 빌드 환경과 실행 환경을 분리함으로써 이미지 크기를 최적화한다.
# STEP 1: 빌드 환경 설정 및 프로젝트 빌드
FROM node:20-alpine AS build-stage
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
# STEP 2: 프로덕션 환경을 위한 Nginx 설정
FROM nginx:1.23-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
현재 프로젝트에서 빌드 도구로 vite를 사용하고 있기 때문에 기본적으로 빌드 결과물이 /dist 디렉터리에 생성된다. 따라서 /app/dist 폴더를 nginx가 정적 파일을 찾는 경로로 복사해준다.
⚠️ CRA는 빌드 결과물이 /build 디렉터리에 생성되므로 혼동하지 말 것!
Reat 앱은 SPA(Single Page Application)이기 때문에 Nginx 설정을 통해 모든 경로의 요청을 index.html로 리디렉션해야 한다. 이 설정 또한 루트 디렉토리의 nginx.conf 파일에 해준다.
server {
listen 3000;
# Nginx가 정적 파일을 찾을 기본 경로를 명시적으로 설정
root /usr/share/nginx/html;
# 모든 요청을 처리하는 location 블록
location / {
# 요청된 URI($uri)가 존재하지 않으면, /index.html을 반환하여 SPA 라우팅을 지원
try_files $uri $uri/ /index.html;
}
# 정적 파일에 대한 캐시 설정 (선택 사항)
# 캐시 기간을 1년으로 설정하여 성능 최적화
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 1y;
access_log off;
add_header Cache-Control "public";
}
# gzip 압축 설정 (파일 전송 용량 최적화)
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
✨ 처음에는 nginx 설정을 아래와 같이 해서 배포된 앱에 접속했을 때 사진과 같이 흰 화면만 뜨는 이슈가 있었는데, 이는 웹서버의 root 경로 설정을 location 블록 밖에 해줌으로써 해결할 수 있었다.
⇒ 이렇게 하면 server 블록 내의 모든 location 블록이 동일한 root 경로를 상속받게 되어 Nginx가 모든 파일을 위에서 지정한 build 결과물이 위치한 /usr/share/nginx/html에서 찾을 수 있게 된다.
server {
listen 3000;
# SPA(Single Page Application) 라우팅을 위한 설정
# 모든 요청을 index.html로 리디렉션
location / {
root /usr/share/nginx/html; # root 경로 설정 위치에 유의
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 중략 ...
}

프로젝트의 루트 디렉토리에서 Dockerfile과 nginx.conf 파일을 생성 후 docker build 명령을 통해 이미지를 생성한다. 이때 태그를 지정하지 않으면 기본적으로 latest 태그가 붙는다.
# docker build [옵션] 이미지이름[:태그] Dockerfile의_디렉토리경로
docker build -t alcha-frontend .
ECR 레포지토리를 만들고 콘솔에서 aws 계정으로 로그인하여 레포지토리에 이미지를 푸시한다.


Docker CLI로 docker 레지스트리에 이미지를 푸시하기 위해서는 docker CLI가 레지스트리(여기서는 ECR)에 대한 접근 권한을 가지고 있어야 한다. 따라서 aws ecr get-login-password 명령을 통해 ECR에 접속할 수 있는 임시 비밀번호(토큰)을 생성하고 이를 docker login 명령어에 넘겨줘서 원하는 레지스트리(<your-aws-account-id>.dkr.ecr.ap-northeast-2.amazonaws.com)에 로그인한다.
# AWS CLI 인증 및 docker 레지스트리 로그인
aws ecr get-login-password --region <your-region> | docker login --username AWS --password-stdin <your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com


레지스트리에 이미지를 등록할 때는 레지스트리/레포지토리/이미지이름:태그 형태가 되어야 하므로 이미지의 태그를 수정하고 push해야 한다.
(이미지 레지스트리로 Docker Hub를 지정하는 경우에는 레지스트리 부분을 생략하고 레포지토리는 자신이 등록한 name으로 작성)
# ECR에 이미지를 푸시하기 위해 이미지 태그 수정
# docker tag 원본이미지이름[:버전] 참조이미지이름[:버전]
docker tag <your-image-name>:latest <your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com/<your-repository-name>:latest
# 이미지 이름에 지정된 push
docker push <your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com/<your-repository-name>:latest

docker pull <aws-account-id>.dkr.ecr.ap-northeast-2.amazonaws.com/alcha/frontend:v1.0.5
docker run -p 5179:3000 -d <your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com/<your-repository-name>:latest
이때 ec2의 인바운드 설정으로 포트를 열어주어야 접속이 가능하다.

EC2의 공인IP 주소로 접속 성공!

Dockerfile의 EXPOSE와 docker run -p 명령에서 지정하는 포트의 차이
- EXPOSE
EXPOSE는 컨테이너의 내부 포트 정보를 문서화하는 역할로, EXPOSE 80은 "이 컨테이너가 80번 포트를 사용해서 서비스를 제공할 거야"라고 다른 개발자나 시스템에게 알려주는 것이다. 하지만 이 명령어 자체만으로는 실제로 포트를 개방하거나 네트워크 연결을 만들지 않는다.- docker run -p
docker run -p <호스트 포트>:<컨테이너 포트>명령은 호스트(EC2)와 컨테이너 사이의 포트를 매핑하여 실제 통신 경로를 만든다.
⇒ 따라서 Dockerfile에서 지정한 80번 포트는 무시되고 3000번 포트만 노출되어 컨테이너는 3000번 포트에서 실행된다. 이 포트는 Nginx 설정 파일에 지정한 listen 포트(3000)와 일치해야 3000번 포트를 listen하고 있는 웹서버가 5173으로 노출되어 외부에서 접근이 가능해진다.
이 과정을 진행하면서 이미지 빌드 및 배포를 적어도 10번은 했는데 너무 번거로웠다.🤕 CI/CD 구축의 필요성을 제대로 느껴서 'CI/CD 구축기'로 돌아오겠다.