Jiwon Min Developer

Dockerizing Your Web Application for Consistent Development

많은 개발자들이 협업 과정에서 한 번쯤은 “제 컴퓨터에서는 잘 되는데요?”라는 말을 들어보거나 해본 경험이 있을 것입니다. 개발자마다 다른 운영체제, 라이브러리 버전, 설정 값의 미세한 차이는 예상치 못한 버그의 원인이 되며, 새로운 팀원이 프로젝트에 합류할 때마다 복잡한 개발 환경 설정 과정에 많은 시간을 소모하게 됩니다. 이는 생산성을 저해하는 고질적인 문제입니다.

이러한 문제들을 해결하기 위해 등장한 기술이 바로 Docker입니다. Docker는 애플리케이션과 그 의존성을 컨테이너(Container)라는 격리된 공간에 패키징하여, 어떤 환경에서든 동일하게 실행될 수 있도록 보장합니다. 이를 통해 개발, 테스트, 운영 환경 간의 차이를 최소화하고, “내 컴퓨터에서만 되는” 문제를 근본적으로 해결할 수 있습니다.

본 포스트에서는 숙련된 서버 엔지니어의 관점에서 왜 개발 환경에 Docker를 도입해야 하는지 그 이유를 설명하고, 간단한 Node.js 애플리케이션을 예제로 Dockerfiledocker-compose.yml을 작성하여 일관성 있고 효율적인 개발 환경을 구축하는 전 과정을 상세하게 안내하겠습니다.

Dockerizing Your Web Application for Consistent Development

© AI Generated by Imagen 4.0


Docker란 무엇일까요?

Docker는 애플리케이션을 신속하게 구축, 테스트 및 배포할 수 있는 오픈소스 플랫폼입니다. Docker의 핵심은 컨테이너 기술에 있습니다. 컨테이너는 애플리케이션 코드와 실행에 필요한 모든 의존성(라이브러리, 시스템 도구, 런타임 등)을 이미지(Image)라는 하나의 패키지로 묶습니다.

  • 이미지(Image): 애플리케이션을 실행하는 데 필요한 모든 것을 담고 있는 읽기 전용 템플릿입니다. 코드, 런타임, 시스템 도구, 라이브러리 및 설정이 포함됩니다.
  • 컨테이너(Container): 이미지의 실행 가능한 인스턴스입니다. 하나의 이미지로부터 여러 개의 컨테이너를 생성할 수 있으며, 각 컨테이너는 호스트 시스템 및 다른 컨테이너와 격리된 상태로 실행됩니다.

가상 머신(VM)이 하드웨어 수준까지 가상화하여 게스트 OS를 통째로 설치하는 것과 달리, 컨테이너는 호스트 시스템의 OS 커널을 공유하면서 프로세스 수준에서 격리됩니다. 이 덕분에 Docker 컨테이너는 VM보다 훨씬 가볍고, 빠르며, 이식성이 높다는 장점을 가집니다.

왜 개발 환경에 Docker를 사용해야 할까요?

개발 환경에 Docker를 도입하면 다음과 같은 명확한 이점을 얻을 수 있습니다.

  1. 환경의 일관성 (Consistency): Dockerfile이라는 코드 기반의 명세서를 통해 개발 환경을 정의하므로, 모든 팀원이 정확히 동일한 환경에서 작업할 수 있습니다. 이는 “내 컴퓨터에서는 되는데…” 문제를 원천적으로 차단합니다.
  2. 의존성 격리 (Dependency Isolation): 각 프로젝트의 의존성은 컨테이너 내부에 완벽하게 격리됩니다. 로컬 머신에 특정 버전의 Node.js나 Python, 데이터베이스를 설치할 필요가 없으므로 프로젝트 간 의존성 충돌이 발생하지 않습니다.
  3. 빠른 설정 (Fast Setup): 새로운 팀원은 Git 저장소를 클론하고 docker-compose up이라는 단 하나의 명령어로 전체 개발 환경(웹 서버, 데이터베이스, 캐시 등)을 즉시 실행할 수 있습니다. 복잡한 설치 가이드는 더 이상 필요 없습니다.
  4. 운영 환경과의 유사성 (Parity with Production): 개발 환경과 운영 환경에서 동일한 Docker 이미지를 사용함으로써 환경 차이로 인해 발생하는 버그를 획기적으로 줄일 수 있습니다.

실습: Node.js 애플리케이션 Dockerizing

이제 간단한 Node.js Express 애플리케이션을 Docker 컨테이너 환경에서 실행하는 과정을 단계별로 진행해 보겠습니다.

먼저, 프로젝트 폴더에 간단한 웹 서버 코드인 server.jspackage.json 파일을 생성합니다.

package.json

{
  "name": "docker-node-app",
  "version": "1.0.0",
  "description": "Simple Node.js app for Docker",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}

server.js

'use strict';

const express = require('express');

const PORT = 8080;
const HOST = '0.0.0.0';

