Dockerfile 하나면 java 설치부터 jar 실행까지 전부 자동화됩니다. 처음 이 사실을 알았을 때 솔직히 이건 예상 밖이었습니다. 매번 서버에 접속해서 손으로 설치하던 게 파일 한 장으로 끝난다는 게 믿기지 않았거든요. 직접 써보고 나서야 "왜 진작 이걸 안 썼지"라는 생각이 들었습니다.

Dockerfile로 백엔드·프론트 이미지 직접 만들기
일반적으로 Docker는 MariaDB나 Nginx 같은 공식 이미지를 그냥 내려받아 쓰는 도구라고만 생각하는 분들도 있는데, 실제로 써보니 내가 만든 프로젝트 자체를 이미지로 패키징해서 배포하는 게 핵심이었습니다.
백엔드 기준으로 기존 배포 방식을 먼저 짚어보면, Spring Boot 프로젝트를 bootJar로 빌드해서 jar 파일을 만들고, 서버에 java를 직접 설치한 뒤 java -jar로 실행하는 흐름입니다. 이걸 컨테이너 안에서 그대로 재현하면 되는 거라 구조 자체는 어렵지 않습니다.
여기서 베이스 이미지(Base Image)란 컨테이너를 만들 때 시작점이 되는 이미지를 말합니다. 제가 백엔드에 사용한 베이스 이미지는 openjdk:17-jdk-slim인데, Docker Hub에서 java 실행 환경이 이미 세팅된 채로 제공되기 때문에 별도로 java를 설치하는 RUN 명령어를 쓸 필요가 없습니다. 이 점이 수동 배포와 비교했을 때 가장 체감 차이가 큰 부분이었습니다.
Dockerfile에서 자주 쓰는 명령어를 정리하면 다음과 같습니다.
- FROM: 베이스 이미지를 지정합니다. 모든 Dockerfile의 시작점입니다.
- COPY / ADD: 로컬 파일을 컨테이너 안으로 복사합니다. ADD는 압축 해제나 URL 다운로드 기능이 추가된 버전입니다.
- RUN: 이미지 빌드 시점에 실행할 명령어로, 패키지 설치 같은 환경 준비에 씁니다.
- CMD / ENTRYPOINT: 컨테이너가 실제로 실행될 때 동작할 명령어입니다. CMD는
docker run뒤에 명령어를 붙이면 덮어써지고, ENTRYPOINT는 덮어써지지 않고 유지됩니다. - ENV: 환경 변수를 설정하며, 기본값 명시와 문서화 역할을 동시에 합니다.
- EXPOSE: 컨테이너가 사용하는 포트를 선언하는 문서화 역할입니다.
프론트엔드는 npm run build로 dist 폴더를 만든 뒤 nginx 베이스 이미지 위에 얹는 구조입니다. 여기서 Nginx란 정적 파일 서빙과 리버스 프록시(Reverse Proxy) 기능을 제공하는 웹 서버로, SPA(Single Page Application) 배포에 사실상 표준처럼 쓰입니다. nginx.conf 파일에서 try_files $uri $uri/ /index.html 설정을 해두면 React나 Vue의 클라이언트 사이드 라우팅이 정상 동작합니다. 제가 직접 써봤는데 이 설정 한 줄 빠지면 새로고침할 때마다 404가 뜨는 황당한 상황이 생깁니다.
이미지 빌드는 docker build --tag 아이디/레포지토리:태그 . 형식으로 실행하고, 빌드된 이미지는 docker push로 Docker Hub에 올려두면 어느 서버에서든 내려받아 쓸 수 있습니다. Docker Hub란 GitHub처럼 이미지를 저장하고 공유하는 원격 레지스트리(Registry)입니다. 팀 프로젝트에서 이미지를 Hub에 올려두고 팀원들이 각자 pull 받아 쓰니 "내 로컬에서는 되는데 서버에서 안 된다"는 상황 자체가 거의 사라졌습니다.
docker-compose로 멀티 컨테이너 한 번에 관리하기
백엔드, 프론트, DB를 따로따로 docker run으로 실행하다 보면 금방 한계가 옵니다. 제 경험상 이건 좀 다릅니다. 컨테이너가 3개만 넘어가도 포트 번호, 네트워크 연결, 실행 순서를 일일이 챙기는 게 손이 너무 많이 갑니다.
docker-compose란 여러 컨테이너를 YAML 파일 하나로 정의하고 docker-compose up 명령어 한 번으로 전체를 한꺼번에 띄울 수 있는 도구입니다. 여기서 YAML 파일이란 들여쓰기 기반의 설정 파일 형식으로, 서비스별 이미지, 포트, 환경 변수, 볼륨, 의존 관계를 계층적으로 기술합니다.
특히 제가 직접 적용해보면서 효과를 확실히 느낀 부분은 DB 마스터-슬레이브 복제 구성입니다. 일반적으로 DB 레플리케이션(Replication)은 세팅하기 까다롭다고 알려져 있지만, 실제로 docker-compose와 쉘 스크립트를 조합하니 훨씬 수월했습니다. 여기서 레플리케이션이란 마스터 DB에 쓰인 데이터를 슬레이브 DB가 실시간으로 복제하는 구조로, 읽기 부하를 분산하거나 장애 대비용으로 활용합니다(출처: MariaDB 공식 문서).
master-init.sh에서 slave 전용 계정을 생성하고 복제 권한을 부여한 뒤, slave-init.sh에서 마스터의 바이너리 로그(Binary Log) 파일명과 포지션(Position)을 읽어와 CHANGE MASTER TO로 연결하는 방식입니다. 바이너리 로그란 MariaDB/MySQL이 데이터 변경 사항을 순서대로 기록하는 파일로, 슬레이브가 이 로그를 읽어 마스터와 동일한 상태를 유지하는 데 사용됩니다. depends_on 설정으로 마스터가 완전히 뜬 다음 슬레이브가 초기화되도록 순서를 잡아두면, 스크립트가 마스터에 접속 실패하는 문제를 방지할 수 있습니다.
MSA(Microservice Architecture) 구성에서는 docker-compose의 위력이 더 커집니다. Eureka 기반 서비스 디스커버리(Service Discovery)를 사용하는 환경에서 api-gateway, api-discovery, api-board, api-user 같은 서비스들을 하나의 docker-compose.yml로 묶어두면, 팀원 누구든 GitHub에서 클론한 뒤 명령어 한 줄로 전체 환경을 세팅할 수 있습니다. 여기서 서비스 디스커버리란 컨테이너 환경처럼 IP가 동적으로 바뀌는 상황에서 각 서비스가 서로를 이름으로 찾아 통신할 수 있게 해주는 메커니즘입니다(출처: Spring Cloud Netflix 공식 문서).
com.palantir.docker Gradle 플러그인을 활용하면 bootJar 빌드와 Docker 이미지 빌드를 하나의 태스크로 묶을 수도 있습니다. build.gradle에 docker 블록을 추가하고 tasks.bootJar.outputs.files를 연결해두면, Gradle 빌드 한 번에 jar 생성부터 이미지 태깅까지 자동으로 처리됩니다.
docker-compose를 써보고 나서 가장 크게 느낀 점은 "환경 세팅 문서를 따로 쓸 필요가 없다"는 겁니다. YAML 파일 자체가 문서이고, 그게 실제로 동작하는 인프라 코드가 됩니다. 이 부분은 일반적으로 생각하는 것보다 훨씬 실용적인 이점입니다.
결국 Dockerfile과 docker-compose의 조합은 "내 로컬에서만 되는 프로젝트"를 "어디서든 동일하게 동작하는 프로젝트"로 바꿔주는 핵심 도구입니다. 처음엔 설정 파일이 낯설게 느껴질 수 있지만, 한 번 구조를 잡아두면 그다음부터는 복사해서 수정하는 수준으로 활용할 수 있습니다. 멀티 모듈이나 MSA로 확장할 때도 이 기반이 탄탄하게 잡혀 있어야 흔들리지 않으니, 지금 배포 자동화를 고민 중이라면 Dockerfile부터 직접 작성해보시길 권합니다.