순서상 Jenkins Pipeline 에 관해서 작성할 때 Dockerfile 에 관해서도 작성했어야 했는데 다 작성하고 보니 생각나서 부연설명으로 작성해보도록 한다.


1. Dockerfile 이란 ?

Dockerfile 은 Docker 컨테이너를 생성하기 위한 설정 파일로, 컨테이너를 실행하기 위한 환경을 자동으로 구성하는 명령어의 모음이다. 이를 통해 일관된 환경에서 애플리케이션을 실행하고 배포할 수 있다. 그렇기 때문에 모든 서비스마다 Dockerfile 은 다르게 구성되어 있으며, 해당 설정 파일을 제대로 작성해야 Jenkins Pipeline 에서 제대로 된 이미지가 빌드되어질 수 있다.

 

1.1 Dockerfile 의 주요 역할

  1. 이미지 자동화 → 매번 수동으로 환경을 설정할 필요 없이 Dockerfile을 실행하면 동일한 환경을 자동으로 구성할 수 있다.
  2. 일관된 배포 → 모든 개발자 및 운영 환경에서 동일한 환경을 유지할 수 있다.
  3. 경량화 및 효율성 → 불필요한 패키지나 의존성을 제거하고 최적화된 컨테이너 이미지를 생성할 수 있다.
  4. 버전 관리 및 재현성 → Dockerfile을 사용하면 환경을 코드로 관리할 수 있어, 쉽게 재현 및 롤백이 가능하다.

2. 각 서비스별 Dockerfile

현재 필자의 서비스는 총 5개이다. 유저 서비스와 알람서비스는 Spring Boot, 채팅 서비스는 Node.js, 친구 서비스는 Go 그리고 Front 의 경우 EXPO 의 프레임워크로 구성되어있다. 따라서 각 서비스별 Dockerfile 은 모두 다르다.

 

2.1 User 서비스의 Dockerfile

#1. Gradle 과 JDK를 포함한 이미지 사용
FROM gradle:8.0-jdk17 AS build
#2. 작업 디렉토리 설정
WORKDIR /app
#3. 파일 복사
COPY . .
#4. Gradle 빌드 실행
RUN gradle clean build --no-daemon
#5. 최종이미지 설정
FROM openjdk:17-slim
#6. 빌드된 JAR 파일 복사
COPY --from=build /app/build/libs/User-0.0.1-SNAPSHOT.jar /app.jar
#7. 애플리케이션 실행
ENTRYPOINT ["java", "-jar", "/app.jar"]

 

1. Gradle 과 JDK 를 포함한 이미지 사용

  • FROM 은 현재 단계에서 사용할 기본 이미지(Base Image) 를 지정한다.
  • gradle:8.0-jdk17 은 Gradle 8.0 및 JDK 17 이 포함된 공식 Gradle 이미지를 사용한다. jdk17 이 포함되어 있어 Java 17 을 기반으로 빌드할 수 있다.
  • AS build 는 멀티 스테이지 빌드(Multi-Stage Build) 를 활용하기 위해 현재 단계에 build 라는 별칭을 지정한다. 이후에 다른 FROM 문에서 이 빌드 단계를 참조할 수 있다.

2. 작업 디렉토리 설정

  • WORKDIR /app 은 컨테이너 내부에서 작업할 디렉토리를 /app 으로 설정한다. 이후 실행되는 모든 명령어는 기본적으로 /app 디렉토리에서 실행된다.

3. 소스코드 복사

  • COPY . . 는 현재 로컬 머신의 디렉토리 내용을 컨테이너 내부의 /app 디렉토리로 복사한다. 즉, 현재 프로젝트의 모든 소스 코드와 설정 파일을 해당 디렉토리에 복사하는 역할을 한다.

4. Gradle 빌드 실행

  • RUN 은 컨테이너 내부에서 이미지 빌드 시 실행할 명령어를 지정한다.
  • gradle clean build --no-daemon 에서 clean 은 기존 빌드된 파일을 삭제하여 깨끗한 빌드 환경을 보장, build 는 프로젝트를 빌드하고, 실행 가능한 .jar 파일을 생성, --no-daemon 은 Gradle 데몬을 사용하지 않고 빌드를 수행하는 옵션이다. Gradle 은 기본적으로 빌드 성능을 향상시키기 위해 daemon process 를 실행하지만, Docker 환경에서는 불필요한 프로세스를 최소화하는 것이 좋기 때문에 데몬을 비활성화 한다.

