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 |
|---|