본문 바로가기
Deployment/CICD

Docker file과 docker-compose, 명령어

by curious week 2025. 12. 29.

Dockerfile

Dockerfile은 “프로젝트를 실행 가능한 컨테이너로 만드는 레시피”

  • 어떤 OS를 쓰고
  • 어떤 언어 런타임을 깔고
  • 어떤 명령으로 빌드하고
  • 어떤 명령으로 실행할지

순서대로 적은 파일


기본 문법

FROM        # 기반 이미지
WORKDIR    # 작업 디렉토리
COPY        # 파일 복사
RUN         # 빌드/설치 명령
ENV         # 환경변수
CMD         # 컨테이너 시작 명령

원칙 1: 멀티 스테이지 빌드

  • 빌드용 이미지 ≠ 실행용 이미지
  • 결과물만 복사

원칙 2: 캐시 잘 타게 COPY 순서 분리

COPY package.json package-lock.json ./
RUN npm install
COPY . .

원칙 3: 실행 이미지는 최대한 가볍게

  • JDK ❌ → JRE ⭕
  • node_modules ❌ → build 결과만 ⭕

Dockerfile 예시

Java Spring Boot Dockerfile

# ---------- build stage ----------
FROM gradle:8.4-jdk17 AS build
WORKDIR /home/gradle/src

# 의존성 캐시
COPY build.gradle settings.gradle ./
COPY gradle gradle
RUN gradle build -x test --no-daemon || true

# 소스 빌드
COPY src src
RUN gradle bootJar --no-daemon

# ---------- runtime stage ----------
FROM eclipse-temurin:17-jre
WORKDIR /app

