본문 바로가기

컴퓨터/웹 개발

[Docker] 용도와 사용법

소개

웹 개발 시 환경 문제

웹 개발을 하게 되면 내 로컬 환경에서는 잘 돌아가지만 웹 서버에서는 안 돌아가는 경우가 생길 수 있다. 이러한 경우는 실행환경이 달라서 발생하게 되는데, 예를 들어 내 pc의 python은 3.11 버전인데 웹서버는 3.10을 쓰고 있는 상황을 예시로 들 수 있다. 이러한 문제를 해결할 때 배포 환경도 pc와 동일하게 맞추어주면 되지만,

- 만약에 dependency가 바뀐다면?

- 만약 새로운 환경에서 개발을 시작해야한다면?

- 이미 다른 버전에 종속된 프로그램이 이미 돌아가고 있다면?

이렇게 매번 동일한 환경을 만들기도 어려울 뿐더러 항상 똑같은 환경에서 배포를 하기 어려워진다는 문제가 있다. 그래서 현재 환경에 구애받지 않고 매번 동일하게 배포를 할 수 있는 방법이 필요해진다. 이를 위해서 Docker을 사용한다.

Docker?

Docker는 애플리케이션을 컨테이너라는 단위로 포장해서 실행할 수 있게 해주는 플랫폼이다. 컨테이너는 애플리케이션과 그 실행에 필요한 모든 요소(라이브러리, 설정 파일 등)를 함께 포함하고 있어, 어떤 환경에서도 일관되게 실행할 수 있다. 컨테이너를 일종의 가상 환경이라고 생각하면 편하다.

설치

Windows의 경우 docker-desktop을 통해 설치가 가능하다.

linux는 공식 홈페이지를 참조하면 된다.

사용법

Docker는

  • Dockerfile : 설정 파일
  • Image : Container를 돌리기 위한 템플릿
  • Container : 이미지를 실행하여 생성, 작은 가상환경

로 구성된다.

Dockerfile을 작성하여 실행환경을 설정하고, 빌드하여 이미지를 만든 뒤, Container를 실행하는 방식이다.

우선 Dockerfile의 작성법을 먼저 보자.

Dockerfile

우선 예시를 보면서 시작하자. 지금 사이드 프로젝트로 진행 중인 fastapi의 docker 환경이다.

# Docker에 있는 공식 python 이미지를 가져온다.
FROM python:3.11-slim

# 작업 디렉토리를 설정한다.
WORKDIR /app

# Python 패키지를 설치한다.
# Dockerfile이 실행되는 폴더에 같이 있는 requirements.txt를 workdir로 복사한다.
COPY requirements.txt . 
RUN pip install --no-cache-dir -r requirements.txt # 패키지 설치.

# app/ 의 코드를 복사해 온다.
# Dockerfile이 실행되는 폴더에 같이 있는 app 폴더를 ./app으로 복사한다.
COPY app/ ./app 
# Dockerfile이 실행되는 폴더에 같이 있는 migrations 폴더를 ./migrations으로 복사한다.
COPY migrations/ ./migrations

# FastAPI를 실행시킨다.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

 

Dockerfile은 앱 디렉토리를 설정하고 필요한 파일들을 복사하거나 명령어를 실행하며 작업환경을 구성하는 것으로 이루어진다.

아래의 코드로 빌드를 해주자.

docker build -t fastapi-app .

`docker`의 `build` 명령어로 `.`(현재 폴더)에 있는 Dockerfile로 이미지를 만들되 tag(`-t`)를 `fastapi-app`으로 설정하겠다는 뜻이다.

빌드한 이미지는 `docker images`로 확인할 수 있다.

생성된 이미지는 아래와 같이 실행할 수 있다.

docker run -d -p 8080:8000 fastapi-app

`-d` 는 detached 모드(백그라운드 실행)를 의미하고 `-p 8000:8000` 는 호스트의 포트 8080컨테이너의 포트 8000과 연결한다는 뜻이다. 마지막으로 `fastapi-app`은 생성한 이미지를 가리킨다.

docker을 소개할 때 언급했던 것처럼 작은 가상환경을 만드는 것이기에 컨테이너 내부는 별도의 네트워크를 가진다고 생각하면 된다. 따라서 컨테이너의 포트와 호스트의 포트를 매핑해 주는 과정이 필요한 것이다.

.dockerignore

Docker에서 파일들을 복사하는 과정에서 일부 파일은 불필요한 경우가 있다. 예를 들어 빌드 파일들이나 케시 파일들은 필요가 없을 것이다. 그래서 .dockerignore에 필요 없는 파일들을 정의해 주면 .gitignore처럼 해당 파일들을 무시하고 실행된다. 아래는 예시이다.

