1. 도커가 해결하는 문제
도커는 애플리케이션을 실행에 필요한 런타임·라이브러리와 함께 이미지로 만들고, 그 이미지를 격리된 컨테이너 프로세스로 실행하는 도구입니다. 두 가지 개념만 잡으면 됩니다.
가상머신(VM)과 무엇이 다른지 궁금할 수 있는데, VM은 자체 커널과 OS를 포함하고 Linux 컨테이너는 호스트 커널을 공유합니다. 그래서 컨테이너가 일반적으로 더 가볍고 빠르게 시작합니다. 그렇다고 비용이 0이라는 뜻은 아닙니다. 네트워크 변환, 저장소 드라이버, 로그, Docker Desktop의 VM 계층 등에 오버헤드가 있을 수 있으므로 성능이 중요한 경로는 직접 측정해야 합니다. Docker의 컨테이너와 VM 설명
그래서 도커의 핵심 가치는 “배포 명령”보다 환경 재현에 있습니다. 서버의 Node 버전이나 시스템 라이브러리가 바뀌어 서비스가 깨지는 문제를 줄이고, 로컬·CI·개발·운영이 같은 산출물을 실행하게 합니다.
언제 사용하는가
| 상황 | 기본 선택 | 예외·고려사항 |
|---|---|---|
| DB·캐시가 필요한 로컬 개발 | 의존 서비스부터 Compose로 실행 | 앱은 IDE에서 실행해도 됩니다. |
| Next.js SSR/API/worker | 컨테이너가 적합 | 계속 실행되는 서버 프로세스가 있습니다. |
| HTML·JS·CSS만 생성하는 정적 사이트 | CDN·정적 호스팅 우선 | 고객사 납품 형식 통일이나 온프레미스 제약이 있으면 컨테이너도 가능합니다. |
| 한 서버의 소규모 서비스 | Compose로 충분한 경우가 많음 | 허용 장애시간, 운영 인력, 데이터 요구사항을 먼저 확인합니다. |
| 다중 노드·자동 확장·강한 고가용성 | 오케스트레이터 또는 관리형 플랫폼 검토 | 컨테이너 개수만으로 결정하지 않습니다. |
도커를 쓰면 비용이 드는가
Linux 서버의 Docker Engine은 오픈소스입니다. 다만 Docker Desktop은 회사 규모와 용도에 따라 유료 구독이 필요합니다. 현재 기준으로 직원 250명 미만 그리고 연 매출 미화 1,000만 달러 미만인 소규모 기업, 개인, 교육, 비상업 오픈소스 사용 등은 무료 범위이며 그 밖의 전문적 사용은 구독 조건을 확인해야 합니다. Docker Desktop 라이선스
도커 도입의 총비용은 아래 항목을 합쳐서 보면 됩니다.
월 총비용 = 서버/관리형 서비스 + 디스크·백업 + 레지스트리·전송량 + 로그·모니터링 + 도구 라이선스 + 운영 인력 시간
도커가 서버 비용을 자동으로 줄여주는 것은 아닙니다. 한 서버에 워크로드를 안전하게 모으고 환경 장애를 줄이면 절감될 수 있지만, 이미지 빌드·레지스트리·패치·백업·관측성 운영이 새 비용이 됩니다. 고정된 “월 2\~3만 원 차이” 대신 실제 트래픽, 저장량, 백업 보존기간, 장애 대응 인건비로 비교해야 합니다. 레지스트리의 private 저장소, pull 한도, 보안 스캔도 요금제별 조건이 다릅니다. Docker 요금제
2. 재현 가능한 이미지 만들기
변경했으면 다시 빌드한다
이미지는 빌드 시점의 스냅샷입니다. 코드나 Dockerfile을 바꿔도 이미 실행 중인 이미지에는 반영되지 않습니다. “수정했는데 반영되지 않는다”는 상황을 만나면 이미지 재빌드와 컨테이너 재생성 여부부터 확인하면 됩니다. docker compose restart만으로는 이미지나 환경변수 변경이 반영되지 않습니다.
캐시가 잘 작동하도록 순서를 잡는다
Dockerfile 명령은 레이어를 만들고, 변경된 지점부터 이후 레이어를 다시 빌드합니다. 그래서 자주 바뀌지 않는 의존성 파일을 먼저 복사하고 소스를 나중에 복사합니다.
FROM node:22-alpine AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN corepack enable && pnpm build FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production RUN addgroup -S app && adduser -S app -G app COPY --from=builder --chown=app:app /app/.next/standalone ./ COPY --from=builder --chown=app:app /app/.next/static ./.next/static COPY --from=builder --chown=app:app /app/public ./public USER app CMD ["node", "server.js"]
운영 이미지는 multi-stage build로 빌드 도구와 devDependencies를 제외하고, 필요하지 않은 패키지를 설치하지 않으며, 가능한 경우 non-root 사용자로 실행합니다. .dockerignore에는 최소한 .git, node_modules, 빌드 결과물, 로컬 .env*와 불필요한 문서를 넣습니다. 도커 공식 문서는 multi-stage, 잦은 재빌드, 작은 신뢰 가능한 베이스 이미지, .dockerignore, non-root 실행을 권장합니다. 빌드 모범 사례
Next.js의 output: 'standalone'은 필요한 런타임 파일을 모아줍니다. public과 .next/static은 자동 포함되지 않으므로 CDN을 쓰지 않으면 별도로 복사해야 합니다. 모노레포에서 앱 폴더 밖의 의존성을 쓴다면 outputFileTracingRoot와 필요 시 include 설정을 확인합니다. Next.js standalone 출력
태그와 업데이트 정책을 함께 둔다
latest는 안정 버전이라는 뜻이 아니라 변경 가능한 라벨입니다. 배포 이력에는 커밋 SHA 태그를 쓰고, 가장 강한 재현성이 필요하면 digest까지 고정합니다. 다만 digest를 고정하면 보안 업데이트도 자동으로 받지 못하므로 정기 재빌드·의존성 업데이트·취약점 스캔을 함께 운영해야 합니다. 도커 공식 문서도 이미지를 자주 재빌드하고 필요에 따라 베이스 이미지를 digest로 고정하라고 안내합니다. 베이스 이미지 갱신과 digest
3. 설정과 시크릿
원칙은 한 번 빌드하고 검증한 이미지를 그대로 승격하는 것입니다. 서버 전용 값은 컨테이너 시작 시 주입해서 개발과 운영에서 같은 이미지를 씁니다.
Next.js에는 예외가 있습니다. NEXT_PUBLIC_* 값은 브라우저 번들에 빌드 시점에 삽입되어 이후 바뀌지 않습니다. 여러 환경에 한 이미지를 승격하려면 공개 런타임 설정을 서버 API로 제공하거나, 환경별 빌드를 의도적으로 선택하고 각각 검증해야 합니다. 이를 단순히 “빌드 인자는 모두 잘못”이라고 판단하면 안 됩니다. Next.js 환경변수
시크릿 값은 Dockerfile, 이미지, Git 이력에 넣지 않습니다. 저장소에는 필요한 키 목록과 비밀이 아닌 예시만 둡니다. Compose에서는 환경변수보다 서비스별 파일 마운트인 secrets를 우선 검토합니다.
services: app: secrets: - db_password secrets: db_password: file: /etc/myapp-secrets/db_password
애플리케이션은 /run/secrets/db_password를 읽어야 합니다. Compose secrets도 원본 파일 자체를 암호화해 주는 비밀 저장소는 아니므로 호스트 파일 권한, 배포 도구, 키 교체 절차가 필요합니다. 값이 Git에 들어갔다면 삭제 커밋만 하지 말고 유출로 간주해 키를 교체해야 합니다. Compose secrets
4. 운영용 Compose 기준선
아래 구성은 Node 웹 서비스의 출발점입니다. 숫자는 정답이 아니라 부하 테스트와 관측 결과에 맞춰 조정합니다.
services: app: image: ghcr.io/example/app:${IMAGE_TAG:?IMAGE_TAG is required} restart: unless-stopped stop_grace_period: 30s ports: - "127.0.0.1:3000:3000" env_file: - .env.runtime secrets: - db_password mem_limit: 512m mem_reservation: 256m cpus: 1.0 pids_limit: 200 healthcheck: test: ["CMD", "node", "-e", "fetch('<http://127.0.0.1:3000/health/ready>').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] interval: 10s timeout: 3s retries: 5 start_period: 30s logging: driver: local options: max-size: "10m" max-file: "3" secrets: db_password: file: /etc/myapp-secrets/db_password
restart: 프로세스가 종료되거나 서버가 재부팅됐을 때 복구합니다. unhealthy이지만 살아 있는 컨테이너를 재시작시키지는 않습니다.stop_grace_period: SIGTERM을 처리해 요청과 작업을 정상 종료할 시간을 줍니다.json-file은 회전이 없어 디스크를 채울 수 있습니다. Docker가 권장하는 local 드라이버는 기본 회전·압축을 제공합니다. 기존 표준 때문에 json-file을 쓴다면 max-size, max-file을 반드시 지정합니다. 로그 드라이버 설정localhost가 IPv6 ::1로 해석되는 환경과 앱 바인딩 주소가 어긋날 수 있으므로 주소 계열을 일치시킵니다.애플리케이션이 지원한다면 read_only: true, 쓰기 위치의 tmpfs/volume 분리, cap_drop, security_opt: ["no-new-privileges:true"]도 단계적으로 적용합니다. 무조건 켜면 Next.js ISR 캐시나 업로드처럼 쓰기가 필요한 기능이 깨질 수 있으므로 테스트 후 적용해야 합니다. Docker 기본 seccomp/AppArmor·SELinux 정책도 특별한 이유 없이 해제하지 않습니다.
5. 배포, 검증, 롤백
권장 흐름은 다음과 같습니다.
개발 배포는 자동화할 수 있지만 운영은 서비스 위험도에 맞는 승인 게이트를 둡니다. CI와 사람이 서로 다른 절차를 쓰지 않도록 같은 배포 스크립트를 호출합니다. private registry 로그인과 토큰 만료·권한 확인도 배포 절차에 포함합니다.
커밋 SHA 태그는 사람이 읽는 이력이고 digest는 실제 내용 식별자입니다. .env나 배포 저장소에는 원하는 버전을 기록하고, 실행 중인 컨테이너의 digest는 실제 상태로 대조합니다. 태그를 원하는 상태의 유일한 기록으로 삼을 수는 있지만 실제 실행 상태까지 같다고 가정하면 안 됩니다.
여기에 흔한 오해가 하나 있습니다. Compose healthcheck는 상태를 기록할 뿐 standalone Docker에서 unhealthy 컨테이너를 자동 복구하지 않습니다. 배포 스크립트가 readiness 실패를 판정하고 롤백해야 합니다. restart 정책은 종료된 프로세스에 대한 정책입니다. 앱은 DB나 외부 API 연결 실패에 지수 백오프와 상한을 둔 재시도를 구현해야 합니다.
운영 호스트에는 현재와 직전 이미지를 남겨 registry 장애나 토큰 만료 때도 롤백할 수 있게 합니다. 검증에 실패한 이미지는 조사에 필요한 정보만 남긴 뒤 정리하고, 수정 배포가 빠른 개발 호스트는 현재 이미지만 보존하는 정책도 가능합니다. 무중단이 필요하면 새 버전을 별도 포트에 먼저 띄우고 readiness 통과 후 프록시를 전환합니다. 단일 Compose 프로젝트의 단순 교체에는 수 초의 중단이 생길 수 있습니다.
6. 네트워크와 포트
같은 Compose 네트워크의 컨테이너는 서비스 이름과 컨테이너 포트로 통신합니다.
api -> postgres:5432
api -> redis:6379
이 통신에는 ports가 필요 없습니다. DB와 캐시는 외부 접속 요구가 없다면 포트를 발행하지 않습니다. 웹 앱만 호스트 nginx가 접근할 수 있도록 127.0.0.1에 발행합니다.
ports: - "127.0.0.1:3000:3000" # 호스트만 접근 # - "3000:3000" # 모든 호스트 인터페이스에 발행
도커 공식 문서는 포트 발행이 기본적으로 외부 노출이라고 경고합니다. Linux에서 도커의 NAT 규칙은 ufw의 일반 INPUT/OUTPUT 처리보다 먼저 적용될 수 있으므로 ufw만 믿으면 안 됩니다. Docker Engine 28.0.0 미만에는 localhost 발행 포트가 같은 L2 네트워크에서 접근 가능했던 이력도 있으므로 구버전은 업그레이드해야 합니다. 포트 발행, Docker와 ufw
Compose 오버레이 확인
여기서 조심할 것이 오버레이 병합입니다. Compose의 ports는 {ip, target, published, protocol} 조합이 다르면 병합 시 함께 남을 수 있습니다. 베이스의 3000:3000을 운영 오버레이에서 루프백으로 좁힐 때는 Compose 2.24.4 이상에서 !override로 완전히 교체합니다.
ports: !override - "127.0.0.1:3000:3000"
DB처럼 발행 자체가 필요 없으면 ports: !reset []를 사용할 수 있습니다. 반면 호스트 nginx가 붙는 앱의 포트를 모두 지우면 502가 발생합니다. 배포 전에는 반드시 최종 병합 결과를 검사해야 합니다. Compose 병합 규칙
docker compose -f compose.yaml -f compose.prod.yaml config
토폴로지별 연결
| 상황 | 연결 방법 | 주의점 |
|---|---|---|
| 같은 Compose 프로젝트 | postgres:5432 | 서비스명과 컨테이너 포트를 씁니다. |
| 같은 호스트의 다른 프로젝트 | 필요한 서비스만 shared external network에 연결하거나, host.docker.internal:host-gateway와 gateway에서 접근 가능한 호스트 포트를 사용 | shared network는 격리 범위를 넓힙니다. host gateway 방식은 루프백 발행 포트에 닿지 않을 수 있으므로 Linux에서 실제 경로를 검증해야 합니다. |
| 다른 서버·같은 VPC | 사설 DNS/IP와 보안그룹 | TLS, 인증, 라우팅, 장애시간을 함께 설계합니다. |
환경별 Compose 파일이 다른 것은 토폴로지가 실제로 다를 때 정상입니다. 차이를 숨기지 말고 최종 구성 검증을 자동화합니다.
7. 데이터와 DB 운영 판단
볼륨이 있으면 데이터가 안전하다고 생각하기 쉬운데, 그렇지는 않습니다. 컨테이너의 쓰기 계층은 컨테이너와 함께 사라지므로 남아야 할 데이터는 named volume이나 외부 저장소에 둡니다. volume은 컨테이너 교체와 생명주기가 분리되지만 백업은 아닙니다. 별도 위치에 백업하고 정기적으로 복구 테스트를 통과해야 합니다. 볼륨 백업·복구
앱과 DB를 같은 Compose 프로젝트에 넣으면 앱 배포의 down이 DB까지 내릴 수 있습니다. 운영에서는 앱 스택과 데이터 스택을 분리하고, 필요한 네트워크만 명시적으로 공유합니다.
# infrastructure networks: app_net: { name: my-app-net } # application networks: app_net: { name: my-app-net, external: true }
프로젝트를 분리하면 depends_on으로 DB 준비를 기다릴 수 없으므로 앱 자체 재시도가 필요합니다. 같은 서버의 여러 스택은 Compose 프로젝트 이름을 고정해 이름 충돌과 실수 대상을 줄입니다.
컨테이너 DB와 관리형 DB 선택
| 질문 | 컨테이너 DB가 가능한 조건 | 관리형 DB를 우선할 조건 |
|---|---|---|
| 장애 허용 | 단일 호스트 장애와 수동 복구를 감당 | 짧은 RTO, 자동 장애조치가 필요 |
| 데이터 손실 허용 | 명확한 RPO에 맞춘 외부 백업과 복구 훈련 보유 | PITR, 다중 AZ 등 기능이 필요 |
| 운영 역량 | 패치·백업·모니터링·용량 계획 담당자 존재 | 전담 DB 운영 인력이 부족 |
| 보안·감사 | 암호화·접근통제·감사 증빙을 직접 구현 가능 | 표준화된 기능과 증빙이 필요 |
| 비용 | 인프라와 운영 인건비를 합쳐 더 유리 | 관리 비용을 포함하면 더 유리 |
법적 의무가 있다고 관리형 DB가 자동으로 강제되거나, 관리형 서비스를 쓴다고 준수가 자동 완성되는 것은 아닙니다. 요구되는 암호화, 접근통제, 백업, 보존, 감사, 데이터 위치를 항목별로 증명해야 합니다.
8. 운영과 장애 대응
디스크
도커는 사용하지 않는 객체를 보수적으로 남깁니다. 그래서 먼저 사용량을 확인해야 합니다.
docker system df -v
주의할 명령이 있습니다. 기본 docker image prune은 dangling 이미지만 지우지만, docker image prune -a와 docker system prune -a는 실행 중 컨테이너가 참조하지 않는 태그 이미지까지 지웁니다. 직전 롤백 이미지를 잃을 수 있으므로 운영 자동화에 무심코 넣으면 안 됩니다. stopped container와 build cache도 보존 기준을 정해 별도로 정리하고, volume은 데이터 삭제 가능성이 있으므로 별도 승인·백업 확인 없이 자동화하지 않습니다. Docker 객체 정리
호스트뿐 아니라 registry에도 보존 정책을 둡니다. 현재·직전·감사에 필요한 release는 보존하고 실패 빌드와 오래된 임시 태그를 정리합니다.
로그
애플리케이션은 기본적으로 stdout/stderr에 구조화 로그를 남기고 Docker logging driver가 처리하게 합니다. 로그 회전은 디스크 보호, 중앙 수집은 검색과 보존 문제입니다. 컨테이너 교체 후에도 장애 로그가 필요하면 Loki, CloudWatch 같은 외부 수집기로 보냅니다. 앱 파일 로그를 volume에 남기는 방식은 별도 회전·권한·백업 책임이 생기므로 특별한 요구가 있을 때만 씁니다.
메모리·CPU·프로세스
docker stats docker inspect --format '{{.State.OOMKilled}}' <container>
OOM 종료라면 무조건 한도를 늘리지 말고 누수, 동시성, 런타임 heap 설정, 호스트 여유량을 함께 봐야 합니다. 메모리 limit은 다른 서비스를 보호하는 안전선이지 필요한 용량을 만들어 주는 기능이 아닙니다. CPU throttling, 재시작 횟수, 디스크 사용률, health 상태에도 경보를 둡니다.
헬스체크
배포 성공과 트래픽 전환에는 readiness를 사용합니다. 의존성이 잠시 느린 것만으로 모든 인스턴스가 동시에 실패하지 않도록 timeout, retry, 캐시, 재시도 폭주를 설계해야 합니다. 같은 Compose 프로젝트의 시작 순서에는 depends_on: condition: service_healthy를 쓸 수 있지만 앱 재시도 자체를 대체하지는 않습니다. Compose 시작 순서
Docker daemon 권한
/var/run/docker.sock 접근 권한과 docker 그룹은 사실상 호스트 root 수준입니다. 컨테이너에 socket을 직접 마운트하지 않고, 원격 daemon을 평문 TCP로 공개하지 않습니다. 원격 관리는 SSH 또는 상호 TLS를 사용하고 접근자를 제한합니다. socket이 꼭 필요한 도구는 위험을 검토하고 허용 API를 제한한 프록시를 둡니다. :ro 마운트만으로 Docker API의 변경 요청이 차단되는 것은 아닙니다. Docker daemon 보호
9. 단계적 도입 절차
Compose는 한 호스트의 예측 가능한 규모와 허용 가능한 복구시간에 적합합니다. Kubernetes 전환 기준은 컨테이너 수나 다운타임 하나가 아니라 가용성 목표, 노드 수, 배포 빈도, 확장 패턴, 운영 인력, 고객 계약을 합쳐 판단해야 합니다.
10. 운영 전 최종 체크리스트
빌드·공급망
.dockerignore, non-root를 적용했습니다.NEXT_PUBLIC_*의 빌드 시점 고정을 의도적으로 처리했습니다.실행·보안
docker compose config 결과를 CI에서 검사합니다.