Overview

IT기술이 나날이 발전하고 있습니다. HTML5의 시대가 도래된 이후, 수많은 기술들이 나왔고 새로운 패러다임을 제시한 것 중 하나인 Docker로 로컬 개발환경 셋팅 하는 것을 소개하려고 합니다.

Contents

  1. Docker의 소개
  2. Docker의 기본적인 설치, 설정, 빌드, 실행 방법
  3. 실무에서 활용 하기와 주의사항
  4. 마무리

1. Docker의 소개
————————————————————————

10년을 웹개발을 하고 있는데, 나날이 새로운 점들이 많아지고 있습니다. 요즘은 개발, 상용 서버셋팅이 클릭 몇 번이면 커맨드 한줄로 로컬환경이 만들어집니다. 불과 5년 전만해도 일반적으로 웹개발을 시작하려면 IDC에서 서버를 구매 또는 임대 할 경우 OS까지 설치해줬고, 사내 인트라넷을 셋팅할 때는 서버를 구매하여 직접 OS를 설치 한 후 각종 패키지 설치를 위해 configure, make, install 지옥이 시작되었습니다. 그 지옥이 끝나면 was, 웹서버를 셋팅하고 실행 시킨 후 index.html 을 추가해서 Hello World! 를 확인 할 수 있었습니다. 짧게는 몇 시간, 길게는 며칠까지도 걸렸습니다.

그것도 개발 및 상용환경에 각각 설치 해야 되고, 기록을 하지 않고 막 작업할 경우에는 똑같은 지옥을 상용서버에서도 겪어야만 했습니다. 형상관리도 하지 않고 바로 FTP로 접속하여 작업하는 회사도 많았는데요. SVN에서 GIT로 전환하기 시작하는 회사도 늘어났습니다. JAVA로 웹 개발할 때 Tomcat 처럼 다른 언어로 로컬에서도 직접 구동하면서 개발하면, 로컬/개발/상용서버 모두 동일하게 구현되면 좋겠다는 상상을 했었습니다. 왜냐하면 그나마 자바는 JDK, Tomcat 버전만 맞춰주면 로컬이든 개발, 상용서버까지 올리면 거의 에러없이 바로 돌아갔기 때문입니다.

그러다 AWS, GCP, Azure 등 클라우드 서비스가 대세가 되고 서버리스 시대가 시작되면서 Kubernetes가 수면 위로 올라올 때 Docker가 소문 나기 시작했습니다. Docker는 간단하게 생각하면 옛날 가상 CD롬 드라이브와 같은 개념으로, 이미지로 구워두면 언제든지 동일한 환경으로 셋팅이 가능하다는 것이었습니다. 또한 Github 처럼 hub.docker.com 같이 이미지 관리를 위한 호스팅도 제공합니다. 원래는 공개 이미지의 경우 무제한 생성과 무기한 영구 저장이었으나, 2020년 11월 1일 부터 6개월 동안 해당 이미지를 Pull / Push가 한번도 일어나지 않은 이미지는 자동삭제 되도록 정책이 변경 되었습니다.

초반에는 직접 빌드하여 올려두고 사용하였으나 현재는 각종 공식 업체에서 기본 이미지를 생성하여 제공하고 있고, Dockerfile에 alpine버전으로 가져와서 필요한 것만 빌드하여 저용량으로 이미지를 수월하게 만들 수 있게 되었습니다. 이렇게 생성된 이미지를 가지고 실행하면 컨테이너가 생성되고 해당 공간은 내부에 전혀 영향 없는 독립적인 별개의 공간으로 활용 할 수 있습니다. docker-compose를 이용하면 더 쉽게 여러 개의 컨테이너를 실행 할 수 있습니다.

2. Docker의 기본적인 설치, 설정, 빌드, 실행 방법
————————————————————————

1) 설치

Window의 경우에는 상관없지만 Mac의 경우 brew로 설치 할 수 있습니다. 다만 docker, docker-compose 등 여러 패키지를 따로 설치해야 되는 번거로움이 있습니다. 그래서 공식 홈페이지에서 dmg 파일을 다운받아 설치하시되, Stable버전의 경우 CPU 사용량이 100%를 초과 해서 750%까지 올라가는 버그가 존재합니다. 모든 Mac이 해당 증상을 겪는 건 아니지만, 저도 이 증상이 나타나서 Edge버전을 설치하여 사용중입니다. Edge버전은 [ 여기 ] 에서 다운로드 가능합니다.

