[Docker] Dockerfile & Build & Docker-Compose

Dockerfile

Dockerfile은 Docker 이미지를 생성하는 데 필요한 명령어를 정의한 파일이다. 애플리케이션 환경을 설정하고 필요한 라이브러리나 의존성을 설치하며, 애플리케이션을 컨테이너 안에서 실행할 수 있도록 준비하는 과정을 정의한다.

 

1. MAINTAINER

  • Docker 이미지의 작성자 정보를 명시한다.
MAINTAINER yourname@example.com

2. LABEL

  • 이미지에 메타데이터를 추가할 수 있는 명령어이다. 예를 들어 작성자나 버전 정보를 태그로 넣을 수 있다.
LABEL maintainer="yourname@example.com"
LABEL version="1.0"

3. RUN

  • 컨테이너 안에서 명령을 실행하고 그 결과를 이미지에 포함한다. 주로 의존성 설치나 파일 설정을 위해 사용된다.
RUN apt-get update && apt-get install -y curl

4. CMD 명령어

  • 생성된 Docker 이미지를 컨테이너로 실행할 때 기본 실행 명령어를 지정하는 역할을 한다. 특히 ENTRYPOINT 명령어가 있을 경우, CMD는 기본 파라미터를 지정하는 데 사용된다.
  • 여러 개의 CMD를 작성할 수 있지만 마지막 CMD만 처리된다.
CMD ["npm", "start"]

5. ENTRYPOINT

  • Docker 이미지가 컨테이너로 실행될 때 반드시 실행될 명령을 지정한다. CMD와 유사하지만 ENTRYPOINT는 기본 명령어로, 다른 명령을 주어도 덮어쓰지 않고 인자만 추가하는 방식이다.
ENTRYPOINT ["npm", "start"]
ENTRYPOINT ["python", "runapp.py"]

ADD ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]

6. COPY

  • 호스트 시스템에서 파일을 컨테이너로 복사한다.
COPY ./source /app

7. ADD

  • COPY와 비슷하지만, URL에서 파일을 다운로드하거나 압축 파일을 자동으로 풀어주는 기능을 추가로 제공한다. 그러나 일반적으로는 COPY가 더 자주 사용된다.
ADD index.html /usr/share/nginx/html
ADD ./archive.tar.gz /app

8. ENV

  • 컨테이너에서 환경 변수를 설정한다.. 이 환경 변수는 애플리케이션에서 참조한다.
ENV NODE_ENV=production

9. EXPOSE

  • 컨테이너가 외부와 통신할 때 사용할 포트를 지정. 이 명령어는 단순히 문서화 용도이며, 실제로 포트를 열려면 docker run -p 옵션을 사용해야 합니다.
EXPOSE 8080

10. VOLUME

  • 컨테이너에서 데이터 볼륨을 마운트하여 데이터를 유지하거나 공유할 수 있게 한다. 이는 주로 데이터베이스와 같은 상태가 있는 애플리케이션에서 사용된다.
VOLUME /data

11. USER

  • 컨테이너 내부에서 명령을 실행할 사용자 계정을 설정한다. 보안 이유로 루트 계정 대신 비루트 사용자로 실행하는 것이 좋다.
USER node

12. WORKDIR

  • 컨테이너 내부에서 명령이 실행될 디렉토리를 설정한다. cd 명령과 같은 역할을 한다.
WORKDIR /app

13. ARG

  • ARG는 Docker build 과정에서 변수를 정의하고, 그 변수의 값을 전달할 수 있도록 한다. --build-arg 플래그를 사용하여 빌드 시 인자 값을 전달하며, 주로 빌드 중 필요한 설정 정보를 동적으로 제공하는 데 사용된다.
  • 민감한 정보(예: 비밀번호, API 키 등)를 ARG로 전달할 경우 이미지 내부에 노출될 위험이 있으므로 주의가 필요하다. 이런 정보는 보통 ENV나 비밀 관리 서비스를 통해 처리하는 것이 좋다.
ARG db_name
- docker build 명령어에서 --build-arg를 사용해 변수를 전달
docker build --build-arg db_name=fastdb .