# 빌드 결과만 복사
COPY --from=build /home/gradle/src/build/libs/*.jar app.jar

EXPOSE 8080

# 실행
ENTRYPOINT ["java", "-jar", "app.jar"]

Next.js Dockerfile (SSR 포함)

# ---------- deps ----------
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# ---------- build ----------
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# ---------- runtime ----------
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules

EXPOSE 3000
CMD ["npm", "start"]

React + Vite Dockerfile

# ---------- build ----------
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---------- runtime ----------
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

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

Nginx

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY dist/ /usr/share/nginx/html

Nginx의 경우, Dev환경에서는 docker-compose volume으로 빠르게 고치고 즉시 확인하면 되고 Prod 환경에서는 Dockerfile로 설정/정적파일을 이미지에 포함, 배포 재현성/안정성 확보하는 게 좋다.


Docker compose

여러 컨테이너(서비스)를 한 번에 실행하고, 서로 연결하고, 환경변수/볼륨/포트/헬스체크를 관리하는 실행 설계도.
Dockerfile이 “이미지 레시피”라면, compose는 “서비스 배치도”라고 할 수 있다.


 

1) Compose 파일의 큰 구조

  • name: 프로젝트 이름(컨테이너/네트워크/볼륨 prefix에 영향)
  • services: 컨테이너 정의의 핵심
  • volumes: named volume 선언
  • networks: 네트워크 선언(커스텀 가능)
  • configs, secrets: 운영에서 유용(특히 Swarm/일부 환경)

Compose v2에서는 version:을 안 쓰는 게 일반적(현대 Compose는 스키마 자동 추론).

name: joined-dev

services:
  db:
    image: postgres:16
    container_name: joined-db
    environment:
      POSTGRES_DB: joined
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - joined_pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d joined"]
      interval: 5s
      timeout: 3s
      retries: 20

2) services 아래 필수 키 완전 정리

A. 이미지/빌드

image

  • 어떤 이미지를 실행할지
  • 예: postgres:16, nginx:alpine, myorg/api:dev

build

  • Dockerfile로부터 이미지를 빌드해서 실행
  • 형태 1: 문자열
    • build: ./apps/api
  • 형태 2: 객체
    • context: 빌드 컨텍스트(폴더)
    • dockerfile: Dockerfile 경로
    • target: 멀티 스테이지 빌드의 특정 stage
    • args: 빌드 ARG 주입(런타임 env랑 다름)
    • cache_from: 빌드 캐시 활용(고급)
sevices:
    build:
      context: ./apps/api
      dockerfile: Dockerfile
      target: runtime
      args:
        APP_VERSION: "dev"

pull_policy

  • image를 언제 pull할지(환경에 따라 지원 범위 다름)
  • dev에서 항상 최신이 필요할 때 유용

B. 실행 명령

command

  • 이미지 기본 CMD를 덮어씀
  • 예: command: ["npm","run","dev"]

entrypoint

  • 이미지의 ENTRYPOINT를 덮어씀(주의: 디버깅용 외엔 최소화)

C. 환경 변수 / 설정 주입

environment

  • key:value로 런타임 env 주입
environment:
  SPRING_PROFILES_ACTIVE: dev
  LOG_LEVEL: info

env_file

  • 파일에서 env를 읽어 주입
env_file:
  - .env
  - .env.local

중요: .env는 compose “변수 치환용”으로도 쓰일 수 있어서 혼동되기 쉬움.

  • ${VAR} 치환에 쓰이는 .env 파일과 컨테이너 런타임에 주입되는 env_file 이 둘이 겹칠 수 있다.

secrets, configs

  • 민감값/설정파일을 “파일로” 주입하고 싶을 때(운영에서 깔끔함) 하지만 로컬 dev에서는 보통 .env로 충분

D. 포트/네트워크

ports

  • 호스트:컨테이너 포트 공개
  • "8080:8080", "127.0.0.1:8080:8080" 같이 바인딩도 가능
  • 프로토콜 지정 가능: "8080:8080/tcp"

expose

  • “내부 네트워크에서만” 포트를 문서화/노출
  • 실제 공개는 아님. 내부 통신만이면 생략해도 됨(대부분은 생략)

networks

  • 서비스가 붙을 네트워크 지정
  • 기본 네트워크만으로 충분하지만, “프론트-백만 연결”, “DB는 내부 전용” 같은 분리가 필요하면 커스텀 네트워크를 만든다.

E. 스토리지

volumes (서비스 아래)

  • named volume 또는 bind mount 마운트
  • :ro(read-only) 적극 권장
volumes:
  - dbdata:/var/lib/postgresql/data
  - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro

F. 의존/시작/헬스체크

depends_on

  • 기본은 “시작 순서”
  • compose v2에서 condition: service_healthy가 되는 경우가 많지만, 환경마다 다르고 “완벽 보장”은 아니니 healthcheck + 앱 레벨 재시도도 같이 고려.

healthcheck (매우 중요)

컨테이너가 “정말 준비됐는지”를 판정한다.

  • test: 실행할 명령
  • interval: 검사 주기
  • timeout: 타임아웃
  • retries: 실패 허용 횟수
  • start_period: 초기 부팅 시간(초반 실패를 봐주는 유예)
healthcheck:
  test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
  interval: 5s
  timeout: 3s
  retries: 30
  start_period: 10s

주의:

  • curl이 없는 이미지가 많다 → wget/nc/앱 자체 헬스 명령으로.
  • DB는 pg_isready, mysqladmin ping 같은 전용 도구가 안정적.

G. 재시작/복구

restart

  • no(기본), always, unless-stopped, on-failure[:n]
  • 실무 dev/prod에서 흔한 선택:
    • dev: 보통 기본(no) 또는 unless-stopped
    • prod 단일 VM: unless-stopped 많이 씀

H. 리소스 제한

Compose 단일 호스트에서도 리소스 제한은 가능(도커 엔진 의존)

deploy

  • 원래는 Swarm 중심 옵션이었는데, 일부는 compose에서도 부분 적용되거나 무시될 수 있음.
  • 단일 VM compose에서 확실히 쓰려면 mem_limit 같은 구형 키를 쓰기도 했지만, 요즘은 환경별로 차이가 있어서 “정확히 적용되는지” 확인이 필요.

실무 팁:

  • 로컬 dev에서는 너무 과한 제한은 오히려 개발을 방해함.
  • 운영은 Kubernetes나 별도 제한 정책을 쓰는 편이 많음.

I. 실행 사용자/권한/보안

user

  • 컨테이너 프로세스를 어떤 UID로 돌릴지
  • 권한 문제 해결에 유용

read_only

  • 컨테이너 파일시스템을 읽기 전용으로(보안 강화)

cap_drop, security_opt

  • 고급 보안 옵션(필요할 때만)

J. 로그

logging

  • 로그 드라이버/옵션 설정
  • 로컬에서는 기본 json-file로도 충분하지만, 운영에선 회전 설정이 중요할 때가 있음.

3) “앵커(&) / 병합(<<) / 확장(x-*)” 제대로 알기

Compose에서 중복을 줄이는 핵심

YAML anchor & merge (<<)

공통 설정을 재사용할 때 쓴다.

x-common-env: &common-env
  TZ: Asia/Seoul
  LOG_LEVEL: info

services:
  api:
    environment:
      <<: *common-env
      SPRING_PROFILES_ACTIVE: dev
  • &common-env로 정의
  • *common-env로 참조
  • <<:는 “병합(merge)” 키

x-확장 필드

Compose가 무시하는 “커스텀 섹션”이라 재사용 블록을 둘 때 좋다.

예: x-env-file 같은 것도 가능(Compose가 무시하고 YAML 재사용만 돕는 용도)

x-env-files: &env-files
  - .env
  - .env.local

services:
  api:
    env_file: *env-files

4) 네트워크/볼륨 전역 섹션

volumes (전역)

named volume 선언

volumes:
  dbdata:

드라이버나 옵션을 줄 수도 있음(고급).

networks (전역)

서비스 분리/격리할 때 사용

networks:
  front:
  back:

services:
  nginx:
    networks: [front, back]
  api:
    networks: [back]
  db:
    networks: [back]

이렇게 하면 db는 front 네트워크에 안 붙어서 외부쪽 서비스가 직접 접근하기 어려움(구조적으로 안전).


실전 예제 Postgresql + Spring Boot + Next.js + NginX

# infra/docker-compose.yml
#
# 실행:
#   docker compose -f infra/docker-compose.dev.yml up --build

name: project

services:
  # ------------------------------------------------------------
  # 1) DB: Postgres
  # ------------------------------------------------------------
  db:
    image: postgres:16
    container_name: project-db

    # (A) 런타임 환경변수: Postgres 초기 DB/계정 생성에 사용
    environment:
      POSTGRES_DB: project_db
      POSTGRES_USER: project_user
      POSTGRES_PASSWORD: project_password

    # (B) 데이터 영속화: 컨테이너가 삭제되어도 DB 데이터는 남음(named volume)
    volumes:
      - project_pgdata:/var/lib/postgresql/data

    # (C) 외부 노출(선택): 로컬에서 psql, DBeaver로 접근하려면 열어둠
    #     내부 통신만이면 ports를 제거해도 됨
    ports:
      - "5432:5432"

    # (D) 준비 완료(ready) 상태를 healthcheck로 판정
    #     - pg_isready는 postgres 이미지에 기본 포함
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U project_user -d project_db"]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 5s

    # (E) 네트워크: 기본 네트워크에 붙어도 되지만, 예제에서는 분리해서 보여줌
    networks:
      - back

  # ------------------------------------------------------------
  # 2) Backend: Spring Boot API
  # ------------------------------------------------------------
  backend:
    # (A) build: Dockerfile로 이미지를 빌드해서 실행
    #     context는 "빌드할 소스의 루트 폴더"를 의미
    build:
      context: ./backend        # <-- 네 repo 구조에 맞게 수정 (예: ./apps/api)
      dockerfile: Dockerfile

    container_name: project-backend

    # (B) depends_on: "시작 순서" + (가능한 경우) 건강상태(health) 기준
    #     - DB가 healthy 되기 전 backend가 뜨지 않도록 함
    depends_on:
      db:
        condition: service_healthy

    # (C) 런타임 환경변수: DB 주소는 "서비스 이름(db)"로 접근한다
    #     - 컨테이너 내부에서 localhost는 "자기 자신"이므로 DB에 접근 불가
    environment:
      # Spring Boot 기본 예시 (프로젝트에 맞게 키 이름/설정은 조정)
      SPRING_PROFILES_ACTIVE: dev

      # JDBC URL은 db 컨테이너로: jdbc:postgresql://db:5432/...
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/project_db
      SPRING_DATASOURCE_USERNAME: project_user
      SPRING_DATASOURCE_PASSWORD: project_password

      # 예: 서버 포트 (Spring이 8080을 쓰는 경우가 많음)
      SERVER_PORT: "8080"

    # (D) 외부 노출(선택): 로컬에서 backend 직접 호출하고 싶으면 열어둠
    #     - nginx가 gateway면 보통은 backend ports를 닫아도 됨
    ports:
      - "8080:8080"

    # (E) healthcheck: backend가 실제로 요청을 받을 준비가 됐는지 확인
    #     - /actuator/health를 쓰려면 Spring Actuator가 필요
    #     - 없다면 프로젝트에 맞는 헬스 엔드포인트(/health 등)로 바꿔
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/actuator/health | grep -q UP"]
      interval: 5s
      timeout: 3s
      retries: 30
      start_period: 15s

    networks:
      - back

  # ------------------------------------------------------------
  # 3) Frontend: Next.js
  # ------------------------------------------------------------
  frontend:
    build:
      context: ./frontend       # <-- 네 repo 구조에 맞게 수정 (예: ./apps/editor)
      dockerfile: Dockerfile

    container_name: project-frontend

    # (A) frontend는 보통 backend가 먼저 떠 있어도 되고,
    #     SSR에서 API 호출을 강제하면 backend health를 기다리는 편이 안전
    depends_on:
      backend:
        condition: service_healthy

    # (B) 런타임 환경변수
    #     - Next는 빌드 타임/런타임 env 경계가 있으니 주의
    #     - 여기서는 "런타임 서버"가 참조할 값을 예시로 둠
    environment:
      NODE_ENV: development

      # 예: Next 서버에서 백엔드를 호출할 때 내부 DNS로 접근(backend:8080)
      # (실제 사용 키는 프로젝트에 맞게: API_BASE_URL, NEXT_PUBLIC_API_BASE_URL 등)
      API_BASE_URL: http://backend:8080

      # Next 서버 포트
      PORT: "3000"

    ports:
      - "3000:3000"

    # (C) healthcheck: Next 서버가 응답 가능한지 확인
    #     - / 는 페이지가 준비된 경우 200을 반환
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/ >/dev/null 2>&1 || exit 1"]
      interval: 5s
      timeout: 3s
      retries: 30
      start_period: 15s

    networks:
      - front
      - back

  # ------------------------------------------------------------
  # 4) Nginx Gateway
  # ------------------------------------------------------------
  nginx:
    image: nginx:alpine
    container_name: project-nginx

    # (A) nginx는 외부에서 들어오는 진입점(게이트웨이)
    #     - 로컬에서는 80 포트를 쓰면 편함
    ports:
      - "80:80"

    # (B) dev에서는 nginx.conf를 volume으로 마운트하는 방식이 편함
    #     - 설정을 수정해도 이미지 rebuild 없이 바로 반영 가능
    volumes:
      - ./infra/nginx/nginx.dev.conf:/etc/nginx/nginx.conf:ro

    # (C) nginx는 backend/frontend가 떠 있어야 정상 프록시가 됨
    depends_on:
      backend:
        condition: service_healthy
      frontend:
        condition: service_healthy

    networks:
      - front
      - back

# ------------------------------------------------------------
# Named Volumes: 컨테이너가 삭제되어도 데이터 유지
# ------------------------------------------------------------
volumes:
  project_pgdata:

# ------------------------------------------------------------
# Networks: 분리해두면 구조가 명확해짐
# - front: 외부 진입(nginx, frontend)
# - back : 내부 통신(backend, db)
# ------------------------------------------------------------
networks:
  front:
  back:

NginX는 위 파일과 같은 이름으로 경로를 맞춰줘야한다.

# infra/nginx/nginx.dev.conf
#
# upstream을 "서비스 이름"으로 잡는다 (frontend, backend)
# compose 내부 DNS가 알아서 연결해준다

events {}

http {
  server {
    listen 80;

    # ------------------------------------------------------------
    # API 라우팅: /api/* → backend:8080
    # ------------------------------------------------------------
    location /api/ {
      proxy_pass http://backend:8080/;   # backend 서비스로 전달
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }

    # ------------------------------------------------------------
    # Frontend 라우팅: / → frontend:3000
    # ------------------------------------------------------------
    location / {
      proxy_pass http://frontend:3000/;  # frontend(Next) 서비스로 전달
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }
  }
}

도커 명령어

# ------------------------------------------------------------
# 1) 이미지 관련
# ------------------------------------------------------------

# Dockerfile → 이미지 빌드
docker build -t project-backend:dev .

# 로컬에 있는 이미지 목록
docker images

# 이미지 내려받기
docker pull nginx:alpine

# 이미지 삭제
docker rmi project-backend:dev


# ------------------------------------------------------------
# 2) 컨테이너 실행 / 상태
# ------------------------------------------------------------

# 이미지 하나를 컨테이너로 실행 (compose 없이)
docker run -d -p 8080:8080 project-backend:dev

# 실행 중인 컨테이너 목록
docker ps

# 모든 컨테이너 목록 (중지 포함)
docker ps -a

# 컨테이너 중지 / 시작 / 재시작
docker stop project-backend
docker start project-backend
docker restart project-backend

# 컨테이너 삭제
docker rm project-backend
docker rm -f project-backend   # 강제 삭제


# ------------------------------------------------------------
# 3) 로그 / 디버깅 (가장 중요)
# ------------------------------------------------------------

# 컨테이너 로그 출력
docker logs project-backend

# 실시간 로그 추적
docker logs -f project-backend

# 컨테이너 내부로 진입 (bash)
docker exec -it project-backend bash

# alpine 기반 이미지일 경우
docker exec -it project-backend sh

# 컨테이너 상세 정보(JSON)
docker inspect project-backend


# ------------------------------------------------------------
# 4) 네트워크 / 볼륨
# ------------------------------------------------------------

# 네트워크 목록
docker network ls

# 특정 네트워크 상세
docker network inspect project_default

# 볼륨 목록
docker volume ls

# 볼륨 상세
docker volume inspect project_pgdata


# ------------------------------------------------------------
# 5) Docker Compose (프로젝트 단위)
# ------------------------------------------------------------

# 전체 서비스 실행
docker compose -f infra/docker-compose.dev.yml up

# 이미지까지 다시 빌드해서 실행
docker compose -f infra/docker-compose.dev.yml up --build

# 백그라운드 실행
docker compose -f infra/docker-compose.dev.yml up -d

# 서비스 상태 확인
docker compose -f infra/docker-compose.dev.yml ps

# 전체 서비스 로그
docker compose -f infra/docker-compose.dev.yml logs

# 특정 서비스 로그
docker compose -f infra/docker-compose.dev.yml logs -f backend

# 특정 서비스 컨테이너 내부 진입
docker compose -f infra/docker-compose.dev.yml exec backend bash
docker compose -f infra/docker-compose.dev.yml exec frontend bash
docker compose -f infra/docker-compose.dev.yml exec nginx sh
docker compose -f infra/docker-compose.dev.yml exec db bash

# DB 컨테이너에서 psql 접속
docker compose -f infra/docker-compose.dev.yml exec db \
  psql -U project_user -d project_db

# 서비스 재시작
docker compose -f infra/docker-compose.dev.yml restart backend

# 전체 서비스 중지 + 네트워크 정리
docker compose -f infra/docker-compose.dev.yml down

# 전체 서비스 중지 + 네트워크 + 볼륨 삭제 (DB 데이터 삭제 ⚠️)
docker compose -f infra/docker-compose.dev.yml down -v


# ------------------------------------------------------------
# 6) 정리 / 청소 (주의해서 사용)
# ------------------------------------------------------------

# 사용하지 않는 컨테이너 삭제
docker container prune

# 사용하지 않는 이미지 삭제
docker image prune

# 사용하지 않는 모든 리소스 삭제 (최후 수단 ⚠️)
docker system prune -a

CLI 옵션(flag)

 -f  : file
# 특정 파일을 사용 (기본 docker-compose.yml 대신)
# 예) docker compose -f infra/docker-compose.dev.yml up

 -d  : detached
# 백그라운드 실행 (터미널 반환)
# 예) docker compose up -d

 -a  : all
# 모든 대상 포함 (중지된 컨테이너 등)
# 예) docker ps -a

 -t  : tag / tty (명령어에 따라 의미 다름)
# docker build -t  → 이미지 이름:태그 지정
# docker exec -t   → 터미널(tty) 할당

 -i  : interactive
# 표준 입력 유지 (사람이 직접 입력 가능)
# 예) docker exec -i backend bash

 -it : interactive + tty (거의 항상 세트)
# 컨테이너 내부에서 쉘 사용
# 예) docker exec -it backend bash

 -p  : publish
# 포트 매핑 (호스트:컨테이너)
# 예) docker run -p 8080:8080 app

 -v  : volume
# 볼륨 또는 바인드 마운트
# 예) docker run -v ./data:/data app

 --rm
# 컨테이너 종료 시 자동 삭제
# 예) docker run --rm app

 -q  : quiet
# 결과를 ID 등 최소 출력으로
# 예) docker ps -q

 --build
# 실행 전 이미지 강제 빌드
# 예) docker compose up --build

 --no-cache
# 빌드 캐시 사용 안 함
# 예) docker build --no-cache .

 --name
# 컨테이너 이름 지정
# 예) docker run --name my-app app

 

'Deployment > CICD' 카테고리의 다른 글

Nginx 파일 작성법  (1) 2026.01.02