프론트엔드 개발자를 위한 Docker로 React 개발 및 배포하기
Docker + React Tutorial: From Development to Production Workflow
원제: Docker + ReactJS tutorial: Development to Production workflow + multi-stage builds + docker compose
이 포스트는 Youtube의 Sanjeev Thiyagarajan라는 분이 올려주신 Docker + ReactJS tutorial 영상을 쉽게 따라하고 이해할 수 있도록 정리한 내용이다.
prerequisite
- 원본 영상은 Windows, 본 설명은 Mac OS 환경에서 진행된다.
- NodeJS
- Docker
- CRA 등을 통해 초기화된 리액트 앱 (또는 본인의 프로젝트)
- Docker, Container 개념에 대한 기본 지식
1) Setting Docker Container
1-1) hub.docker.com
hub.docker.com은 docker에서 관리하는 컨테이너 이미지 레지스트리이다. node, nginx 등 기본 이미지를 쉽게 받을 수 있는 저장소 개념이다.
리액트 앱을 위한 컨테이너를 만들기 때문에 nodeJS를 컨테이너의 기본 이미지로 한다.
- node 이미지를 다운로드 받아 이미지를 커스터마이징 하는 것
- 특정 개발 환경을 제공하는 이미지들을 pre-built container라고 한다.
- 프로젝트 root 디렉토리에
Dockerfile
을 만든다. - Dockerfile은 이미지를 빌드하기 위한 모든 단계를 레이어별로 명시한다.
Dockerfile
# 가져올 이미지를 정의
FROM node:14
# 경로 설정하기
WORKDIR /app
# package.json 워킹 디렉토리에 복사 (.은 설정한 워킹 디렉토리를 뜻함)
COPY package.json .
# 명령어 실행 (의존성 설치)
RUN npm install
# 현재 디렉토리의 모든 파일을 도커 컨테이너의 워킹 디렉토리에 복사한다.
COPY . .
# 각각의 명령어들은 한줄 한줄씩 캐싱되어 실행된다.
# package.json의 내용은 자주 바뀌진 않을 거지만
# 소스 코드는 자주 바뀌는데
# npm install과 COPY . . 를 동시에 수행하면
# 소스 코드가 조금 달라질때도 항상 npm install을 수행해서 리소스가 낭비된다.
# 3000번 포트 노출
EXPOSE 3000
# npm start 스크립트 실행
CMD ["npm", "start"]
# 그리고 Dockerfile로 docker 이미지를 빌드해야한다.
# $ docker build .
2) Build Docker Image
리액트 앱 root 디렉토리에서 IDE 터미널 커맨드라인에서 아래 명령어를 실행한다.
$ docker build .
build 뒤의 구두점은 현재 디렉토리에 이미지를 만들겠다는 이야기다. 각 단계들은 한 번 실행되면 캐싱되기 때문에 두번째 빌드부터는 더 빠르게 실행된다.
다음 명령어로 도커 이미지를 찾아보자.
$ docker image ls
이미지에 이름을 정해주지 않아 <none>
으로 나온다. docker 이미지를 지우고 다시 만들어본다.
$ docker image rm <docker-image-id>
rm 다음 도커 이미지의 이름 또는 아이디를 입력하여 이미지를 삭제한다.
$ docker build -t react-image .
-t 옵션으로 이름(태그)를 만들어 다시 빌드한다.
결과가 캐싱되어 있어 훨씬 빠르게 이미지가 빌드되는 것을 확인할 수 있다.
3) Create Docker Container
만들어진 이미지로 도커 컨테이너를 띄우려면 아래의 명령어를 실행한다.
$ docker run -d --name <container-name> <image-name>
만들어질 컨테이너 이름은 react-app, 이미지 이름은 react-image이므로 아래와 같이 실행하게 된다.
$ docker run -d --name react-app react-image
3-1) -d
옵션 (detached)
d, --detach Run container in background and print container ID
- 컨테이너를 백그라운드(detached 모드)에서 실행하고, 실행 결과로 컨테이너 ID를 출력하는 옵션
- 이 옵션 없이 실행하면 터미널에 컨테이너 로그가 출력되고, 종료하기 위해
Ctrl + C
를 입력해야 한다.
3-2) docker ps
docker
컨테이너의 리스트를 보여주는 명령어로 현재 가동중인 컨테이너만 보인다. 만약 모두 보고 싶다면-a
옵션을 붙인다.
4) Run Container and Port Fowarding
$ docker run ~
로 컨테이너를 실행했는데 리액트 로컬 서버인 3000번 포트에 접속이 되지 않는다 왜일까?
- 컨테이너는 호스트 환경과 격리된 파일 시스템과 네트워크를 가지기 때문
- 호스트에서 컨테이너로 접근 가능하도록 포트 포워딩을 시켜줘야 한다.
4-1) docker rm <container-name> -f
- 위 명령어는 컨테이너를 제거할 것이다.
- 보통 컨테이너를 삭제하기 전에 실행을 중단시킨 후 삭제한다.
- 중단은
$ docker stop <container-name>
을 사용한다.
4-2) 포트 포워딩
이미지로 컨테이너를 띄울때 커맨드 라인에 -p hostPort:ContainerPort
옵션으로 포트 포워딩을 할 것을 명시한다.
$ docker run -d -p 3306:3000 --name <container-name> <image-name>
결과를 구분하기 위해 호스트 포트를 3306으로 지정했다.
위 명령에서 -p
의 콜론을 사이에 둔 숫자에 주목해야한다. -p 3306:3000
의 의미는 로컬 머신(127.0.0.1)의 3000번 포트로 접근하는 모든 트래픽을 도커 컨테이너의 3000번 포트로 보낸다는 뜻이다.
브라우저에서 로컬호스트 3306 포트로 접속하면 컨테이너 환경에서 실행된 리액트 로컬 서버가 앱을 잘 서빙해주는 것을 확인할 수 있다.
4-3) docker exec
- docker 컨테이너 환경에서 커맨드라인(bash) 띄우기
$ docker exec -it <container-name> bash
ls
명령어로 확인해보니 컨테이너의 리액트 앱 루트 폴더가 잘 확인된다.
- 여기서
docker exec
은 명령어를 실행하는 것
$ docker exec [option] <container-command>
exec
에-it
옵션을 쓰는 이유-i
: 표준 입출력 STDIN을 열겠다는 의미-t
: 가상 TTY(Pseudo TTY)를 통해 접속하겠다는 의미
4-4) docker ignore
도커 컨테이너에 포함시키지 않을 파일들을 명시한다. .gitignore
와 개념이 같다.
# .dockerignore
node_modules
Dockerfile
.git
.gitignore
.dockerignore
.env
.dockerignore
에 명시된 파일은 컨테이너에 포함되지 않은 것을 볼 수 있다.
5) Volume and Bind Mount
참고) Docker 컨테이너에 데이터 저장 (볼륨/바인드 마운트)
5-1) 코드 변경 후 어떻게 컨테이너의 프로젝트를 업데이트할까?
- 컨테이너 종료 > 다시 이미지 빌드 > 컨테이너 다시 띄우기는 너무 귀찮다.
- 코드 수정이 즉각적으로 반영되어야 개발이 편하다. Volume과 Bind Mount 개념을 알면 가능하다.
5-2) Volume / Bind Mount
Docker 컨테이너의 라이프 사이클과 상관 없이 도커 단에서 데이터를 저장하는 방법
- Volume
일반적인 상황에서 권장
- Bind Mount
로컬 개발 환경에서 권장
우리는 로컬 개발 환경에서 실시간으로 수정 사항이 화면에 반영이 되어야 하기 때문에 호스트 환경의 프로젝트 폴더를 바인드 마운트 방식으로 컨테이너에 연결시킬 것이다.
-v dirlocaldirectory:containerdirectory
pwd
로 마운트시킬 호스트 파일 시스템의 프로젝트 경로를 바인딩한다.
$ docker run -v $(pwd)/src:/app/src -d -p 8080:3000 --name react-app react-image
위 명령을 풀어쓰면 아래와 같다.
docker run
도커 컨테이너를 띄울(실행) 것이다.-v $(pwd)/src:/app/src
$(현재 경로)/src 폴더가 컨테이너의 /app/src 경로로 동기화 되도록 바인드 마운트 할 것이다.-d
컨테이너 프로세스를 백그라운드에서 실행시킬 것이다. (detached)-p 8080:3000
호스트 환경에서 8080으로 접속되는 트래픽을 컨테이너의 3000번 포트로 포워딩 할 것이다.—name react-app
컨테이너의 이름은 react-app으로 명한다.react-image
를 docker 이미지로 사용할 것이다.
5-3) Read-only Bind Mount
의도치 않게 컨테이너 환경에서 소스 코드를 수정할 수도 있다. 이 경우 도커 컨테이너에서 호스트를 수정하지 못하도록 읽기 전용 모드를 사용하면 양방향 Sync에서 호스트 ⇒ 컨테이너로 동기화된다.
바인드 마운트 명령어에 :ro
(read-only)만 붙여주면 된다.
$ docker run -v $(pwd)/src:/app/src:ro -d -p 8080:3000 —name react-app react-image
위와 같이 컨테이너 환경 CLI로 파일 시스템 수정이 불가하다.
6) Environment Variables
도커 환경에서 환경 변수를 어떻게 설정할 수 있을까?
6-1) Dockerfile에 하드코딩
컨테이너 환경에서 리액트 서버를 띄우기 전 라인에서 직접 환경 변수를 선언할 수 있다.
6-2) 커맨드 라인에서 하드코딩
컨테이너를 띄우는 시점(run)에 커맨드 라인에서 환경 변수를 세팅할 수도 있다.
-e ENV_VARIABLE_NAME=value
커맨드 라인에서 설정된 환경 변수는 Dockerfile
과 .env
에 선언된 같은 이름의 환경 변수를 덮어쓰기 때문에 주의해야 한다.
6-3) .env 파일로 설정
REACT_APP_NAME=Wonkook Lee
REACT_APP_TITLE=Kiwi
환경 변수 파일을 .env 파일로 관리하고 CLI에서 환경 변수 파일을 지정해주면 사용 가능하다.
--env-file <ENV_FILE_DIRECTORY>
--env-file ./.env
6-4) 우선 적용 순위
CLI
> .env
> Dockerfile
순위가 높은 쪽이 순위가 낮은 쪽을 덮어 씌운다.
7) Docker Compose
Docker-compose란 여러 개의 컨테이너로부터 이루어진 서비스를 구축, 실행하는 순서를 자동으로 하여, 관리를 간단히 하는 기능이다.
여러개의 컨테이너를 관리하면서 수 많은 명령어를 하나 하나 실행시키는 것이 여간 귀찮은 일이 아니다.
루트 디렉토리에 docker-compose.yml
파일을 만들고 명령어를 정리한다.
# docker 컨테이너 버전을 명시
version: "3"
# services는 컨테이너
services:
react-app:
# -it 옵션을 위해 사용됨 (표준입출력)
stdin_open: true
tty: true
# 현재 경로에 이미지 빌드
build: .
# 포트 포워딩
ports:
- "8080:3000"
# 호스트 디렉토리에 바인드 마운트
volumes:
- ./src:/app/src:ro
# 환경 변수 설정 - opt.1(하드코딩)
environment:
- REACT_APP_NAME=wonkook
- REACT_APP_TITLE=kiwi
# 환경 변수 설정 - opt.2(.env)
env_file:
- ./.env
docker-compose.yml
을 작성하고 서비스 컨테이너를 생성하고 실행하기 위해 docker-compose up
명령어를 사용한다.
7-1) docker-compose up -d
docker-compose.yaml
에 명시된 모든 서비스 컨테이너를 생성하고 실행시켜주는 명령어
7-2) docker-compose down
모든 서비스 컨테이너를 한 번에 정지시키고 삭제한다.
7-3) docker-compose up —build -d
이미지를 다시 빌드해서 컨테이너를 띄워야 하는데 docker-compose
는 멍청해서 이미지 이름만 같아도 새롭게 빌드하지 않는다. 기존 이미지와 컨테이너를 stale 시키고 다시 빌드하기 위해 —build
옵션을 사용한다.
8) Multi-Stage Build for Production with NGINX
8-1) Development Environment
개발 환경에서는 CRA가 리액트 Dev 서버를 로컬 머신 3000번 포트에 띄워 앱을 서빙한다. 하지만 리액트 개발 서버는 개발용이기 때문에 실제 웹서버로써 활용할 수 없다.
8-2) Production Environment
프로덕션 환경에서는 빌드 후 생성된 정적 리소스를 업로드하게 된다.
8-3) NGINX: Production Grade Web-Server
정적 파일을 서빙하기 위해 프로덕션용 웹서버를 NGINX(엔진엑스)로 띄운다. 반드시 NGINX여야만 하는 것은 아니며 필요에 따라 아파치도 사용할 수 있다. 뭐가 됐든 정적 파일을 요청했을때 서빙해줄 서버가 필요하다.
8-4) NGINX를 사용한 프로덕션용 다단계 빌드
8-5) Use an external image as a “stage”
- 개발 환경을 위한 도커 이미지 빌드를 위해
Dockerfile.dev
와Dockerfile.prod
를 분리한다. - 빌드 패턴을 사용하기 위해 builder와 실제 실행될 이미지 두개의 Dockerfile이 필요하다.
Dockerfile.dev
FROM node:14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Dockerfile.prod
FROM node:14 AS build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx
COPY --from=build /app/build /usr/share/nginx/html
-
리액트 코드 Production 빌드
- node:14을 베이스 이미지로 설치하고, 이 빌드 스테이지를
build
로 명명한다. - 워킹 디렉토리 설정, package.json 복사 및 의존성 설치, 의존성 파일 컨테이너에 복사
- 컨테이너 환경에서
npm run build
로 프로젝트를 빌드한다. - 그러면 루트 패스에 build(또는 dist) 디렉토리가 생성되고 변환된 정적 파일들이 들어있다.
- node:14을 베이스 이미지로 설치하고, 이 빌드 스테이지를
-
NGINX 웹서버로 리액트 앱 서빙하기(Hosting simple static content)
- nginx를 베이스 이미지로 설치하는 프로덕션 빌드 스테이지를 만든다.
- 이때
COPY —from=<stage-name>
명령을 사용해서 이전 스테이지에서 산출한 빌드된 정적 바이너리를 NGINX의/usr/share/nginx/html
디렉토리로 복사한다. - 간단한 정적 컨텐츠는 아래와 같이 웹서버 컨테이너에 추가한다.
FROM nginx
COPY static-html-directory /usr/share/nginx/html
nginx - Official Image | Docker Hub
COPY —from
의 역할
The
COPY --from=0
line copies just the built artifact from the previous stage into this new stage.
COPY
instruction에서—from
옵션을 사용하면 이전 스테이지에서 Build를 통해 생성된 결과만을 복사할 수 있다.
AS <Alias-Name>
의 역할
- 특정 빌드 스테이지의 별칭(alias)을 정할 수 있다.
FROM
instruction 레이어에서 사용 가능하고,COPY
의—from=<name>
옵션으로 참조한다.
8-6) 멀티 스테이지 빌드의 장점
멀티 스테이지 빌드란 컨테이너 이미지를 만들면서 빌드 등에는 필요하지만, 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어 기반 이미지를 만드는 방법이다.
- 빌드에 사용한 파일 및 디렉토리 등 의존 파일을 삭제하고 컨테이너를 실행할 수 있어서 훨씬 가볍다.
- 앱을 실행하기 위해 최소한으로 필요한 실행 모듈만 배치하여 컴퓨팅 리소스 효율화 및 보안에 도움이 된다.
9) Development vs. Production Workflow
Dockerfile & docker-compose Dockerfile은 사용자가 이미지를 어셈블하기 위해 호출할 수 있는 명령이 포함된 간단한 텍스트 파일인 반면 Docker Compose는 다중 컨테이너 Docker 앱을 정의하고 실행하기 위한 도구다.
docker-compose 파일을 dev와 prod 두 버전으로 나눈다.
9-1) 공통 부분
- docker-compose.yml
- 공통적인 configuration을 설정한다.
version: '3'
services:
react-app:
build:
context: .
9-2) Development 환경
- docker-compose-dev.yml
version: '3'
services:
react-app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- '4000:3000'
volumes:
- ./src:/app/src:ro
env_file:
- ./.env.local
- Dockerfile.dev
FROM node:14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
- 이미지 빌드 및 컨테이너 마운트시 -f 옵션으로 configuration file을 지정하면 된다.
$ docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d --build
9-3) Production 환경
- docker-compose-prod.yml
version: '3'
services:
react-app:
build:
context: .
dockerfile: Dockerfile.prod
# npm run build시 환경 변수 참조가 안되어 args로 전달하고, Dockerfile에서 변수로 사용한다.
args:
- REACT_APP_NAME=Wonkook-prod
- REACT_APP_TITLE=Kiwi-prod
ports:
- "8080:80"
# 80: HTTP Port for NGINX Web Server
- Dockerfile.prod
FROM node:14 AS build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
# docker-compose-prod에 명시된 args(변수)를 이미지 빌드시 환경 변수로 사용한다.
ARG REACT_APP_NAME
ENV REACT_APP_NAME=${REACT_APP_NAME}
# 인수를 환경 변수에 할당하는 순서를 잘 지켜야 한다.
ARG REACT_APP_TITLE
ENV REACT_APP_TITLE=${REACT_APP_TITLE}
RUN npm run build
FROM nginx
COPY --from=build /app/build /usr/share/nginx/html
- 이미지 빌드 및 컨테이너 마운트
$ docker-compose -f docker-compose.yml -f docker-compose-prod.yml up -d --build
9-4) 정리
- 4000번 포트는 dev 환경의 리액트 앱이 실행되는 컨테이너 3000번 포트로 포워딩
- 8080번 포트는 prod 환경의 nginx가 실행되고 있는 컨테이너 80번 포트로 포워딩
9-5) Build Target
특정 빌드 스테이지만 실행시킬 수 있다. 예를 들어 필요한 경우 아래 build라는 별칭을 가진 리액트 앱 빌드 프로세스만 이미지로 빌드할 수 있다.
FROM node:14 AS build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
ARG REACT_APP_NAME
ENV REACT_APP_NAME=${REACT_APP_NAME}
ARG REACT_APP_TITLE
ENV REACT_APP_TITLE=${REACT_APP_TITLE}
RUN npm run build
# FROM nginx
# COPY --from=build /app/build /usr/share/nginx/html
build —target
뒤에 타겟 이름을 붙이고, Dockerfile
을 Dockerfile.prod
로 지정한다.
$ docker build --target build -f Dockerfile.prod -t multi-stage-example .