Docker로 Next.js와 Express.js 개발 환경 완벽 구축 및 배포하기
이 글의 목적
이번 글에서는 Docker를 활용해 Next.js(프론트엔드)와 Express.js(백엔드)로 구성된 풀스택 개발 환경을 구축하고, 이를 최종적으로 프로덕션 환경에 배포하는 방법을 안내합니다.
PostgreSQL을 데이터베이스로 포함하여 실전 개발 및 배포 환경을 구성하는 과정을 단계별로 설명할 예정입니다.
전제 지식
이 글을 이해하려면 다음 기초 지식이 필요합니다:
- Git의 기본적인 사용법 (clone, commit, push 등)
- Docker의 기본 개념
- Node.js 기본 지식
- 터미널 명령어 사용
그럼 이제 본격적으로 환경을 구축하고, 배포하는 방법을 알아볼까요?
환경 구축의 흐름
- 프로젝트 구성
- Git 서브모듈 설정
- Docker Compose 설정
- Next.js 환경 구축
- Express.js 환경 구축
- PostgreSQL 설정
- 개발과 배포를 위한 Dockerfile 분리
- 프로덕션 환경으로 배포
1. 프로젝트 구성
먼저, 프로젝트의 전체 구조는 다음과 같습니다. front
는 Next.js 애플리케이션을, back
은 Express.js 애플리케이션을 담당합니다.
project-root/
├── front/ # Next.js 프로젝트
│ ├── Dockerfile
│ └── (Next.js 관련 파일)
├── back/ # Express.js 프로젝트
│ ├── Dockerfile
│ └── (Express.js 관련 파일)
└── docker-compose.yml
이 구조는 Next.js와 Express.js를 각각 독립적으로 관리하면서도, docker-compose.yml
파일을 통해 쉽게 통합하여 실행할 수 있습니다.
2. Git 서브모듈 설정
서브모듈은 하나의 리포지토리 안에서 다른 리포지토리를 포함해 관리할 수 있는 Git 기능입니다.
project-root
라는 메인 리포지토리 안에 front
와 back
이라는 두 개의 독립적인 리포지토리를 서브모듈로 추가하여 관리할 수 있습니다.
1) 리포지토리 및 서브모듈 생성
먼저, GitHub이나 GitLab 같은 서비스에서 project-root
, front
, back
이라는 세 개의 리포지토리를 각각 만듭니다.
그런 다음 project-root
리포지토리 안에 front
와 back
을 서브모듈로 추가합니다.
git submodule add <front 리포지토리의 SSH> front
git submodule add <back 리포지토리의 SSH> back
이렇게 하면 project-root
안에 front
와 back
디렉토리가 생기고, 각각의 리포지토리가 서브모듈로 연결됩니다.
서브모듈을 추가한 후에는 .gitmodules
파일이 자동으로 생성되며, 이 파일에는 서브모듈 정보가 들어있습니다.
2) 서브모듈 커밋 및 푸시
서브모듈을 커밋하고 원격 리포지토리에 푸시합니다.
git add .
git commit -m "Add front and back as submodules"
git push origin main
3) 서브모듈 복제(클론)
다른 사람이 이 리포지토리를 클론할 때 서브모듈까지 함께 가져오려면, 다음 명령어를 사용합니다:
git clone --recurse-submodules <project-root 리포지토리의 SSH>
이제 서브모듈 설정이 완료되었으니, Docker 설정을 진행해봅시다.
3. Docker Compose 설정
Docker Compose는 여러 서비스를 한 번에 관리할 수 있도록 도와줍니다.
이번 프로젝트에서는 Next.js(프론트엔드), Express.js(백엔드), PostgreSQL(데이터베이스)를 하나의 docker-compose.yml
파일로 통합하여 실행할 수 있습니다.
docker-compose.yml 설정
다음은 docker-compose.yml
파일의 설정 예시입니다:
version: '3'
services:
front:
build:
context: ./front
dockerfile: Dockerfile
ports:
- "3000:3000" # Next.js 기본 포트
volumes:
- ./front:/app # 소스 코드 동기화
environment:
- NEXT_PUBLIC_API_URL=http://back:8000
depends_on:
- back
restart: unless-stopped
back:
build:
context: ./back
dockerfile: Dockerfile
ports:
- "8000:8000" # Express.js 기본 포트
volumes:
- ./back:/app
- /app/node_modules # node_modules는 컨테이너 내에만 보관
environment:
- DATABASE_URL=postgresql://user:password@db:5432/dbname
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=dbname
restart: unless-stopped
volumes:
postgres_data:
이 설정 파일을 통해 Next.js, Express.js, PostgreSQL 세 가지 서비스를 한 번에 관리할 수 있습니다.
4. Next.js 환경 구축
이제 프론트엔드인 Next.js 환경을 구축해보겠습니다.
1) Next.js 프로젝트 생성
project-root
디렉토리로 이동하여, 다음 명령어로 Next.js 프로젝트를 생성합니다:
docker-compose run --rm front npx create-next-app .
Next.js 프로젝트가 생성되면, 기본 설정을 적용합니다.
2) Next.js Dockerfile 설정
front
디렉토리 내의 Dockerfile
을 다음과 같이 설정합니다:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
3) Next.js 빌드 및 실행
이제 Docker에서 Next.js 애플리케이션을 빌드하고 실행할 수 있습니다.
docker-compose build
docker-compose up front
브라우저에서 http://localhost:3000에 접속하여 Next.js 애플리케이션이 정상적으로 실행되는지 확인합니다.
5. Express.js 환경 구축
다음으로 백엔드인 Express.js 환경을 설정하겠습니다.
1) Express.js 프로젝트 생성
project-root
디렉토리에서 다음 명령어로 Express.js 프로젝트를 초기화합니다:
docker-compose run --rm back npm init -y
docker-compose run --rm back npm install express
docker-compose run --rm back npm install typescript @types/express @types/node ts-node nodemon -D
2) Express.js Dockerfile 설정
back
디렉토리 내의 Dockerfile
을 다음과 같이 설정합니다:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 8000
CMD ["node", "dist/app.js"]
3) Express.js 서버 생성
src/server.ts
파일을 만들어서 다음과 같이 작성합니다:
import express from "express";
import { Request, Response } from "express";
const app = express();
const port = 8000;
app.get("/", (req: Request, res: Response) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
4) Express.js 빌드 및 실행
Express.js 애플리케이션을 빌드하고 실행합니다:
docker-compose build
docker-compose up back
브라우저에서 http://localhost:8000에 접속하여 "Hello World!" 메시지가 출력되는지 확인합니다.
6. PostgreSQL 설정
이제 PostgreSQL을 설정해봅시다.
docker-compose.yml
파일에서 이미 PostgreSQL 설정이 되어 있으므로, 데이터베이스가 자동으로 실행됩니다.
PostgreSQL과 Express.js 연동
Express.js에서 PostgreSQL을 사용하려면 pg
패키지를 설치하여 데이터베이스와 연동할 수 있습니다.
pg
는 PostgreSQL과의 연결을 관리하고 쿼리를 실행할 수 있는 라이브러리입니다.
docker-compose run --rm back npm install pg
그 다음, PostgreSQL에 연결하기 위한 코드를 작성합니다. src/db.ts
파일을 생성하고 다음과 같이 작성합니다:
import { Pool } from "pg";
const pool = new Pool({
user: 'user',
host: 'db', // PostgreSQL 컨테이너의 서비스 이름 (docker-compose.yml의 'db')
database: 'dbname',
password: 'password',
port: 5432,
});
export default pool;
이 코드는 PostgreSQL 데이터베이스에 연결할 수 있는 풀(pool)을 생성합니다.
이제 Express.js 서버에서 이 풀을 사용하여 데이터베이스에 쿼리를 날릴 수 있습니다.
Express.js에서 PostgreSQL 사용
server.ts
파일을 수정하여 PostgreSQL 연결을 테스트해보겠습니다.
예를 들어, 데이터베이스에서 현재 시간을 가져오는 간단한 쿼리를 실행해보겠습니다.
import express from "express";
import pool from "./db"; // PostgreSQL 연결 설정 가져오기
import { Request, Response } from "express";
const app = express();
const port = 8000;
app.get("/", async (req: Request, res: Response) => {
try {
const result = await pool.query("SELECT NOW()"); // 현재 시간을 가져오는 쿼리
res.json(result.rows[0]);
} catch (err) {
console.error(err.message);
res.status(500).send("Server Error");
}
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
이 코드는 Express.js 서버에서 PostgreSQL에 쿼리를 실행하여 현재 시간을 가져오고, 그 결과를 브라우저에 표시하는 간단한 API입니다.
이제 서버를 실행하고 PostgreSQL 연동이 잘 되는지 확인해봅시다.
docker-compose up
브라우저에서 http://localhost:8000에 접속하면, PostgreSQL에서 가져온 현재 시간이 JSON 형태로 표시될 것입니다.
7. 개발과 배포를 위한 Dockerfile 분리
개발과 배포 환경에서는 요구 사항이 다르기 때문에, 개발용 Dockerfile과 배포용 Dockerfile을 분리하는 것이 좋습니다.
이렇게 하면 개발할 때 필요한 도구와 배포할 때 최적화된 설정을 각각 적용할 수 있습니다.
왜 개발과 배포를 위한 Dockerfile을 분리해야 할까요?
개발 환경과 배포 환경은 서로 다른 요구 사항을 가지고 있습니다.
- 개발 환경:
- 핫 리로딩(Hot Reloading): 코드 변경 시 서버가 자동으로 다시 빌드되고, 새로고침 없이 변경 사항이 반영됩니다.
- 디버깅 도구:
nodemon
,ts-node
, 또는 기타 개발 도구들이 필요합니다. - volumes를 사용하여 로컬 파일을 컨테이너와 동기화하여, 파일을 수정할 때마다 컨테이너 안에서 실시간으로 반영되도록 설정합니다.
- 배포 환경:
- 배포 시에는 최적화된 빌드가 필요하며, 개발용 도구는 포함되지 않습니다.
- 서버가 빠르고 가볍게 실행되도록 해야 하며, 불필요한 파일이나 개발 도구는 제거됩니다.
따라서 개발용 Dockerfile과 배포용 Dockerfile을 따로 관리하는 것이 더 효율적입니다.
방법 1: Dockerfile 분리 (권장)
개발용과 배포용 Dockerfile을 각각 분리하는 방법을 소개합니다.
1.1 개발용 Dockerfile
개발용 Dockerfile은 핫 리로딩과 디버깅 도구를 포함한 설정이 필요합니다. 이 파일은 일반적으로 Dockerfile.dev
로 저장합니다.
# Dockerfile.dev (개발용)
FROM node:20
WORKDIR /app
# 패키지 설치
COPY package*.json ./
RUN npm install
# 소스코드 복사
COPY . .
# Next.js 개발 서버 실행 (핫 리로딩)
EXPOSE 3000
CMD ["npm", "run", "dev"]
이 Dockerfile은 Next.js와 같은 프론트엔드 환경에서 핫 리로딩을 지원하는 개발 서버를 실행하는 데 최적화되어 있습니다.
1.2 배포용 Dockerfile
배포용 Dockerfile은 불필요한 도구 없이 최적화된 환경에서 애플리케이션을 실행하도록 설정합니다.
이 파일은 기본적으로 Dockerfile
로 저장합니다.
# Dockerfile (배포용)
FROM node:20
WORKDIR /app
# package.json과 package-lock.json을 복사하고 의존성 설치
COPY package*.json ./
RUN npm install --production
# 소스코드 복사
COPY . .
# 애플리케이션 빌드 (Next.js의 경우)
RUN npm run build
# Next.js 애플리케이션 실행
EXPOSE 3000
CMD ["npm", "start"]
이 Dockerfile은 배포용으로, 불필요한 개발 도구를 제외하고 프로덕션 빌드를 실행하도록 최적화되어 있습니다.
방법 2: Dockerfile 하나로 분기 처리
두 개의 Dockerfile을 관리하는 대신, 하나의 Dockerfile에서 환경 변수를 통해 개발과 배포 모드를 분리할 수도 있습니다.
# 단일 Dockerfile
FROM node:20
WORKDIR /app
# package.json 복사 및 의존성 설치
COPY package*.json ./
ARG NODE_ENV
RUN if [ "$NODE_ENV" = "production" ]; \
then npm install --production; \
else npm install; \
fi
# 소스코드 복사
COPY . .
# 빌드 단계 (배포 환경에서만 실행)
RUN if [ "$NODE_ENV" = "production" ]; \
then npm run build; \
fi
# 포트 노출
EXPOSE 3000
# 실행 명령어 (개발 모드와 배포 모드에 따라 다름)
CMD if [ "$NODE_ENV" = "production" ]; \
then npm start; \
else npm run dev; \
fi
이 방식은 Dockerfile 하나로 관리하면서도, 개발과 배포 환경을 환경 변수(NODE_ENV
)에 따라 분리할 수 있는 방법입니다.
8. 프로덕션 환경으로 배포
1) Docker Compose 파일 준비
배포 환경에서는 개발 환경에서 사용하던 핫 리로딩 등의 기능이 필요 없으므로, 배포용 Dockerfile
을 사용해 배포합니다.
version: '3'
services:
front:
build:
context: ./front
dockerfile: Dockerfile # 배포용 Dockerfile 사용
ports:
- "3000:3000"
environment:
- NODE_ENV=production # 프로덕션 모드
depends_on:
- back
restart: unless-stopped
back:
build:
context: ./back
dockerfile: Dockerfile # 배포용 Dockerfile 사용
ports:
- "8000:8000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:password@db:5432/dbname
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=dbname
restart: unless-stopped
volumes:
postgres_data:
2) 프로덕션 빌드 및 실행
프로덕션 환경에서 애플리케이션을 빌드하고 실행하려면 다음 명령어를 사용하세요:
docker-compose build
docker-compose up -d
이 명령어를 실행하면, 프론트엔드와 백엔드가 각각 배포용 모드로 실행됩니다.
3) 서비스 상태 확인
docker-compose ps
이 명령어를 사용하여 각 서비스의 상태를 확인할 수 있습니다.
4) 로그 확인 및 문제 해결
docker-compose logs front
docker-compose logs back
각 서비스의 로그를 확인하여 발생한 문제를 해결할 수 있습니다.
마무리하며
이번 글을 통해 Next.js와 Express.js 기반의 풀스택 애플리케이션을 Docker Compose를 사용해 개발하고, 최종적으로 프로덕션 환경에 배포하는 방법을 알아봤습니다.
Docker Compose를 사용하면 개발 환경과 배포 환경을 일관성 있게 유지할 수 있기 때문에, 특히 팀 단위로 작업하거나 배포할 때 큰 도움이 됩니다.
'Docker' 카테고리의 다른 글
GitHub Actions를 활용한 CI/CD 파이프라인 구축 및 Docker Compose 자동 배포 (1) | 2024.11.03 |
---|---|
도커 튜토리얼 2부: Go로 만든 REST API를 Dockerfile 이용해서 도커에 넣기 (0) | 2024.03.31 |
도커 튜토리얼 1부. 도커 기초 다지기 (0) | 2024.03.31 |
Docker 기초 배우기 (0) | 2024.03.30 |