__pycache__/
*.pyc
*.pyo
*.pyd
.env
*.log
.git

docker-compose

실제로 Docker을 활용하려면 docker-compose로 자주 활용할 것이다.

이게 무엇이냐면, db랑 service1 이랑 service2,... 이렇게 여러 가지 컨테이너들을 실행시켜야 할 때 한 번에 컨테이너들을 정의하고 실행시킬 수 있게 하는 것이다.

`docker-compose.yml`을 현재 폴더에 만들고 아래 같이 작성해 주자.

* 아래는 나의 빌드 환경이기에 설명에 맞추어 내용을 추가/변경/삭제해주어야 한다.

services:
  app:
    build:
      context: .
    ports:
      - "8000:8000"
    environment:
      MYSQL_HOST: db
      MYSQL_PORT: ${MYSQL_PORT}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - ./app:/usr/src/app
    networks:
      - bq_network
    depends_on:
      - db

  db:
    image: mysql:latest
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - "3307:3306"
    networks:
      - bq_network
    volumes:
      - ./migrations:/migrations
      - ./db-init:/docker-entrypoint-initdb.d

networks:
  bq_network: {}

각 내용에 대한 설명이다. 이때 ${}로 작성된 것은 docker가 알아서 .env에서 가져와 준다.

  • services: 실행할 컨테이너를 정의한다.
  • app, db: 컨테이너의 이름.
  • build: 빌드를 정의한다. 여기서 `.`으로 적혀 있다는 것은 현재 폴더의 Dockerfile으로 빌드하라는 뜻이다.
  • port: (호스트의 포트):(컨테이너의 포트)로 작성한다.
  • environment: 컨테이너 내부의 환경변수를 정의한다
  • depends_on: app이 실행되기 전에 db 컨테이너를 실행하라는 뜻이다.
  • volume: (추가 설명 참조)
  • networks: 네트워크를 생성해서 컨테이너끼리 통신이 가능하게 한다. 현재 `bq_network`를 공유하고 있기 때문에 app 쪽에서 `db`라는 DNS 이름으로 통신이 가능하고 반대로 db 쪽에서 `app`이라는 이름으로 통신이 가능하다.
networks 추가 설명
version: '3'
services:
  web:
    image: nginx
    networks:
      - frontend

  app:
    image: myapp
    networks:
      - frontend
      - backend

  db:
    image: mysql
    networks:
      - backend

networks:
  frontend:
  backend:​
이런 식으로 작성하게 되면
web ↔ app ↔ db의 형태로 통신하도록 만들 수 있다. Named Volume
volume 추가 설명
모든 파일을 무조건 복사하게 되면 불편한 경우가 있다. DB를 매번 빌드할 때마다 초기화하거나 설정 파일들이 매번 초기화되는 것은 별로 즐겁지 않은 일이기에 데이터를 유지할 수 있는 기능이 있다.
호스트의 폴더를 지정해서 그곳을 컨테이너의 폴더로 활용하는 것이다.
volume 기능을 사용하면 되는데
docker volume create myvolume
docker run -v myvolume:/app/data myimage​
처럼 CLI로 써도 되지만, docker-compose로 사용하는 것 위주로 보자.
version: '3'
services:
  db:
    image: postgres
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./localdir:/app/data
      - /data

volumes:
  db-data:

위는 세 가지 방식으로 volume을 사용한 코드이다.
1. Named Volume  ( volume 이름: 컨테이너의 경로 )
    `db-data`라는 volume을 생성하고 그 volume은 docker이 관리한다. 컨테이너에서 /var/lib/postgresql/data에 접근하면 실제로는 호스트 내부 폴더 중 docker이 생성한 volume 폴더를 액세스 하는 것이다.
2. Bind Mount  ( 호스트의 경로: 컨테이너의 경로 )
    `./localdir` 폴더를 컨테이너의 `/app/data`와 연결한다. (1)의 방식에서 직접 폴더를 지정한 것이라 생각하면 된다. 주의: bindmount는 /app/data가 바뀌면 그대로 반영되기에 주로 개발과정에서 사용된다. 따라서 이에 맞추어 개발용 docker-compose.dev.yml와 배포용 docker-compose.prod.yml를 별도로 만들기도 한다.
3. Anonymous Mout ( 컨테이너의 경로 )

    (1)과 동일하지만 volume명도 docker가 생성하도록 한다. (자주 안 쓰임)

 

`docker compose up --build` 나 `docker-compose up --build` 로 컨테이너를 실행할 수 있다.

추가 내용

docker exec -it 컨테이너이름 bash

위 명령어로 컨테이너에 접속할 수 있다.