본문으로 건너뛰기
Previous
Next
Docker 완벽 가이드 2026 | 컨테이너 기반 개발 환경 구축

Docker 완벽 가이드 2026 | 컨테이너 기반 개발 환경 구축

Docker 완벽 가이드 2026 | 컨테이너 기반 개발 환경 구축

이 글의 핵심

Docker는 애플리케이션을 컨테이너로 패키징하여 어디서나 동일하게 실행할 수 있게 하는 플랫폼입니다. VM보다 가볍고 빠르며, 이미지 레이어 시스템으로 효율적인 스토리지 관리가 가능합니다. Docker Compose로 멀티 컨테이너 애플리케이션을 쉽게 관리하고, 개발/프로덕션 환경을 일관되게 유지할 수 있습니다.

“Build once, run anywhere”는 말은 예쁜데, real world에선 어디서나 같은 image를 돌리는 것에 가깝다고 보면 돼. Docker의 핵심은 image(읽기 전용 스택)랑 container(실제 프로세스)를 분리해놓고, layer 캐시로 build도 재미있게 만든 거지. 뉴비 킬러 question인 “이미지가 뭐고 컨테이너가 뭔가요?” — image는 레시피 + 재료 박스, container는 그걸로 실제로 굽는 동작이라고 잡고 가면 절반은 먹고 간다.

VM이랑 섞어서 말하면, VM은 guest OS를 통째로 들이밀고 hypervisor가 CPU·메모리를 쪼개 쓰는 쪽이고, container는 host OS kernel을 공유하면서 user space만 격리해. 그래서 기동은 수 초(혹은 그 이하)로 가고, disk도 GB → MB 쪽으로 줄어드는 경우가 많고, isolation은 “완벽한 하드웨어 가상화”만큼은 아니라도 앱 배포·스케일에는 보통 이게 훨씬 효율적이야. 쓰임새는 VM이 “OS 단위 랩핑”에 가깝고, Docker는 “앱 단위 런타임”에 가깝다고 보면 된다. 레지스트리(Docker Hub나 private)는 image 창고, layer는 “바뀐 것만 쌓기”로 저장 공간 쓰는 요령이다.

솔직한 취향: production image에 node:full + devDependencies + gcc + 소스 repo 통째로 넣는 single-stage는 나는 그냥 tech debt로 본다. 멀티스테이지 빌드는 필수라고 말해도 될 정도로, 최종 image에는 run에 필요한 것만 남기는 게 default stance야. (로컬에서 한번 돌려보는 throwaway image는 그럴 수 있다 쳐도, registry에 push하는 tag에는 최소 surface가 기본값.)


최근에 겪은 kind of debugging story를 짧게 풀면 이래. Staging에 새 API container 올렸는데 health check만 계속 fail. docker psRestarting만 도배되고, 슬쩍 docker logs --tail=50 api를 찍어보니 Cannot find module '/app/dist/server.js'. build stage에선 npm run build가 잘 돌아갔는데, COPY --from=builder 경로를 한 단계 잘못 써서 runtime stage에 dist가 아예 없었던 케이스였지. 로그만 보고 “Node가 미쳤나?” 하기 전에, docker run --rm -it으로 같은 image exec 느낌으로 ls /app 해보면 금방 드러나는 그림이야. 또 다른 day엔, compose에서 depends_on만 믿고 DB에 붙었는데 실제론 postgres ready 전에 migration이 떠서 connection refused. 그땐 docker compose logs -f db로 “ready to accept connections” 온 뒤에 앱을 올리거나, healthcheck + condition: service_healthy 쪽을 손봤다. OOM이면 docker stats로 누가 memory를 쳐먹는지 먼저 본다. 요약하면, container debugging은 “앱 log → image 안의 filesystem이 진짜 그렇게 생겼는지 → network가 붙는지(같은 network인지, DNS 이름이 맞는지) → resource limit” 순서로 내려가면 half는 여기서 끝이다.


설치 쪽은 Windows면 WSL2 + Docker Desktop 조합이 아직은 현실적으로 편해. wsl --installDocker Desktop 깔고 docker --version 찍으면 끝에 가깝다. macOSbrew install --cask docker 또는 공식 앱, Linux(Ubuntu 계열)aptdocker-ce + containerd 넣고 sudo usermod -aG docker $USER 해서 sudo 없이 쓰는 패턴. 세부 copy-paste는 공식 doc이 항상 최신이라 거기 line 따라가는 걸 추천한다.

띄우는 쪽 command는 자주 쓰는 것만. 백그라운드, 포트, 이름, env, mount까지 한 방에.

docker run -d --name my-api -p 8080:3000 -e NODE_ENV=production -v $(pwd):/app my-api:1.0

막혔을 때 docker logs -f, docker exec -it my-api sh, docker inspect… 이 세 개는 사실상 스페셜무브다. image 빌드는 docker build -t name:tag .이고, 캐시를 건드리지 않으려면 package.json / lockfile 먼저 COPY하는 패턴이 기본 (layer cache friendly).

멀티스테이지 필수 입장을 Dockerfile으로 박으면 대략 이런 느낌이다. builder에선 compile·test·npm ci 다 하고, 최종 stage는 slim/alpine + prod deps + artifact만.

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

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

.dockerignorenode_modules, .git, .env*, *.md 정도는 넣어서 context 쓰레기부터 차단. Python이면 pip/uv + slim base, Go면 golang stage에서 빌드하고 scratchdistroless바이너리만 넘기는 식—원칙은 같다. “한 Dockerfile에 RUN apt 난사 + 소스 풀카피 + 루트로 실행”은 review에서 걸리는 쪽이 맞다고 본다.

멀티 컨테이너는 docker compose로 묶는다. depends_on만으론 ready를 보장 못한다는 점, 한 번쯤 밟아보면 body로 각인됨. 예시 skeleton:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://user:pass@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: pass
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata: {}

network는 기본 bridge로 잡히는데, custom network 써서 service name으로 DNS resolve 되게 만드는지 확인. volume은 named volume이 데이터에, bind mount는 local dev hot reload에 쓰다가 “permission 지옥”을 경험하면 그때 철학이 생긴다. docker system prune은 위험하니 (특히 -a --volumes) 팀 룰 정하고 쓰는 게 좋다.

보안 쪽 당연한 line: 이미지에 secret 박지 말고, runtime에 env/secret store로, container는 non-root 권한으로. Trivy / docker scout 같은 건 CI에 한번 얹어두는 게 싸게 먹힌다. Kubernetes는 “Docker = runtime 엔진, k8s = 여러 node에서 돌릴 orchestrator” 정도의 관계로 이해하면 frontmatter faq이랑도 맞고, 여기서 더 깊게는 다른 글에서.

Docker를 잘 쓰면 “내 machine에선 됐는데”가 진짜로 줄어든다. 다만 multistage 없이 fat image push하고, log 안 보고 restart만 때리는 건 도구를 망치는 쪽에 가깝다. 멀티스테이지 빌드는 필수—이 stance로 가져가다 보면 image도 작아지고, attack surface도 줄고, 민감한 build tool이 production에 안 남는다. 이 정도 강도로 쓰는 게 요즘 내 default다.