- CMD에서도 사용가능 
CMD db_start.sh -h 127.0.0.1 -d ${db_name}

Dockerfile 최적화

Dockerfile을 최적화하는 이유는 이미지 크기 감소, 빌드 속도 향상, 그리고 보안 강화를 위해서이다. 최적화된 Dockerfile은 불필요한 레이어와 파일을 제거하여 이미지 크기를 줄여 전송 및 배포를 더 빠르게 하고, 캐시 활용을 통해 빌드 시간을 단축할 수 있다. 또한, 민감한 정보가 포함되지 않도록 주의함으로써 보안성을 높일 수 있다.

또한, 이미지 내부에 불필요한 파일을 포함하지 않기 위해 ".dockerignore" 사용으로 빌드와 관련 없는 파일을 이미지 내부에 넣지 않는다.(.gitignore와 동일한 방식이다)

Dockefile 작성 시 불필요한 패키지 설치를 피하고, 설치된 패키지 파일은 autoremove, clean등의 명령을 통해 제거하면 경량화에 도움이 된다.

Layer 수 최소화

Dockerfile 작성 시 생성되는 레이어(Layer) 수 최소화 예로, Run 사용시 가능하면 명령을 결합하여 사용한다.

결합 전

FROM ubuntu:20.04

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git

결합 후 

FROM ubuntu:20.04

RUN apt-get update && \
    apt-get install -y curl git

Multi-stage build

  • 빌드 과정을 여러 단계로 나누고, 필요할 때마다 각 단계의 산출물을 다른 단계로 복사하거나 참조한다.
  • 최종 단계에서는 필요한 파일만 포함된 이미지를 생성한다.
# 1단계: 빌드 스테이지
FROM node:16 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 2단계: 실행 스테이지
FROM node:16-alpine
WORKDIR /app
COPY --from=build /app/dist /app
COPY --from=build /app/package*.json ./
RUN npm install --only=production
CMD ["node", "server.js"]

단계별 설명

  • 빌드 스테이지 (build)
    첫 번째 FROM node:16 AS build 명령어는 빌드를 위한 스테이지를 정의한다.
    이 단계에서는 전체 프로젝트 소스를 복사하고, 개발 의존성을 포함한 모든 패키지를 설치한 뒤, 빌드 명령어(npm run build)를 실행한다.
    빌드 결과물은 /app/dist 디렉토리에 생성됩니다.
  • 실행 스테이지
    두 번째 FROM node:16-alpine 명령어는 경량화된 Node.js 이미지를 사용해 최종 실행용 이미지를 만든다.
    빌드 스테이지에서 생성된 빌드 결과물(/app/dist)과 package.json 파일을 이 단계로 복사한다.
    실행에 필요한 프로덕션 의존성만 설치한다.
    CMD 명령어로 애플리케이션을 실행한다.

-> 최종 이미지를 경량화하고, 불필요한 빌드 툴을 포함시키지 않음으로써 효율적인 배포가 가능

캐시를 고려한 Dockerfile 작성

Dockerfile 작성 시 캐시를 효율적으로 활용하면 빌드 시간을 크게 줄일 수 있다. Docker는 각 명령어 단위로 캐시를 생성하고, 이전 빌드에서 캐시가 존재하면 동일한 명령어를 재실행하지 않고 캐시된 결과를 재사용한다. 하지만 캐시가 깨지는 경우, 해당 명령어부터 모든 명령어가 다시 실행된다. 캐시 활용을 극대화하려면, 자주 변경되지 않는 명령어를 상단에 배치하고, 변경 가능성이 높은 명령어를 하단에 배치하는 것이 좋다.

autoremovem, autoclean, rm 등으로 패지지 잔여물 제거

