원제: Docker + ReactJS tutorial: Development to Production workflow + multi-stage builds + docker compose
이 포스트는 Youtube의 Sanjeev Thiyagarajan라는 분이 올려주신 Docker + ReactJS tutorial 영상을 쉽게 따라하고 이해할 수 있도록 정리한 내용이다.
hub.docker.com은 docker에서 관리하는 컨테이너 이미지 레지스트리이다. node, nginx 등 기본 이미지를 쉽게 받을 수 있는 저장소 개념이다.
리액트 앱을 위한 컨테이너를 만들기 때문에 nodeJS를 컨테이너의 기본 이미지로 한다.
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 .
리액트 앱 root 디렉토리에서 IDE 터미널 커맨드라인에서 아래 명령어를 실행한다.
$ docker build .
build 뒤의 구두점은 현재 디렉토리에 이미지를 만들겠다는 이야기다.
각 단계들은 한 번 실행되면 캐싱되기 때문에 두번째 빌드부터는 더 빠르게 실행된다.
다음 명령어로 도커 이미지를 찾아보자.
$ docker image ls
이미지에 이름을 정해주지 않아 <none>
으로 나온다. docker 이미지를 지우고 다시 만들어본다.
$ docker image rm <docker-image-id>
rm 다음 도커 이미지의 이름 또는 아이디를 입력하여 이미지를 삭제한다.
$ docker build -t react-image .
-t 옵션으로 이름(태그)를 만들어 다시 빌드한다.
결과가 캐싱되어 있어 훨씬 빠르게 이미지가 빌드되는 것을 확인할 수 있다.
만들어진 이미지로 도커 컨테이너를 띄우려면 아래의 명령어를 실행한다.
$ docker run -d --name <container-name> <image-name>
만들어질 컨테이너 이름은 react-app, 이미지 이름은 react-image이므로 아래와 같이 실행하게 된다.
$ docker run -d --name react-app react-image
-d
옵션 (detached)d, --detach Run container in background and print container ID
Ctrl + C
를 입력해야 한다.docker ps
docker
컨테이너의 리스트를 보여주는 명령어로 현재 가동중인 컨테이너만 보인다. 만약 모두 보고 싶다면 -a
옵션을 붙인다.$ docker run ~
로 컨테이너를 실행했는데 리액트 로컬 서버인 3000번 포트에 접속이 되지 않는다 왜일까?
docker rm <container-name> -f
$ docker stop <container-name>
을 사용한다.이미지로 컨테이너를 띄울때 커맨드라인에 -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 포트로 접속하면 컨테이너 환경에서 실행된 리액트 로컬 서버가 앱을 잘 서빙해주는 것을 확인할 수 있다.
docker exec
$ docker exec -it <container-name> bash
ls
명령어로 확인해보니 컨테이너의 리액트 앱 루트 폴더가 잘 확인된다.
docker exec
은 명령어를 실행하는 것$ docker exec [option] <container-command>
exec
에 -it
옵션을 쓰는 이유-i
: 표준 입출력 STDIN을 열겠다는 의미-t
: 가상 TTY(Pseudo TTY)를 통해 접속하겠다는 의미docker ignore
도커 컨테이너에 포함시키지 않을 파일들을 명시한다. .gitignore
와 개념이 같다.
# .dockerignore
node_modules
Dockerfile
.git
.gitignore
.dockerignore
.env
.dockerignore
에 명시된 파일은 컨테이너에 포함되지 않은 것을 볼 수 있다.
참고) Docker 컨테이너에 데이터 저장 (볼륨/바인드 마운트)
Docker 컨테이너의 라이프 사이클과 상관 없이 도커 단에서 데이터를 저장하는 방법
일반적인 상황에서 권장
로컬 개발 환경에서 권장
우리는 로컬 개발 환경에서 실시간으로 수정 사항이 화면에 반영이 되어야 하기 때문에 호스트 환경의 프로젝트 폴더를 바인드 마운트 방식으로 컨테이너에 연결시킬 것이다.
-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 이미지로 사용할 것이다.의도치 않게 컨테이너 환경에서 소스 코드를 수정할 수도 있다. 이 경우 도커 컨테이너에서 호스트를 수정하지 못하도록 읽기 전용 모드를 사용하면 양방향 Sync에서 호스트 ⇒ 컨테이너로 동기화된다.
바인드 마운트 명령어에 :ro
(read-only)만 붙여주면 된다.
$ docker run -v $(pwd)/src:/app/src:ro -d -p 8080:3000 —name react-app react-image
위와 같이 컨테이너 환경 CLI로 파일 시스템 수정이 불가하다.
도커 환경에서 환경 변수를 어떻게 설정할 수 있을까?
컨테이너 환경에서 리액트 서버를 띄우기 전 라인에서 직접 환경 변수를 선언할 수 있다.
컨테이너를 띄우는 시점(run)에 커맨드 라인에서 환경 변수를 세팅할 수도 있다.
-e ENV_VARIABLE_NAME=value
커맨드 라인에서 설정된 환경 변수는 Dockerfile
과 .env
에 선언된 같은 이름의 환경 변수를 덮어쓰기 때문에 주의해야 한다.
REACT_APP_NAME=Wonkook Lee
REACT_APP_TITLE=Kiwi
환경 변수 파일을 .env 파일로 관리하고 CLI에서 환경 변수 파일을 지정해주면 사용 가능하다.
--env-file <ENV_FILE_DIRECTORY>
--env-file ./.env
CLI
> .env
> Dockerfile
순위가 높은 쪽이 순위가 낮은 쪽을 덮어 씌운다.
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
명령어를 사용한다.
docker-compose up -d
docker-compose.yaml
에 명시된 모든 서비스 컨테이너를 생성하고 실행시켜주는 명령어
docker-compose down
모든 서비스 컨테이너를 한 번에 정지시키고 삭제한다.
docker-compose up —build -d
이미지를 다시 빌드해서 컨테이너를 띄워야 하는데 docker-compose
는 멍청해서 이미지 이름만 같아도 새롭게 빌드하지 않는다. 기존 이미지와 컨테이너를 stale 시키고 다시 빌드하기 위해 —build
옵션을 사용한다.
개발 환경에서는 CRA가 리액트 Dev 서버를 로컬 머신 3000번 포트에 띄워 앱을 서빙한다.
하지만 리액트 개발 서버는 개발용이기 때문에 실제 웹서버로써 활용할 수 없다.
프로덕션 환경에서는 빌드 후 생성된 정적 리소스를 업로드하게 된다.
정적 파일을 서빙하기 위해 프로덕션용 웹서버를 NGINX(엔진엑스)로 띄운다. 반드시 NGINX여야만 하는 것은 아니며 필요에 따라 아파치도 사용할 수 있다. 뭐가 됐든 정적 파일을 요청했을때 서빙해줄 서버가 필요하다.
Dockerfile.dev
와 Dockerfile.prod
를 분리한다.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
build
로 명명한다.npm run build
로 프로젝트를 빌드한다.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>
의 역할FROM
instruction 레이어에서 사용 가능하고, COPY
의 —from=<name>
옵션으로 참조한다.멀티 스테이지 빌드란 컨테이너 이미지를 만들면서 빌드 등에는 필요하지만, 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어 기반 이미지를 만드는 방법이다.
Dockerfile & docker-compose
Dockerfile은 사용자가 이미지를 어셈블하기 위해 호출할 수 있는 명령이 포함된 간단한 텍스트 파일인 반면 Docker Compose는 다중 컨테이너 Docker 앱을 정의하고 실행하기 위한 도구다.
docker-compose 파일을 dev와 prod 두 버전으로 나눈다.
version: '3'
services:
react-app:
build:
context: .
version: '3'
services:
react-app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- '4000:3000'
volumes:
- ./src:/app/src:ro
env_file:
- ./.env.local
FROM node:14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
$ docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d --build
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
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
특정 빌드 스테이지만 실행시킬 수 있다. 예를 들어 필요한 경우 아래 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 .
Wonkook Lee
Frontend Developer
LinkedIn
감사합니다. 내용 잘 정리해주셔서 유익하게 읽었었습니다