들어가며
본 포스팅은 어떻게 dockerfile을 작성할 것인지에 대해 실습하는 과정을 중점적으로 포스팅하였기 때문에, docker의 구조 및 자세한 설명은 생략한다. docker의 다운로드 등은 다른 포스팅을 참고하는 것을 권장합니다.
What is a container?
docker의 공식 문서에 따르면 컨테이너의 정의는 아래와 같다.
A container is a sandboxed process running on a host machine that is isolated from all other processes running on that host machine. That isolation leverages kernel namespaces and cgroup features that have been in Linux for a long time. Docker makes these capabilities approachable and easy to use. To summarize, a container:
- Is a runnable instance of an image. You can create, start, stop, move, or delete a container using the Docker API or CLI.
- Can be run on local machines, virtual machines, or deployed to the cloud.
- Is portable (and can be run on any OS).
- Is isolated from other containers and runs its own software, binaries, configurations, etc.
If you're familiar with chroot, then think of a container as an extended version of chroot. The filesystem comes from the image. However, a container adds additional isolation not available when using chroot.
요약하면, image의 인스턴스이며, local, VM, Cloud 등 어디에서도 실행되며, OS에 영향을 받지 않는다고 한다. window나 mac 같은 경우에는 디렉토리 구조가 다르기 때문에 mac에서 잘 작동하는 소스 코드가 window에서는 안될 수 있고, 그 반대의 일이 일어날 수 있지만 docker를 이용한 컨테이너는 어떤 환경에서도 실행된다는 것이다.
What is an image?
docker의 공식 문서에 따르면 이미지의 정의는 아래와 같다.
A running container uses an isolated filesystem. This isolated filesystem is provided by an image, and the image must contain everything needed to run an application - all dependencies, configurations, scripts, binaries, etc. The image also contains other configurations for the container, such as environment variables, a default command to run, and other metadata.
컨테이너는 분리된 파일시스템을 사용하는데, 이미지가 이를 제공한다. 이미지는 컨테이너를 실행시키는데 필요한 모든 정보를 담고 있는데, dockerfile에 쓰인 내용을 통해서 docker image를 만들 수 있다.
즉 docker를 통해 현재 파일들을 가지고 낮선 환경에서도 dockerfile에 쓰인 대로 명령어를 실행하면 프로그램을 실행시킬 수 있다는 것이다.
dockerfile 작성 방법?
위에서 살펴본 내용에 의하면 컨테이너는 이미지의 인스턴스이기 때문에 컨테이너를 만들기 위해서는 먼저 이미지가 필요하다. dockerfile을 잘 작성하여, dockerfile에 쓰여있는 대로 실행하며 이미지를 만들 수 있게 해야 한다.
dockerfile에서 사용하는 명령어는 다음과 같다.
- FROM : 기본 이미지를 지정하는 명령. 기본 이미지를 기반으로 이미지를 생성한다.
- RUN : 컨테이너 내에서 실행할 명령. 패키지 설치 등이 있다.
- COPY : 로컬 파일은 컨테이너 내로 복사하는 데 사용되는 명령.
- ARG : 빌드 시간에 환경 변수를 정의하는 데 사용되는 명령.
- ENV : 환경 변수를 설정하는 명령.
- EXPOSE : 컨테이너가 리스닝할 포트를 지정하는 명령.
- CMD 및 ENTRYPOINT : 컨테이너가 시작될 때 실행될 명령을 지정하는 명령.
이와 같은 명령어가 있다.
dockerfile of node.js
다음은 node 환경에서 크롤링하는 프로그램이다. 구조는 아래와 같다. 이 프로젝트는 node 18 버전을 사용하고 하고 있고, typescript라는 언어를 사용한다.
Dockerfile을 아래와 같이 작성했다.
FROM node:18-alpine
WORKDIR /app
COPY . /app/
RUN yarn install
CMD ["tsc", "scraper.ts"]
해당 dockerfile을 분석해보면
- FROM node:18-alpine :
해당 프로젝트를 이미지로 만들기 위해 먼저 node 18 버전의 이미지를 기반으로 진행한다. - WORKDIR /app
해당 프로젝트는 docker 환경의 /app 위에서 실행될 것이다. 따라서 이후 실행될 명령어는 /app/ 에서 실행된다. - COPY . /app/
해당 프로젝트의 루트 디렉토리에 존재하는 모든 파일을 /app 디렉토리 위에 COPY한다. - RUN yarn install
yarn.lock과 package.json의 내용을 읽고 의존성을 설치한다. yarn install = npm install 이다. - CMD ["tsc", "scraper.ts"]
scraper.ts 파일을 빌드하여 scraper.js 파일을 생성한다.
앞서 설명했듯이 이미지는 isolated filesystem이 쓰여있어야 한다. 위 프로젝트의 파일들을 가지고 낮선 환경에 놓여 있다고 생각했을 때, dockerfile에 쓰인 대로만 실행하면 잘 작동해야 한다는 것이다.
여기까지 이해했다면 위 dockerfile의 문제점을 두 가지 떠올려야 한다.
- COPY . /app/ 명령어를 통하면 /node_modules 파일이 모두 복사되는 게 아닌가?
- 모든 파일을 옮긴 후 tsc scraper.ts 명령어를 실행하면 코드의 빌드만 되는 것이고 실행은 안되는 것이 아닌가?
두 문제점 모두 맞는 이야기이다. 하지만 이는 의도된 것이다. 하나하나 짚어가며 이해해보자.
1. node_modules의 복사
node_modules는 yarn install 명령어를 통해서 package.json 및 yarn.lock 파일을 읽고 패키지들을 설치한 결과다. 깃허브를 다룰 줄 아는 개발자라면 이 디렉토리는 .gitignore를 통해서 깃허브 레포지토리에도 올리지 않는다는 사실을 알 것이다. 어차피 yarn install을 하면 만들어지고, node_modules는 용량이 크기 때문이다.
docker 환경에서는 이 문제점을 .dockerignore를 통해서 해결한다. 내용은 다음과 같다.
.dockerignore는 Dockerfile로 이미지를 빌드할 때 어떤 파일을 제외시킬 것인지 명시하는 파일입니다.
.idea
node_modules
.gitignore
README.md
.dockerignore을 다음과 같이 작성하여 node_module는 dockerfile을 통해 /app/ 디렉토리로 파일들을 복사하는 과정에서 같이 복사되지 않는다.
위 구조 그대로 /app/ 디렉토리에 복사되어 올라온 후, yarn install 명령어를 통해서 node_modules 가 생성되어 빌드된다.
README.md, .gitignore 등과 같이 prod 환경에서 아예 필요 없는 파일들은 무시되게 된다. 첫 번째 문제인 node_modules가 복사되는 문제는 .dockerignore를 통해 해결한 셈이다.
2. 실행은 안되는 것이 아닌가요?
해당 문제점은 의도된 것이고, 실행하는 커맨드는 docker-compose.yml라는 파일에 담겨 있다. dockerfile에는 의존성 설치, 타입스크립트를 빌드하는 과정만 포함하고 있다.
docker-compose.yml에 대한 내용은 추후 다룰 예정입니다.
dockerfile of springboot
이번에는 카드의 정보를 크롤링하여 반환하는 프로젝트에서의 dockerfile을 살펴보자. 해당 프로젝트는 kotlin 언어를 사용하고 있고, springboot 프레임워크를 사용한다. java 및 kotlin을 컴파일하는 데 필요한 JDK ( Java Developer Kit )는 17 버전을 사용하고 있다.
FROM openjdk:17
WORKDIR /apps
COPY build/libs/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
해당 dockerfile을 분석해보면
- FROM openjdk:17
해당 프로젝트를 이미지로 만들기 위해 먼저 JDK 18 버전의 이미지를 기반으로 진행한다. - WORKDIR /apps
해당 프로젝트는 docker 환경의 /apps 위에서 실행될 것이다. 따라서 이후 실행될 명령어는 /apps/ 에서 실행된다. - COPY build/libs/*.jar app.jar
build/libs/*.jar 파일을 workdir 위에 app.jar라는 이름으로 복사한다. - EXPOSE 8080
컨테이너가 8080 포트를 리스닝하도록 설정한다. - CMD ["java", "-jar", "app.jar"]
app.jar를 실행시키는 명령어이다.
Java & Kotlin 언어는 이와 같이 dockerfile을 작성할 수 있다. node 환경에서의 dockerfiler과 살짝 비슷하지만 이번 dockerfile이 원하는 행동은 조금 다르다. 다음 사진과 함께 비교해보자.
프로그램의 실행은 이와 같은 과정으로 이루어져 있다. 매우 깊게 들어가면 Java 계열 언어는 컴파일러, Javascript 계열 언어는 인터프리터라 하나의 사진으로 뭉뚱그릴 수는 없다. 하지만 대략적으로 이런 구조로 프로그램이 작동한다고 보자.
첫 번째 dockerfile에서는 현재 디렉토리의 파일을 모두 /app으로 옮긴 후 tsc scrapers.ts 명령어를 통해서 빌드하는 과정까지를 담고 있다. 프로그램이 동작할 수 있는 환경을 세팅해 둔 후 빌드되어 생긴 scraper.js를 실행시키면 프로그램이 동작하게 된다.
두 번째 dockerfile에서는 현재 디렉토리의 파일 중 build/libs/*.jar 파일을 /apps 폴더로 옮긴 후 실행하는 과정을 담고 있다. Java 계열 언어는 프로그램을 빌드하면 .jar 파일이 만들어진다. 이 .jar 파일을 실행시키면 프로그램이 동작하게 된다. 즉, 첫 번째 dockerfile과는 다르게 local에서 먼저 파일을 빌드하고나서 dockerfile을 통해 image를 만들어야 정상적으로 동작하는 것이다.
Quiz. 그렇다면 다음 dockerfile은 무엇을 의미하는 것일까?
FROM python:3.10
WORKDIR /app
COPY main.py /app/
CMD ["python3", "main.py"]
아마 python 계열의 언어로 만들어진 프로젝트를 dockerfile을 통해 이미지를 만드는 것일 것이다. python 진영의 파일 실행 명령어는 python3 main.py이기 때문에 이를 통해 만들어진 이미지는 실행하는 커맨드까지 담고 있는 것이다. ( 이미지를 통해 컨테이너를 만들자 마자 실행이 된다는 뜻 ) 프로젝트의 루트 디렉토리에 있는 main.py을 /app 디렉토리에 복사하고, 실행시키는 dockerfile 스크립트일 것으로 추측할 수 있다.
마치며
이번 포스팅에서는 dockerfile을 어떻게 작성하는 지에 대해서 포스팅했습니다. 앞으로 docker-compose.yml 및 kubernetes같은 다양한 infra 기술을 포스팅 할 예정입니다.
Reference
- 프로그램의 실행 과정 사진 출처 : https://st-lab.tistory.com/176
- docker docs : https://docs.docker.com/
'DevOps > docker' 카테고리의 다른 글
docker multi-stage build에 대해서 ( spring ) (0) | 2024.11.25 |
---|---|
[Jenkins] DinD vs DooD 그리고 DooD 설정법 (1) | 2024.10.30 |
docker의 경량화 버전인 enroot (0) | 2024.07.13 |
docker를 사용하는 이유 (0) | 2023.09.03 |