설치 후 아래 경로에서 파일 또는 폴더를 공유 해주세요.

docker


2) 빌드

해당 작업에 앞서 Dockerfile에 대한 정보를 한 번 읽고 시작하는 게 좋습니다. 공식 홈페이지는 영어로 되어 있어서, 한글이 필요하신 분은 [ 여기 ] 를 클릭 하시면 한글로 조금 더 쉽게 접근 하실 수 있습니다.

간단하게 Flask를 빌드해서 띄워보겠습니다. vi를 이용하여 확장자 없이 Dockerfile 을 생성합니다. Window10의 경우 WSL2를 설치하여 Ubuntu 터미널에 접속하셔서 vi로 생성하시면 편합니다. 저희 팀은 주언어가 Python 3.7이므로 [ 파이썬 허브 ] 에 올라온 이미지 버전을 확인해봅니다.

아래 스크린샷처럼 3.7 버전에 해당하는 태그가 존재하네요.

docker

이미지를 가져오는 명령어는 다음과 같습니다

docker pull 이미지명:태그명
태그명이 없다면 기본값은 latest 입니다.

docker pull python:3.7-alpine

빌드 Dockerfile 은 아래와 같습니다.
Dockerfile안에도 주석을 추가할 수 있습니다. 주석은 # 으로 시작하면 됩니다.

Dockerfile은 아래와 같습니다.

# 가져올 이미지
FROM python:3.7-alpine

# bash를 사용하기 위해 설치
RUN apk update && \
        apk add --no-cache \
        bash

# python 기본 패키지
RUN apk add --update build-base python3-dev py-pip

# 환경변수
ENV LIBRARY_PATH=/lib:/usr/lib

# 호스트와 연결할 포트 ( 이렇게 빌드하는 이유는 추후 jwilder/nginx-proxy 를 위해서 입니다 )
EXPOSE 3000

# 기본 디렉토리
WORKDIR /var/www/html
COPY . /var/www/html/

# flask 설치
RUN pip install flask

# 실행 명령어
CMD ["python", "app.py"]

app.py은 아래와 같습니다.

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, world!'

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=3000)

위에서 alpine버전으로 가져와 저용량 이미지를 생성 할 수 있다고 하였는데 그 차이는 아래와 같습니다.

docker

기본 이미지 용량이 무려 약 21.4배 차이가 납니다. 물론 alpine 버전 안에 필요한 패키지를 추가하여 설치하면 용량이 높아집니다. 하지만 시작점은 엄청난 차이가 나죠. 저희 팀원 모두 Docker를 사용하지는 않지만 사용중인 이미지의 경우 적게는 개당 124MB에서, 많게는 414MB 사용합니다. 필요한 걸 이미지에 다 빌드해도 876MB의 반 밖에 안됩니다.

Dockerfile 을 빌드해 봅시다. 빌드 명령어는 아래와 같습니다. -t 옵션은 태그 추가 명령어입니다.

만약,
1) Docker Hub 에 올리시려면 이미지명 앞에 계정명을 쓰시면 됩니다.
2) AWS, GCP, Azure에도 이미지 저장 서비스를 제공합니다.
3) 시작경로 안에 특정 파일 또는 폴더를 제외하고 싶으면, 타겟경로 안에 .dockerignore 파일을 생성해서 제외 하실 것을 추가 해주세요. 규칙은 .gitignore 와 유사합니다.

docker build -t 빌드될_이미지명:태그명 타겟경로

Docker hub 에 올릴 시

docker build -t 계정명/빌드될 이미지명:태그명 시작경로

docker build -t my_python_37:v1 .

아래와 같은 화면이 이미지가 생성 되었습니다.

docker


3) 실행

Dockerfile에서 빌드 시 CMD 를 지정했기 때문에 컨테이너 실행 시, 바로 app.py가 실행됩니다. run 명령어는 아래와 같습니다. -p는 해당 컨테이너와 로컬간에 연결할 포트를 직접 지정하는 옵션입니다.

docker run -p 로컬포트:컨테이너포트 이미지명:태그명

docker run -p 3000:3000 my_python_37:v1

docker run -p 3000:3000 my_python_37:v1

그럼 아래와 같은 스크린샷과 함께 localhost:3000 으로 접속 시 Hello World! 가 뜨게됩니다

docker
docker

여기서 하나 생각하게 되는 게

“그러면 app.py가 바뀔때 마다 빌드를 다시 해줘야 되나요?”