5. 최종 이미지 설정 (최적화된 런타임 환경 구성)

  • FROM openjdk:17-slim 은 최종 실행을 위한 베이스 이미지를 지정한다.
  • openjek:17-slimJava 17을 실행할 수 있는 가벼운 버전의 OpenJDK 이다. Slim 버전을 사용하여 불필요한 패키지가 포함되지 않도록 최적화 한다. 이전 gradle:8.0-jdk17 이미지는 빌드과정에서만 사용되고, 최종 실행 이미지는 더 가벼운 openjdk:17-slim 을 사용한다. 결과적으로 컨테이너 크기가 작아지고 실행 속도가 향상된다.

6. 빌드된 JAR 파일 복사

  • COPY --from=build 는 이전 단계에서 빌드된 결과물을 가지고 온다.
  • /app/build/libs/User-0.0.1-SNAPSHOT.jar빌드 과정에서 생성된 실행 파일 (.jar) 을 찾는 경로이다. 
  • /app.jar 는 최종 실행 환경에서 사용될 .jar 파일을 컨테이너 내부의 /app.jar 로 복사한다.

7. 애플리케이션 실행

  • ENTRYPOINT 는 컨테이너가 실행될 때 자동으로 실행되는 명령어를 설정한다.
  • ["java", "-jar", "/app.jar"] 는 JSON 배열 형식 ["명령어", "옵션", "옵션 값"] 으로 작성되었으며, 쉘 환경에서 명령어를 직접 실행할 수 있게 된다. 따라서 최종 명령어는 java -jar /app.jar 로 Spring Boot 애플리케이션을 실행하게 되는 것이다.

 

2.2 Chat 서비스의 Dockerfile

#1. 실행용 이미지 설정
FROM node:20.15.0-slim
#2. 권한 문제 해결
USER root
#3. 빌드된 파일 복사
COPY . .
#4. npm 캐시 정리
RUN npm cache clean --force
#5. npm 패키지 설치
RUN npm install
#6. 애플리케이션 실행
CMD ["node", "src/server.js"]

1. 실행용 이미지 설정

  • Spring boot 와 동일하게 Node.js 20.15.0 버전이 포함된 공식 Docker 이미지를 사용한다. 또한 slim 버전을 사용하여 불필요한 패키지를 제거하여 컨테이너의 용량을 줄여 최적화 하였다.

2. 권한 문제 해결

  • 기본적으로 Docker 컨테이너에서 실행되는 프로세스는 보안상의 이유로 root 권한이 없는 기본 사용자로 실행된다. 하지만 npm install 과정에서 특정 파일 및 폴더 접근에 권한문제가 발생하여 임시적으로 root 사용자 권한을 부여하였다.

3. 빌드된 파일 복사

  • 현재 디렉토리에 있는 모든 파일을 컨테이너 내부의 현재 디렉토리 / 로 복사한다. WORKDIR 이 지정되지 않았기 때문에 루트 디렉토리에 복사된다.

4. npm 캐시 정리

  • 기존에 모든 캐시를 제거하여 불필요한 용량을 줄이고, 패키지 설치 시 최신패키지를 가져오도록 강제한다.

5. npm 패키지 설치

  • package.json 및 package-lock.json 을 기반으로 필요한 Node.js 패키지를 설치한다. 예를 들어 채팅서비스의 경우 express, socket.io, kafka 등이 있을 수 있다.

6. 애플리케이션 실행

  • 컨테이너가 실행될 때 node src/server.js 명령어가 실행된다. 즉, Node.js 애플리케이션이 실행되는 것이다.

 

2.3 Alert 서비스의 Dockerfile

#1. Gradle 과 JDK 를 포함한 이미지 설정
FROM gradle:8.5-jdk17 AS build
#2. 작업 디렉토리 설정
WORKDIR /app
#3. 소스 코드 복사
COPY . .
#4. Gradle 빌드 실행
RUN gradle clean build --no-daemon
#5. 최적화된 실행용 이미지 설정
FROM openjdk:17
#6. 빌드된 JAR 파일 복사
COPY --from=build /app/build/libs/grpcServer-0.0.1-SNAPSHOT.jar /app.jar
#7. 애플리케이션 실행
ENTRYPOINT ["java", "-jar", "/app.jar"]

Alert 서비스의 경우 User 서비스와 거의 동일하기 때문에 추가적인 설명 없이 넘어가도록 한다.

 

2.4 Friend 서비스의 Dockerfile

#1. Go 및 Alphine 기반 이미지 설정
FROM golang:1.23-alpine
#2. 작업 디렉토리 설정
WORKDIR /app
#3. 소스 코드 복사
COPY . .
#4. Go 의존성 및 다운로드
RUN go mod tidy
#5. Go 애플리케이션 빌드
RUN go build -o server ./main.go
#6. 애플리케이션 실행
CMD ["/app/server"]

