
이번 포스팅에서는 프로젝트를 진행하면서 사용한 docker의 기본적인 사용방식에 대해 설명하고자 한다.
docker와 kubernates에는 많은 기능이 있어 공부 할것이 많기에 기본적인 요소들 부터 다루고자 한다.
왜 Docker를 사용할까?
그 이유는 독립되고 표준화된 애플리케이션 패키지를 원하기 때문이다.

다음과 같이 개발 환경이 달라져버리는 순간 애플리케이션이 환경이 달라져서 실행이 안될수 있다!
소프트웨어 개발 과정에서는 환경 차이로 인해 다양한 문제가 발생하므로 동일한 환경을 제공하기 위해서 docker를 사용하는것이다.
| 문제 상황 | Docker로 해결 |
|---|---|
| 개발 환경과 운영 환경 차이 | 동일 이미지로 실행 |
| 라이브러리 버전 충돌 | 컨테이너 내부에서 관리 |
| 실행 환경 재구성 어려움 | 이미지 기반 배포 |
Docker를 사용하면 실행 환경 자체를 패키징하기 때문에 어디서 실행하든 동일한 결과를 얻을 수 있다.
그렇다면 docker에서 컨테이너와 이미지라는 개념을 통해서 패키징을 하는데 이것은 무엇일까?
컨테이너는 애플리케이션과 실행에 필요한 환경을 함께 묶은 단위이다.
예를 들어,
이 모든 것이 하나의 컨테이너 안에 포함된다.
그렇다면 이미지란 무엇일까?
docker에서는 다음과 같은 과정으로 컨테이너가 만들어지게 된다.