그러실 필요 없습니다. run 명령어 옵션 중, -v 옵션을 주면 컨테이너를 실행시킬 때 변경 즉시 반영됩니다.

아래의 ${PWD} 는 절대경로를 말합니다

docker run -p 80:3000 -v ${PWD}/app.py:/var/www/html/app.py my_python_37:v1

물론 파일이 아닌 폴더를 통으로 할 수 있습니다.

위와 같이 실행 시킨 후 app.py 파일을 수정 하고 새로고침 하면 즉시 반영되는 화면을 보실 수 있습니다.

4) docker-compose 사용하여 같은 포트 및 여러 컨테이너 사용하기

위 명령어 docker run을 사용하여 하나씩 띄울 수도 있고, docker-compose up을 사용하여 동시에 작업 할 수도 있습니다. 요즘은 프론트, 백엔드를 같이 띄워놓고 작업하고 추가로 또 여러 개를 띄워야 할 때가 있습니다. 같은 포트로 개발을 해야 하는 난감함이 생길 수 있습니다. 위에서처럼 이미 3,000번을 사용 했을 경우, 추가로 3,000번을 띄워야 할 때 더 수월하게 할 수 있습니다.

직접 nginx를 띄우거나 컨테이너를 실행시켜 reverse-proxy를 연결 할 수 있지만, 잘 만들어진 이미지들이 많고 그 중에 많이 알려진 jwilder/nginx-proxy 이미지를 사용하여 시간을 아껴주시는 게 좋습니다.

your_app.py은 아래와 같습니다.

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'your python :: Hello, world!'

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=3000)

docker-compose.yml 은 아래와 같습니다.

version: '3'

services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy
    restart: always
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      #- ${PWD}/etc/conf/location.conf:/etc/nginx/vhost.d/default_location

  my-python-api:
    image: my_python_37:v1
    container_name: my_python_api
    environment:
      - VIRTUAL_HOST=my.python
      - VIRTUAL_PORT=3000
    volumes:
      - ./app.py:/var/www/html/app.py

  your-python-api:
    image: my_python_37:v1
    container_name: your_python_api
    environment:
      - VIRTUAL_HOST=your.python
      - VIRTUAL_PORT=3000
    volumes:
      - ./your_app.py:/var/www/html/app.py

environment에 있는 부분 중 VIRTUAL_HOSThosts 파일 안에 설정된 호스트이고, VIRTUAL_PORT가 컨테이너 안에 연결할 포트입니다. IPv6, ssl(와일드카드 포함), nginx etc 관련 작업하여 특정 URI 리다이렉트 같은 부분도 모두 가능합니다. 자세한 내용은 공식 [ 도커허브 ] 에서 확인 해주세요.

실행은 아래와 같습니다.
특정 경로에 있는 yml 파일을 실행 하시려면 -f 옵션을 주시면 됩니다.

# 현재 경로의 docker-compose.yml 파일 실행 시
docker-compose up

# 특정 경로의 yml 파일을 실행 시킬 때
docker-compose -f yml경로 up

그러면 아래와 같이 같은 포트를 사용하여 여러 개의 컨테이너를 띄울 수 있습니다.

docker
docker


3. 실무에서 활용 하기
————————————————————————

2번 과정을 응용하여 실무에서 사용 할 개발환경을 구축해 봅시다. 이미지를 미리 만들어 놓고 쓰면 좋겠지만 따로 저장소를 쓰지 않는 이상 그러기가 쉽지 않습니다. 공개 저장소를 사용할 경우 인증서 같이 민감한 부분이 잘못해서 딸려 올라가거나 하는 불상사가 생길 수 있기 때문에, 공개 저장소에 올리되 최대한 심플하게 이미지를 만들고 docker-compose 할 때 command 라인에서 처리하도록 해보겠습니다.

저희 팀의 로컬 개발환경은 아래와 같습니다.

[ 프론트 ]

  1. Node 12.x
  2. Vue 2.6.11
  3. Babel 7
  4. Webpack 4

[ 백엔드 ]

  1. Python 3.7
  2. 브랜디 프레임워크 25
  3. Flask ( 로컬은 AWS SAM 을 사용하지 않고 Flask 로 띄웁니다 )

# 프론트 Docker 설정 파일 #

Dockerfile 은 아래와 같습니다.

# 가져올 이미지
FROM node:12-alpine

# bash를 사용하기 위한 설정
RUN apk add --no-cache \
        bash