Friend 서비스의 경우에도 Go 언어에 맞는 이미지 다운로드 및 의존성 설치 이기 때문에 추가적인 설명 없이 넘어가도록 한다.

 

2.5 Front 의 Dockerfile

#1. 실행용 이미지 설정
FROM node:20.16.0-slim
#2. 작업 디렉토리 설정
WORKDIR /frontend
#3. 소스 코드 복사
COPY . .
#4. .expo 디렉토리 생성 및 권한 설정
RUN mkdir -p /frontend/.expo && chmod -R 777 /frontend
#5. 기존 node_modules 삭제 및 npm 캐시 정리 후 의존성 설치
RUN rm -rf node_modules && \
npm cache clean --force && \
npm install
#6. 환경 변수 설정
ENV EXPO_HOME=/frontend/.expo
ENV HOME=/frontend
#7. 애플리케이션 실행
CMD ["npm","start"]

Front 의 경우 웹 앱을 목표로 개발하였기 때문에 EXPO 를 사용하기로 했다. (프론트 개발을 담당한 팀원이 선택) 다만 EXPO 는 다른 서비스와 다르게 빌드 과정이 조금 복잡했다. #1, #2, #3, #7 의 경우 다른 Dockerfile 과 동일하기 때문에 넘어가도록 한다.

4. .expo 디렉토리 생성 및 권한 설정

  • Expo 프로젝트의 설정 파일을 저장할 .expo 디렉토리를 생성한다. -p 옵션을 활용하여 디렉토리가 존재하지 않을 경우 자동 생성되게 하였다. 
  • /fronted 디렉토리의 모든 파일과 폴더의 권한을 777로 변경하여 모든 사용자가 이 디렉토리에 대한 접근 및 수정이 가능하도록 설정하였다. USER root 를 활용하여 root 권한을 줘서 접근할 수 있게 했지만 권한 접근 문제가 발생하여 모두에게 접근이 가능하도록 하였다.

5. 기존 node_modules 삭제 및 npm 캐시 정리 후 의존성 설치

  • 기존의 node_modules 폴더를 완전히 삭제하여 의존성을 초기화 하였다. npm install 보다는 ci 를 사용하는 것이 더 빠르고 일관된 의존성 설치를 보장한다고 하나, 빌드시 의존성 충돌이 발생하여 초기화 후 재설치 방식으로 의존성 문제를 해결하였다.

6. 환경 변수 설정

  • Expo 는 실행시 특정 디렉토리를 사용하기 때문에 명확한 환경 변수를 설정하여 불필요한 경로 문제를 방지하였다. 해당 환경변수를 명확히 하지 않을 경우 .expo 를 찾지 못해 실행시 오류가 발생하였다. 따라서 반드시 경로를 지정해줘야 했다.

3. 특이사항

User 와 Alert 서비스의 경우 베이스 이미지로 먼저 설정하여 의존성을 설치하고 이후에 slim 모델을 활용하여 최종 베이스 이미지를 설정하게 된다. 이 때 빌드된 JAR 파일의 경로를 찾는 방법에 대해 작성해보도록 한다.

기본적으로 Gradle 빌드 결과물은 프로젝트 디렉토리 안에 build/libs/폴더가 생성되고, .jar 또는 .war 파일이 여기에 저장된다. Gradle 빌드 후 JAR 파일의 실제 경로 확인 방법은 로컬에서 직접 빌드 후 확인할 수 있는데, 필자의 경우 팀원이 개발한 프로젝트이다 보니 직접 빌드하여 확인하는 방법은 다소 귀찮은 방법이다. 물론 담당한 서비스의 팀원에게 프로젝트 명을 물어봐도 되지만 그렇지 못할 경우의 방법이다.

GitHub 에 들어가서 build.gradle settings.gradle 을 확인하면 해당 경로를 유추하여 조합할 수 있다. 

User 에서 Build 후에 이미지 복사를 위한 경로 설정을 확인해보면 다음과 같다.  /app/build/libs/User-0.0.1-SNAPSHOT.jar

해당 경로는 (작업한 디렉토리)/build/libs/(settings.gradle 에서의 프로젝트 명)-(build.gradle 에서의 version).jar 로 생성되는 것을 확인할 수 있다.


4. 마치며

이것으로 배포 과정의 일부를 경험해보는 시간을 가져보았다. 배포 과정이 꽤나 까다롭고 복잡한 것을 매번 느끼게 된다. 하지만 하나씩 알아갈 수록 점점 재미를 느껴가는 것 같다. 앞으로도 배포를 담당하게 될 예정이다. 계속해서 배포하면서 발생하는 오류와 이를 해결하는 과정들을 적도록 하겠다.