FROM ubuntu:14.04
RUN apt update && apt install apache2 -y && \
	apt clean autoclean && \
	apt autoremove -y && \
	rm -rfv /tmp/* /var/lib/apt/lists/* /var/tmp/*
WORKDIR /var/www/html
COPY index.html .
EXPOSE 80
CMD apachectl -D FOREGROUND

Dockerfile 보안

1. Docker 이미지 서명 방식
Docker Content Trust (DCT)

  • Docker Content Trust는 Docker 이미지를 서명하고 검증할 수 있는 기술이다.
  • Docker에서 제공하는 기본 기능으로, DOCKER_CONTENT_TRUST=1을 설정하면 서명된 이미지만 풀(pull)하고 푸시(push)할 수 있게 된다.
  • Notary라는 도구를 사용해 이미지를 서명하고 검증한다. 
  • 서명된 이미지는 RSA 공개키를 기반으로 서명되어, 이미지가 다운로드되는 시점에 검증된다.
export DOCKER_CONTENT_TRUST=1

2. Dockerfile에서 USER 명렁어 사용으로 root사용자 접근을 억제한다. USER 명령은 Run, ENTRYPOINT 또는 CMD 명령을 실행하기 위한 특정 사용자를 지정할 경우 사용한다.

FROM alpine:3.14

RUN apk --no-cache add nginx

RUN addgroup -S myuser && adduser -S myuser -G myuser

RUN chown -R myuser:myuser /var/lib/nginx /var/www/html

WORKDIR /var/www/html

USER myuser

COPY index.html .

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

3. Dive를 사용하여 이미지검사 수행한다. SETUID, SETGID 비트가 있는 모든 바이너리를 찾아서 제거한다.

Dockerfile build

scratch를 활용한 멀티스테이징 빌드

# gostart.go 작성 
$ vi gostart.go
package main

import "fmt"

func main() {
    fmt.Println("Go app is starting!")
}

# Dockerfile 작성 
# From 1절 
FROM golang:1.15-alpine3.12 AS gobuilder-stage
WORKDIR /app/
COPY gostart.go /app/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /app/gostart

# From 2절 
FROM scratch
COPY --from=gobuilder-stage /app/gostart /app/gostart
CMD ["/app/gostart"]

단일 스테이지 빌드는 쉽게 사용할 수 있지만, 불필요한 빌드 도구와 환경이 포함되어 이미지 용량이 크다.
멀티 스테이지 빌드는 빌드 단계에서 필요한 도구를 포함한 후, 최종 이미지는 실행 파일만 포함하게 하여 이미지 크기를 최소화할 수 있다.

User 를 활용한 멀티스테이징 빌드

# appstart.sh 스크립트 작성 
$ vi appstart.sh
#!/bin/bash
echo "Best image build, multi stage build!"

# From 1절 
FROM ubuntu:20.04 as v1-stage
RUN apt-get update && apt-get install nginx -y
WORKDIR /app
COPY appstart.sh /app

# From 2절 
FROM alpine:3.12.1
RUN addgroup -S appgroup && adduser -S kdj -G appgroup -h /home/kdj
COPY --from=v1-stage /app /home/kdj
USER kdj
ENTRYPOINT ["sh","/home/kdj/appstart.sh"]

appgroup이라는 그룹을 생성하고, kdj이라는 사용자 계정을 추가한다. kdj은 /home/kdj 디렉토리를 홈 디렉토리로 가지며, appgroup 그룹에추가한다. 
컨테이너에서 root가 아닌 사용자 권한을 가진 특정 계정(kdj)을 설정하여 애플리이션을 시작하여 보안성을 높일수 있다. 이로써 멀티스테이징 빌드로 이미지를 경량화하고 User 계정을 사용하여 보안성 또한 같이 높였다. 

ADD를 사용하여 tar.gz으로 빌드

FROM ubuntu:14.04

RUN apt-get update && \
    apt-get -y install apache2 vim curl && \
    rm -rf /var/lib/apt/lists/* && \
    rm -rf /etc/apt/sources.list.d/*

ADD webapp.tar.gz /var/www/html

WORKDIR /var/www/html

EXPOSE 80

CMD /usr/sbin/apache2ctl -D FOREGROUND

rm 명령어로 쓸데 없는 패키지 종속성을 제거해주고 tar.gz를 ADD 방식으로 추가해주었다. Dockerfile에서는 tar.gz로 ADD를 해줘도 따로 파일을 압축해제 해줄 필요없이 바로 적용된다.

Nodejs Image build

완성된 파일은 다음과 같다.

1. Node.js 설치 및 기본 설정

sudo apt-get update
sudo apt-get -y install npm

2. Node.js 프로젝트 초기화

# 프로젝트 디렉토리로 이동 후 npm 초기화
npm init -y

npm install express

3. Node.js 서버 스크립트 작성 (server.js)

vi server.js

const express = require('express');
const app = express();
const path = require('path');

app.use(express.static('public'));

........

4. Dockerfile 작성

# Dockerfile 작성
vi Dockerfile

# Dockerfile 내용
# Node.js 20 알파인 이미지를 베이스로 사용
FROM node:20-alpine

# 컨테이너 내부 작업 디렉토리 설정
WORKDIR /usr/src/app

# package.json과 package-lock.json 파일 복사
COPY package*.json ./

# 종속성 설치
RUN npm install

# 애플리케이션 코드 복사
COPY . .

# 3000 포트 노출
EXPOSE 3000

# 애플리케이션 실행
CMD ["node", "server.js"]

5. dockerignore 파일 작성

# .dockerignore 작성
vi .dockerignore

# .dockerignore 내용
node_modules
npm-debug.log

6. Docker 이미지 빌드 & 컨테이너 실행

# Docker 이미지 빌드
docker build -t lab2-nodejs-app:1.0 --no-cache .

# Docker 컨테이너 실행 (포트 3000 -> 3001로 매핑)
docker run -d -p 3001:3000 lab2-nodejs-app:1.0

결과

JAVA Image build

1. Java 개발 환경 설치
Maven과 Java Development Kit (JDK)를 설치하여 Java 프로젝트를 생성할 준비를 완료한다.

sudo apt install maven -y
sudo apt install default-jdk -y

2. Maven 프로젝트 생성
Maven 명령어를 사용하여 Java 프로젝트를 생성한다. groupId와 artifactId를 정의하고, maven-archetype-quickstart를 사용해 기본 템플릿으로 프로젝트를 생성한다.

mvn archetype:generate -DgroupId=com.example -DartifactId=lab3-java-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

 

3. 프로젝트 파일 구조
생성된 프로젝트 구조는 아래와 같다.

lab3-java-app/
├── pom.xml               # Maven 설정 파일
├── main
│   └── java
│       └── com
│           └── example
│               └── App.java
└── test
    └── java
        └── com
            └── example
                └── AppTest.java
    └── test/             # 테스트 코드

4. 리소스 파일 추가
src/main/resources/static/ 디렉터리에 index.html과 styles.css 파일을 작성하여 웹 페이지를 구성한다.

$ cd src/main/
$ mkdir resources && cd $_
$ mkdir static && cd $_
$ vi index.html
<!-- index.html -->
<html>
<head>
  <meta charset="UTF-8">
  <title>My Java App</title>
</head>
<body>
  <h1>Welcome to Fastcampus - Java App using dockerfile</h1>
</body>
</html>

$ vi styles.css
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
h1 {
color: #333;
}
p {
color: #777;
}

5. Java 메인 애플리케이션 작성
MainApplication.java 파일을 작성하여 애플리케이션의 진입점을 정의한다. Spring Boot 애플리케이션으로 설정한다.

package com.example.mywebapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

6. Dockerfile 작성
Dockerfile을 작성하여 Java 애플리케이션을 컨테이너화할 수 있도록 설정한다.

$ vi Dockerfile
FROM adoptopenjdk:11-jre-hotspot
WORKDIR /usr/src/app
COPY ./target/lab3-java-app-1.0-SNAPSHOT.jar app.jar
COPY ./target/dependency/*.jar ./lib/
COPY ./src/main/resources/static ./static
CMD ["java", "-cp", "app.jar:lib/*", "com.example.mywebapp.MainApplication"]

7. Maven 빌드
Maven을 사용하여 프로젝트를 빌드하고 JAR 파일을 생성합니다.

mvn clean package

결과물

root@dj-master:~/java/lab3-java-app$ ls
build Dockerfile pom.xml src target
root@dj-master:~/java/lab3-java-app$ cd target/
root@dj-master:~/java/lab3-java-app/target$ ls
classes dependency generated-sources generated-test-sources lab3-java-app-1.0-SNAPSHOT.jar maven-archiver maven-status surefire-reports test-classes
root@dj-master:~/java/lab3-java-app/target$ cd ..
root@dj-master:~/java/lab3-java-app/src/main/resources/static/images$ ls
fastcampus.png

8. Docker 이미지 빌드 & 컨테이너 실행

docker build -t lab3-java-app:1.0 --no-cache .
docker run -itd -p 8080:8080 --name=java-app lab3-java-app:1.0

Docker compose

Docker Compose는 여러 개의 Docker 컨테이너로 구성된 애플리케이션을 쉽게 정의하고 관리할 수 있도록 도와주는 도구이다. 이를 통해 복잡한 컨테이너 구조를 설정할 때도 간단하게 yaml파일 하나로 간단하게 다룰 수 있다.

  •  YAML 파일 사용: docker-compose.yml 파일에 애플리케이션을 구성하는 여러 컨테이너의 설정을 정의가능하다.
  •  환경 구성: 네트워크 설정, 볼륨 마운트, 환경 변수 등을 yaml파일로 쉽게 설정할 수 있어 간편하다.

Docker compose 3단계 프로세스
1. Dockerfile 작성으로 배포하고자 하는 애플리케이션 환경 정의(선택)
2. docker-compose.yml 하나 이상의 컨테이너 서비스를 실행할 수 있도록 정의
3. docker compose up 명령으로 YAML 코드로 정의된 서비스를 시작하고 실행한다.

Docker compose 옵션

1. build
context: Dockerfile이 위치한 경로를 지정합니다. 동일한 경로일 경우 생략 가능하다.
dockerfile: 사용할 Dockerfile의 이름을 명시한다.

mydiary-front:
build:
context: ./my-diary-front 
dockerfile: Dockerfile

2. container_name
컨테이너 이름을 지정. 생략 시 자동으로 생성된다.

 

3. ports
서비스 내부 포트와 외부 호스트 포트를 바인드하는 옵션. 외부 노출 포트도 지정 가능하다.

 

4. expose
서비스 간 통신에 필요한 경우 사용할 포트를 지정한다.
docker run --expose 옵션과 유사합니다.

 

5. networks
사용할 네트워크 이름을 정의한다.

 

6. volumes
서비스 내부 디렉토리와 호스트 디렉토리를 연결하여 데이터의 볼륨을 설정한다.

 

7. environment
서비스 내부에서 사용할 환경 변수를 설정한다.
환경 변수가 많은 경우, .env 파일을 사용하여 지정할 수도 있다.

 

8. command
서비스가 구동된 이후 실행할 명령어를 작성한다.
docker run의 마지막에 작성되는 명령어와 동일하다.

 

9. restart
서비스 재시작 옵션을 설정한다.

 

10. depends_on
서비스간의 종속성을 의미하며 먼저 실앵해야하는 서비스를 지정하여 순서를 지정한다.

 

11. scale
쿠버네티스 replicas랑 같다. 해당 서비스의 복제 컨테니어 수 지정

Docker compose를 이용한 3-tier application

Docker-compose는 기존에 워낙 많이 다뤄봐서 간단하게 Docker-compose의 파일을 보고 아키텍처링만 간단하게 설명하면서 작성해보겠다.

Docker-compose.yml

version: '3.8'

services:
 mydiary-db:
  image: mysql:5.7-debian
  container_name: rolling-db
  environment:
   MYSQL_ROOT_PASSWORD: pass123
   MYSQL_DATABASE: paperdb
   MYSQL_ROOT_HOST: '%'
   MYSQL_USER: user
   MYSQL_PASSWORD: user
  ports:
   - '3306:3306'
  networks:
   - mydiary-net
  restart: always
  command:
   - --character-set-server=utf8
   - --collation-server=utf8_general_ci

 mydiary-back:
  build:
   context: ./my-diary-back
   dockerfile: Dockerfile
  deploy:
   replicas: 3
  restart: always
  depends_on:
   - mydiary-db
  ports:
   - '8081-8083:8080'
  environment:
   SPRING_DATASOURCE_URL: jdbc:mysql://rolling-db:3306/paperdb?serverTimezone=Asia/Seoul
   SPRING_DATASOURCE_USERNAME: user
   SPRING_DATASOURCE_PASSWORD: user
  networks:
   - mydiary-net

 mydiary-front:
  build:
   context: ./my-diary-front
   dockerfile: Dockerfile
  deploy:
   replicas: 3
  restart: always
  depends_on:
   - mydiary-back
  ports:
   - '3000-3002:3000'
  networks:
   - mydiary-net

 proxy-be:
  image: nginx:1.21.5-alpine
  container_name: rolling-server-lb
  restart: always
  depends_on:
   - mydiary-back
  ports:
   - '8080:80'
  volumes:
   - ${PWD}/proxy/nginx-be.conf:/etc/nginx/nginx.conf
  networks:
   - mydiary-net

 proxy-fe:
  image: nginx:1.21.5-alpine
  container_name: rolling-front-lb
  restart: always
  ports:
   - '80:80'
  volumes:
   - ${PWD}/proxy/nginx-fe.conf:/etc/nginx/nginx.conf
  networks:
   - mydiary-net

networks:
 mydiary-net:
  driver: bridge
  ipam:
   driver: default
   config:
   - subnet: 172.20.0.0/24
     ip_range: 172.20.0.0/24
     gateway: 172.20.0.1

서비스 설명

개발자의 코드분석이 중요 요점이아닌 서비스 구성도에 대한 목적 파악이 우선순위이기 때문에 다음 내용과 같이 정리만 하였다.

Docker Compose 프로젝트 구성 요약

 

1. 부하분산 적용

  • 백엔드(mydiary-back)와 프론트엔드(mydiary-front) 서비스의 deploy.replicas를 3개로 설정해 부하 분산이 가능하도록 구성하였다.

2. 네트워크 격리와 서브넷 지정

  • mydiary-net 네트워크를 사용해 서비스들을 격리시켰으며, config 명령어로 서브넷을 지정해 네트워크 내부 IP 구성하였다.

3. Nginx 프록시 설정

  • 프론트엔드와 백엔드용 Nginx 프록시를 각각의 컨테이너로 설정하고, 호스트의 nginx.conf 파일을 수정해 볼륨 마운트로 컨테이너 내부에 적용하였다.

nginx.conf파일 작성 시 주의할점

nginx.conf에서 각각의 목적지를 잘 지정해주어야한다. nignx.conf에서 Upstream에서 IP 대신 컨테이너 이름과 포트를 지정했는데, 이는 Docker가 DNS 기반 서비스 검색을 지원하여 같은 네트워크에 있는 컨테이너 이름을 호스트명처럼 사용할 수 있기 때문이다.

예시

events {
    worker_connections 1024;
}

http {
    upstream mydiary-back {
        server mydiary-back:8081;
        server mydiary-back:8082;
        server mydiary-back:8083;
    }

    server {
        listen *:8080 default_server;

        location / {
            proxy_pass http://mydiary-back;
        }
    }
}

4. 컨테이너 실행 순서 지정

  • depends_on 옵션을 사용해 컨테이너 실행 순서를 DB → 백엔드 → 프론트엔드로 지정하였다.
  • DB와 백엔드가 연동하기 위해선 환경변수가 잘들어가야하고 프론트엔드와 백엔드가 연동되기 위해선 백엔드가 먼저실행되어야 하기때문에 이런 종속 관계 오류가 발생하지 않도록 지정하였다.

5. 재시작 정책

  • restart: always 옵션을 통해, 컨테이너가 예기치 않게 종료되더라도 자동으로 재기동되도록 설정했다. 

'Docker' 카테고리의 다른 글

[Docker] Docker Swarm  (0) 2024.10.24
[Docker] Docker Resource Limit & Volume  (0) 2024.10.23
[Docker] Docker Networking & Proxy  (0) 2024.10.21
[Docker] Docker Lifecycle & Image & CLI  (0) 2024.10.20