-즉 docker 이미지란? 실행 환경이 담긴 템플릿 (빌드 결과물), 컨테이너를 만들기 위한 설계도 라고 생각하면 된다.
-그리고 docker 이미지를 만들기 위한 스크립트를 dockerfile 이라도 한다. dockerfile은 도커 이미지를 자동으로 빌드 하기 위한 지침서 역할을 한다. '
그렇다면 docker file 작성 명령어에 대해서 알아보도록 하자
베이스 이미지를 지정한다. 도커 이미지가 어떤 기본 이미지를 사용할지 명시한다.
FROM ubuntu:20.04
이미지 빌드 과정에서 명령어를 실행한다. 패키지를 설치하거나 스크립트를 실행할 때 사용한다.
RUN apt-get update
파일을 호스트에서 컨테이너로 복사한다. 로컬 파일을 도커 이미지로 복사할 때 사용한다.
COPY ./myapp /app
컨테이너 시작 시 실행할 기본 명령어를 설정한다. 컨테이너가 실행될 때 기본적으로 실행되는 명령어이다.
CMD ["python3", "/app/main.py"]
⚠️ 주의사항: CMD는 컨테이너 실행 시의 기본 명령어로, 여러 개 작성할 수 있지만 마지막으로 작성된 명령어만 실행된다.
작업 디렉토리를 설정합니다. 이미지 내에서 작업할 기본 디렉토리를 지정합니다.
WORKDIR /app
컨테이너에서 사용할 포트를 지정한다. 실제로 포트가 열리는 것은 아니다.
📌 문서화용 힌트일 뿐, 실제로 포트를 열려면
docker run시-p옵션을 사용해야 합니다.
EXPOSE 8080
환경 변수를 설정합니다. 컨테이너 내부에서 사용할 환경 변수를 설정합니다.
ENV APP_ENV=production
멀티 스테이지 빌드는 빌드용 컨테이너와 실행용 컨테이너를 분리하여 최종 이미지 크기를 줄이는 방식이다.
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
# Gradle Wrapper 스크립트 먼저 복사 (Docker 캐시 활용)
COPY gradlew .
COPY gradle/ gradle/
COPY build.gradle .
COPY settings.gradle .
# 실행 권한 부여
RUN chmod +x gradlew
# 의존성 다운로드 (소스 변경 시 재다운로드 방지)
RUN ./gradlew dependencies --no-daemon -q
# 소스 코드 복사 후 빌드
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test
Stage 1 포인트
AS builder로 이름 지정)chmod +x gradlew: Gradle Wrapper 실행 권한 부여chmod +x 후 git에서 clone 시 실행 권한이 없을 수 있으므로 필요-x test: 테스트 생략 (빌드 시간 단축, Windows에서 Gradle 실행 시 권한 이슈 방지)FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 빌드된 JAR 파일만 복사
COPY --from=builder /app/build/libs/*.jar app.jar
# 8080 포트 노출
EXPOSE 8080
# 애플리케이너 실행 (실행 가능한 환경 변수와 함께 최종 주 명령어 지정)
ENTRYPOINT ["java", "-jar", "app.jar"]
Stage 2 포인트
--from=builder: Stage 1(builder)에서 빌드된 파일만 가져옴EXPOSE 8080: 포트 번호 문서화 (docker run 시 -p 옵션으로 실제 포트 연결 필요)ENTRYPOINT: 컨테이너가 항상 실행할 명령어 지정# ---- Stage 1: Build ----
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY gradlew .
COPY gradle/ gradle/
COPY build.gradle .
COPY settings.gradle .
RUN chmod +x gradlew
RUN ./gradlew dependencies --no-daemon -q
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test
# ---- Stage 2: Run ----
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
앞에서 언급한 FROM은 베이스 이미지를 선정하여 주는 명령어이다. 베이스 이미지란 무엇일까? 이것은 Docker의 구조와 밀접하게 관련이 되어있다.
베이스 이미지는 도커 이미지를 만들기 위한 기반이 되는 이미지다.
-도커 이미지는 여러 개의 레이어로 구성되며, 베이스 이미지는 가장 하단의 레이어를 담당한다
도커 이미지를 만들기 위한 기본 환경을 제공한다
Docker Hub에서 다운로드 받을 수 있다
-도커 이미지 레이어 (Docker Image Layer)
레이어 구조
도커 이미지(Docker Image)는 여러 개의 읽기 전용 레이어로 구성된다.
-FROM, RUN, COPY 등 각 명령어가 실행될 때마다 새로운 레이어가 생성됨
레이어는 기본적으로 캐시되어 재사용 가능 → 이미 존재하는 레이어는 다시 다운로드할 필요가 없음
Container Layer ← Read & Write (컨테이너 실행 시 추가되는 레이어)
─────────────────────────────────
Layer 1: RUN yum install -y wget ← Read Only
─────────────────────────────────
Base Layer: FROM centos:7 ← Read Only
레이어와 컨테이너의 관계
이미지 레이어 vs 컨테이너 레이어
┌─────────────────────┐
│ Container Layer │ ← Read & Write (컨테이너 실행 시 생성)
├─────────────────────┤
│ Image Layer │ ← Non-Writeable (읽기 전용)
│ Image Layer │
│ Image Layer │
├─────────────────────┤
│ Base Layer │ ← Non-Writeable (읽기 전용)
└─────────────────────┘
이미지 레이어 위에 Container 층이 쌓이는 구조
이미지 층은 수정 불가 (Non-Writeable)
컨테이너 층만 읽기/쓰기 가능
즉, 이미지는 수정이 안 되고 컨테이너들만 차례대로 쌓이는 구조다.
비효율적인 부분은 모두 copy가 된다 (Copy-on-Write 방식)
그렇다면 마지막으로 이미지와 컨테이너를 다루는 명령어에 대해서 알아보자
# 이미지 목록 보기
docker images
# 이미지 빌드 (Dockerfile 있는 디렉토리에서)
docker build -t 이미지명:태그 .
# 이미지 삭제 -> 삭제는 docker desktop에서 관리해서 지우는게 더 편하기는 하다.
docker rmi 이미지명
# Docker Hub에서 이미지 받기
docker pull mysql:8.0
# 컨테이너 실행
docker run -d -p 8080:8080 --name 컨테이너이름 이미지명
# 실행 중인 컨테이너 목록
docker ps
# 모든 컨테이너 목록 (중지된 것 포함)
docker ps -a
# 컨테이너 중지
docker stop 컨테이너이름
# 컨테이너 시작
docker start 컨테이너이름
# 컨테이너 재시작
docker restart 컨테이너이름
# 컨테이너 삭제 -> 삭제는 docker desktop에서 관리해서 지우는게 더 편하기는 하다.
docker rm 컨테이너이름
# 컨테이너 로그 보기
docker logs 컨테이너이름
docker logs -f 컨테이너이름 # 실시간 (-f = follow)
# 컨테이너 내부 접속 (bash 셸)
docker exec -it 컨테이너이름 bash
| 옵션 | 설명 |
|---|---|
-d | 백그라운드 실행 |
-p 8080:8080 | 호스트포트:컨테이너포트 연결 |
--name | 컨테이너 이름 지정 |
-e | 환경변수 설정 |
-v | 볼륨(폴더) 마운트 |
--rm | 종료 시 자동 삭제 |
--network | 네트워크 지정 |
컨테이너 내부 애플리케이션은 특정 포트에서 실행되고(예: 3000), docker run -p 옵션으로 호스트 포트와 매핑한다.
docker run -p 8080:3000 myapp
# → 브라우저에서 localhost:8080 접속 시, 컨테이너 내부의 3000 포트로 전달
형식:
docker run -p <호스트포트>:<컨테이너포트>
Docker에서는 컨테이너를 실행할 때 Detached, Attached, Interactive 세 가지 모드로 실행할 수 있다.
-d)docker run -d 이미지명
docker run 명령어는 Attached 모드로 실행됨docker run 이미지명
-it)-i는 입력, -t는 터미널을 의미docker run -it 이미지명
# 컨테이너 목록 확인
docker ps -a
# (Optional) 컨테이너 중지
docker stop container_id
# 컨테이너 삭제
docker rm container_id
# 컨테이너 모두 삭제
docker rm $(docker ps -a -q)
Docker Desktop에서도 삭제 가능 (복잡하지 않으니 Desktop 사용 추천)
# 이미지 목록 확인
docker images
# 이미지 삭제
docker rmi image_id
# 이미지 모두 삭제
docker rmi $(docker images -q)
⚠️ 주의: 이미지를 삭제할 때는 해당 이미지를 사용하는 모든 컨테이너를 먼저 삭제해야 한다.
컨테이너가 이미지 위에 쌓인 레이어층이기 때문이다.
Docker 이미지는 이름:태그 형식으로 관리된다.
# 빌드 시 이름(tag) 지정
docker build -t mynode:1 .
# 실행 시 이름으로 실행 가능
docker run -p 3000:3000 mynode:1
mynode:1 → 이름 = mynode, 태그 = 1 (버전 1)mynode:2 → 이름 = mynode, 태그 = 2 (버전 2)mynode:latest → 태그 생략 시 기본값은 :latest💡 Tip:
docker run mynode=docker run mynode:latest
운영 환경에서는latest대신 특정 버전 태그를 쓰는 것이 안전하다.
태그를 사용하면 여러 버전을 동시에 관리할 수 있고, ID 대신 이름을 사용하면 명령어가 간단하고 직관적이다.
app.py 작성print("Hello World from Docker!")
Dockerfile 작성FROM python:3.12-slim
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]
docker build -t py-hello .
docker run --rm py-hello
Hello World from Docker!