# 연결할 포트
EXPOSE 80

# 기본 디렉토리 설정
WORKDIR /var/www/html

.dockerignore 은 아래와 같습니다.

# 포함되지 말아야 할 파일 및 폴더

# IntelliJ 설정 파일
**/.idea/
# git 폴더
**/.git/
# Node 모듈
**/node_modules/


# 백엔드 Docker 설정 파일 #

Dockerfile 은 아래와 같습니다.

# 사용할 이미지
FROM python:3.7-alpine

# bash를 위한 설정
RUN apk add --no-cache \
        bash

# 글로벌 패스 해지
RUN unset -v PYTHONPATH

# 연결할 포트
EXPOSE 3000

# 기본 디렉토리 셋팅
WORKDIR /var/www/html

.dockerignore 은 아래와 같습니다.

# 포함되지 말아야 할 파일 및 폴더

# IntelliJ 설정 파일
**/.idea/
# git 폴더
**/.git/
# Python경로들
**/opt/
**/requirements/
**/venv/

docker-compse.yml

version: '3'

services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy
    restart: always
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro

  my_front:
    image: my_front:v1
    build:
      context: ./my_front
      dockerfile: Dockerfile
    container_name: my_front_v1
    environment:
      - VIRTUAL_HOST=my.front
    volumes:
      - ./my_front:/var/www/html
    command:
      - bash
      - -c
      - |
        rm -rf ./node_modules &&
        npm ci &&
        npm rebuild node-sass &&
        npm run serve

  my-backend:
    image: my_backend:v1
    build:
      context: ./my_backend
      dockerfile: Dockerfile
    container_name: my_backend_v1
    environment:
      - VIRTUAL_HOST=my.backend
      - VIRTUAL_PORT=3000
    volumes:
      - ./my_backend/aws_credentials_seoul:/root/.aws/credentials
      - ./my_backend:/var/www/html
    command:
      - bash
      - -c
      - |
        python -m venv ./venv &&
        source ./venv/bin/activate &&
        pip install --upgrade pip &&
        pip install boto3 Flask==1.1.1 Flask-Cors==3.0.8 ruamel.yaml==0.16.10 &&
        ./pip.sh &&
        python -B flask_server.py -h 0.0.0.0 -p 3000

docker-compose up만 하시면 실행 됩니다.

  • 단 OS가 Mac일 경우 컨테이너 안에서 Python이나 Vue 실행 시, 호스트 옵션을 0.0.0.0 으로 해야 외부와 연결이 됩니다. 또한 Window와는 다르게 /~/ 가 가상 경로로 되어 있어서 실제 경로와 다르게 나옵니다. 설정을 바꿀 수는 있지만 한 번 하고 잊어버릴 수 있기 때문에, 간단하게 workspace 안 docker-compose.yml을 넣고 상대경로로 바꾼 뒤 실행 시키시면 됩니다.

첫 실행 시에만 위처럼 하고, 한 번 실행 후 AWS Lambda Layer, requirements.txt 나 package.json 이 바뀌지 않는 이상 설치 관련된 부분은 # 으로 주석처리 하시면 빠르게 실행 됩니다. Vue 나 Python 이 바뀌면 즉시 리빌드해서 표현되는 건 동일합니다.

npm install을 하지 않고 npm ci를 사용한 이유는, install 시 package-lock.json 내용이 수정 되면서 의존성이 깨지게 되는데 다른 팀원이 안 될 수도 있는 상황을 방지 할 수 있습니다.

실행 결과

docker

이제 맡은 프로젝트를 진행 하시면 됩니다!

4. 마무리
————————————————————————

Docker를 사용하여 이미지 하나만 잘 만들어 놓으면 어디서든 동일한 환경으로 개발 할 수 있는 시대가 되었습니다. 굳이 회사 노트북을 가지고 다니지 않고 집 PC 또는 노트북에 Docker를 설치하고 VPN만 키면 됩니다. 윈도우 또한 WSL2가 나오면서 개발은 MAC이라는 고정관념도 많이 없어졌습니다. ‘iOS 개발 아니면 굳이?’라는 생각이 들었던 시대가 있었으니까요.

이 글을 읽으시고 Docker에 처음 접근할 때 조금이나마 도움이 되셨으면 좋겠습니다. 읽어 주셔서 감사합니다.


황선규 | 풀필먼트 개발팀
hwangsg@brandi.co.kr
브랜디, 오직 예쁜 옷만