const app = express();
app.get('/', (req, res) => {
  res.send('Hello from Docker Container!');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

다음으로, 이 애플리케이션을 실행할 Docker 이미지를 정의하는 Dockerfile을 작성합니다.

Dockerfile

# 1. 베이스 이미지 선택
FROM node:18-alpine

# 2. 애플리케이션 디렉토리 생성
WORKDIR /usr/src/app

# 3. 애플리케이션 의존성 설치
# 와일드카드(*)를 사용해 package.json과 package-lock.json 모두 복사
COPY package*.json ./
RUN npm install

# 4. 앱 소스 코드 복사
COPY . .

# 5. 애플리케이션이 사용할 포트 노출
EXPOSE 8080

# 6. 컨테이너 시작 시 실행될 명령어 정의
CMD [ "npm", "start" ]

각 명령어의 의미는 다음과 같습니다.

  • FROM: 이미지를 빌드할 기반이 되는 베이스 이미지를 지정합니다. node:18-alpine은 경량 리눅스인 Alpine 기반의 Node.js 18 버전 이미지입니다.
  • WORKDIR: 컨테이너 내의 작업 디렉토리를 설정합니다. 이후의 COPY, RUN, CMD 명령어는 이 디렉토리 내에서 실행됩니다.
  • COPY: 호스트의 파일을 컨테이너 내부로 복사합니다.
  • RUN: 컨테이너 내에서 명령어를 실행합니다. 여기서는 npm install을 통해 의존성을 설치합니다.
  • EXPOSE: 컨테이너가 외부에 노출할 포트를 지정합니다.
  • CMD: 컨테이너가 시작될 때 기본으로 실행될 명령어를 정의합니다.

이제 터미널에서 아래 명령어를 실행하여 Docker 이미지를 빌드하고 컨테이너를 실행합니다.

# 1. Docker 이미지 빌드 (-t 옵션으로 이미지에 이름과 태그 부여)
$ docker build -t my-node-app:1.0 .

# 2. 빌드된 이미지를 사용하여 컨테이너 실행
# -p 4000:8080 : 호스트의 4000번 포트를 컨테이너의 8080번 포트와 연결
# -d : 백그라운드에서 컨테이너 실행
$ docker run -p 4000:8080 -d my-node-app:1.0

이제 웹 브라우저에서 http://localhost:4000으로 접속하면 “Hello from Docker Container!” 메시지를 확인할 수 있습니다.

Docker Compose로 서비스 오케스트레이션

실제 프로젝트에서는 웹 애플리케이션 외에도 데이터베이스, 캐시 서버 등 여러 서비스가 함께 동작해야 합니다. 이처럼 여러 컨테이너를 한 번에 관리하고 연결하기 위해 Docker Compose를 사용합니다.

프로젝트 루트에 docker-compose.yml 파일을 작성해 보겠습니다. 이 파일은 웹 애플리케이션(app)과 PostgreSQL 데이터베이스(db) 두 개의 서비스를 정의합니다.

docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    ports:
      - "4000:8080"
    volumes:
      - .:/usr/src/app
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://user:password@db:5432/mydatabase

  db:
    image: postgres:14-alpine
    restart: always
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydatabase
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
  • services: 관리할 컨테이너들을 정의합니다. appdb라는 두 개의 서비스를 정의했습니다.
  • build: .: app 서비스는 현재 디렉토리의 Dockerfile을 사용하여 이미지를 빌드합니다.
  • image: postgres:14-alpine: db 서비스는 Docker Hub에 공개된 공식 PostgreSQL 이미지를 사용합니다.
  • ports: 호스트와 컨테이너의 포트를 매핑합니다.
  • volumes: 호스트의 특정 경로(또는 Docker 볼륨)를 컨테이너의 경로에 마운트합니다. 이를 통해 코드 변경 사항이 컨테이너에 실시간으로 반영되거나, 컨테이너가 삭제되어도 데이터베이스 데이터가 유지됩니다.
  • environment: 컨테이너 내에서 사용할 환경 변수를 설정합니다.
  • depends_on: 서비스 간의 의존 관계를 설정합니다. app 서비스는 db 서비스가 시작된 후에 시작됩니다.

이제 터미널에서 다음 명령어를 실행하면 docker-compose.yml에 정의된 모든 서비스가 한 번에 실행됩니다.

# 백그라운드에서 모든 서비스 시작
$ docker-compose up -d

# 모든 서비스 중지 및 컨테이너 삭제
$ docker-compose down

docker-compose up 단 한 줄의 명령어로 복잡한 멀티 서비스 애플리케이션 환경을 완벽하게 재현할 수 있게 되었습니다.

결론

Docker는 더 이상 선택이 아닌 현대적인 웹 개발과 서버 운영의 필수 기술이 되었습니다. 개발 환경에 Docker를 도입함으로써 우리는 환경의 일관성을 확보하고, 의존성을 격리하며, 새로운 팀원의 온보딩 과정을 획기적으로 단축할 수 있습니다. 이는 곧 개발 생산성의 향상과 안정적인 서비스 운영으로 이어집니다.

오늘 소개한 DockerfileDocker Compose를 활용하여 여러분의 다음 프로젝트부터는 “내 컴퓨터에서는 되는데…“라는 말 대신, 재현 가능하고 이식성 높은 개발 환경의 편리함을 경험해 보시기를 바랍니다.

참고문헌