Node.js 언어별 가이드
Node.js 언어별 가이드
이 가이드는 Docker로 컨테이너화된 TypeScript Node.js 어플리케이션을 만드는 방법을 알려줘요.
출처: 문서
본문
Node.js는 서버 사이드 어플리케이션을 만들기 위한 JavaScript 런타임이에요. 이 가이드는 단순한 Express API에서 시작해 데이터베이스 같은 기능을 점진적으로 추가하며, Docker로 TypeScript Node.js 어플리케이션을 컨테이너화하는 방법을 보여줘요.
이 가이드는 백엔드 Node.js API에 초점을 맞춰요. 독립형 프론트엔드 어플리케이션을 만든다면 Docker는 React.js, Vue.js, Angular, Next.js 전용 가이드를 갖추고 있어요.
감사의 말 (Acknowledgment)
Docker는 이 가이드에 기여해준 Kristiyan Velkov에게 감사를 전해요.
무엇을 배우게 될까요?
이 가이드에서 다음을 배우게 돼요:
- Docker를 사용해 Node.js 어플리케이션을 컨테이너화하고 실행하기.
- 컨테이너를 사용해 로컬 개발 환경 설정하기.
- Docker 컨테이너 안에서 테스트 실행하기.
Node.js 어플리케이션을 컨테이너화하는 것부터 시작해요.
준비 사항 (Prerequisites)
- JavaScript와 TypeScript에 대한 기본 이해.
- Node.js와 npm에 대한 기본 지식.
- 이미지, 컨테이너, Dockerfile 같은 Docker 개념에 대한 이해. Docker가 처음이라면 Docker basics 가이드부터 시작해요.
Node.js 어플리케이션 컨테이너화하기
준비 사항 (Prerequisites)
- 최신 버전의 Docker Desktop을 설치했어야 해요.
- 기본적인 Docker 개념에 익숙해야 해요. Docker가 처음이라면 "Build and share a containerized application"부터 시작해요.
개요 (Overview)
어플리케이션을 컨테이너화하는 것은 어플리케이션을 그 의존성, 구성, 런타임과 함께 컨테이너 이미지라는 단일 이식 가능한 단위로 패키징하는 것을 의미해요. 그 이미지를 실행하면 컨테이너가 만들어지는데, 랩톱, CI 러너, 프로덕션 서버 등 어떤 머신에서든 동일하게 동작하는 격리된 프로세스예요.
이 섹션에서는 TypeScript로 작성된 단순한 Express.js API를 컨테이너화해요. 이미지를 빌드하는 방법을 설명하는 Dockerfile을 작성하고, Docker가 컨테이너를 실행하는 방법을 정의하는 compose.yaml 파일을 추가한 다음, 한 명령으로 어플리케이션을 빌드·시작할 거예요.
베이스로 Docker Hardened Images를 사용할 거예요. Docker가 관리하는 최소화되고 안전한 Node.js 이미지예요.
이 가이드는 백엔드 Node.js API에 초점을 맞춰요. 독립형 프론트엔드 어플리케이션을 만든다면 Docker는 React.js, Vue.js, Angular, Next.js 전용 가이드를 갖추고 있어요.
어플리케이션 만들기
샘플 어플리케이션은 단일 엔드포인트가 JSON 인사말을 반환하는 최소한의 Express API예요. 새 nodejs-docker-example 디렉토리에 다음 파일들을 만들어요. 파일을 모두 한 번에 만들려면 파일 브라우저에서 Scaffold script 탭으로 전환해 셸 명령을 복사해요.
nodejs-docker-example/src/index.ts (새 파일):
// A minimal Express application.
// The root endpoint (GET /) returns a JSON greeting.
// See https://expressjs.com/ for the framework reference.
import express, { type Request, type Response } from "express";
const app = express();
const port = parseInt(process.env.PORT ?? "3000", 10);
app.get("/", (_req: Request, res: Response) => {
res.json({ message: "Hello World" });
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
nodejs-docker-example/package.json (새 파일):
{
"name": "nodejs-docker-example",
"version": "1.0.0",
"description": "A minimal Node.js TypeScript application.",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"express": "^4.21.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3"
}
}
nodejs-docker-example/tsconfig.json (새 파일):
{
// TypeScript compiler configuration for the Node.js application.
// Compiles src/ to dist/ as CommonJS modules targeting ES2022.
// See https://www.typescriptlang.org/tsconfig/ for all options.
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
nodejs-docker-example/.gitignore (새 파일):
# Files and directories that Git should ignore. Covers Node.js dependencies,
# TypeScript build output, environment files, and common editor artifacts.
# See https://git-scm.com/docs/gitignore for syntax reference.
node_modules/
dist/
.env
*.log
.DS_Store
coverage/
db/password.txt
Note
이 섹션의 Bash/PowerShell 탭에는 Scaffold 스크립트가 있어 위 파일들을 한 번에 만들 수 있어요.
Node.js가 설치되어 있고 컨테이너화하기 전에 앱이 동작하는지 확인하고 싶다면 로컬에서 실행할 수 있어요.
핫 리로드가 있는 개발 모드로 실행하려면:
$ npm install
$ npm run dev
컴파일된 프로덕션 빌드를 실행하려면 (Dockerfile이 하는 것과 일치):
$ npm install
$ npm run build
$ npm start
그런 다음 브라우저에서 http://localhost:3000 을 열어요. {"message":"Hello World"} 가 보일 거예요.
Node.js가 설치되어 있지 않다면 건너뛰어도 돼요. 나머지 단계들은 로컬 Node.js 없이 컨테이너에서 어플리케이션을 실행해요.
Docker 자산 만들기
빌드 중에 Docker가 Node.js 베이스 이미지를 pull할 수 있도록 DHI 레지스트리에 로그인해요. 사용 가능한 Node.js 이미지들은 catalog에 나열돼 있어요.
$ docker login dhi.io
nodejs-docker-example 디렉토리에 다음 세 파일을 추가해요. Dockerfile은 이미지를 빌드하는 방법을 설명하고, compose.yaml은 Docker가 컨테이너를 실행하는 방법을 정의하며, .dockerignore는 원하지 않는 파일을 빌드 컨텍스트에서 제외해요.
Tip
Gordon, Docker의 AI 어시스턴트가 프로젝트에 맞는 Docker 자산을 생성해줄 수 있어요. Gordon에게 어플리케이션에 맞춘 Dockerfile, Compose 파일,
.dockerignore를 만들어 달라고 요청해보세요.
nodejs-docker-example/Dockerfile (새 파일):
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Builder stage: install all dependencies and compile TypeScript.
FROM dhi.io/node:24-alpine3.23-dev AS builder
WORKDIR /app
# Install dependencies as a separate step to take advantage of Docker's
# caching. Leverage a cache mount to /root/.npm to speed up subsequent
# builds. Leverage a bind mount to package.json to avoid having to copy
# it into this layer.
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install
# Once you create a package-lock.json by running npm install locally, switch to npm ci and bind both files:
# RUN --mount=type=cache,target=/root/.npm \
# --mount=type=bind,source=package.json,target=package.json \
# --mount=type=bind,source=package-lock.json,target=package-lock.json \
# npm ci
# Copy the source code into the container and compile TypeScript.
COPY . .
RUN npm run build
# Deps stage: install production dependencies only.
FROM dhi.io/node:24-alpine3.23-dev AS deps
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install --omit=dev
# Once you create a package-lock.json by running npm install locally, switch to npm ci and bind both files:
# RUN --mount=type=cache,target=/root/.npm \
# --mount=type=bind,source=package.json,target=package.json \
# --mount=type=bind,source=package-lock.json,target=package-lock.json \
# npm ci --omit=dev
# Runner stage: minimal runtime image with compiled app and production deps.
FROM dhi.io/node:24-alpine3.23 AS runner
ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
# Expose the port that the application listens on.
EXPOSE 3000
# Run the application.
CMD ["node", "dist/index.js"]
nodejs-docker-example/compose.yaml (새 파일):
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 3000:3000
nodejs-docker-example/.dockerignore (새 파일):
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
node_modules/
dist/
.env
.git
.gitignore
.DS_Store
npm-debug.log*
coverage/
db/
Dockerfile은 세 개의 스테이지를 사용해요. builder 스테이지는 모든 의존성을 설치하고 TypeScript를 컴파일해요. deps 스테이지는 프로덕션 전용 의존성만 새로 설치해요. runner 스테이지는 컴파일된 출력과 프로덕션 node_modules를 Node.js만 포함하는 최소 런타임 이미지로 복사해요.
각 파일에 대해 더 알아보려면 다음을 참고해요:
어플리케이션 실행하기
nodejs-docker-example 디렉토리 안에서 터미널에 다음 명령을 실행해요.
$ docker compose up --build
브라우저를 열고 http://localhost:3000에서 어플리케이션을 확인해요. {"message":"Hello World"} 를 볼 수 있어요.
터미널에서 ctrl+c를 눌러 어플리케이션을 중지해요.
어플리케이션을 백그라운드로 실행하기
-d 옵션을 추가하면 터미널에서 분리된 상태로 어플리케이션을 실행할 수 있어요. nodejs-docker-example 디렉토리 안에서 터미널에 다음 명령을 실행해요.
$ docker compose up --build -d
브라우저를 열고 http://localhost:3000에서 어플리케이션을 확인해요.
터미널에서 다음 명령을 실행해 어플리케이션을 중지해요.
$ docker compose down
Compose 명령에 대한 자세한 내용은 Compose CLI reference를 참고해요.
Node.js 개발에 컨테이너 사용하기
준비 사항 (Prerequisites)
Node.js 어플리케이션 컨테이너화를 완료해요.
개요 (Overview)
어플리케이션이 컨테이너에서 실행되면 다음 단계는 컨테이너를 일상적인 개발 워크플로의 일부로 만드는 거예요. 코드 변경이 빠르게 반영되어야 하고, 데이터베이스처럼 앱이 의존하는 서비스도 바로 옆에서 실행되어야 해요.
이 섹션에서는 builder 스테이지를 dev로 이름을 바꾸고 Compose가 그것을 가리키도록 해서 Dockerfile을 로컬 개발용으로 조정할 거예요. 그리고 어플리케이션을 PostgreSQL 데이터베이스에 연결하도록 업데이트하고, compose.yaml에 데이터베이스 서비스를 추가하며, 명명된 볼륨에 데이터를 유지하고, 수동 리빌드 없이 편집기의 변경 사항이 반영되도록 Compose Watch를 활성화하며, 실행 중인 컨테이너에 VS Code나 Chrome DevTools를 연결할 수 있도록 Node.js 디버깅을 설정할 거예요.
어플리케이션 업데이트하기
어플리케이션을 PostgreSQL 데이터베이스에 연결하도록 업데이트할 거예요. nodejs-docker-example 디렉토리에서 계속 작업해요.
src/index.ts와 package.json을 다음 내용으로 교체해요. 파일 브라우저는 이 단계에서 변경되는 파일들만 보여줘요.
Note
이 단계 후에는 어플리케이션이 아직 실행되지 않아요. 존재하지 않는 PostgreSQL 데이터베이스에 연결하려고 하기 때문이에요. 다음 두 섹션에서 데이터베이스 서비스와 모든 것을 함께 실행하는 데 필요한 Docker 구성을 추가해요.
nodejs-docker-example/src/index.ts (수정됨):
// Express application backed by a PostgreSQL database.
// Creates a heroes table at startup.
// Endpoints: GET / (greeting), GET /health (health check), POST /heroes/ (create), GET /heroes/ (list).
// See https://expressjs.com/ and https://node-postgres.com/
import express, { type Request, type Response } from "express";
import { Pool } from "pg";
import { readFileSync } from "fs";
const app = express();
const port = parseInt(process.env.PORT ?? "3000", 10);
app.use(express.json());
function getPassword(): string {
const passwordFile = process.env.POSTGRES_PASSWORD_FILE;
if (passwordFile) {
return readFileSync(passwordFile, "utf8").trim();
}
return process.env.POSTGRES_PASSWORD ?? "";
}
const pool = new Pool({
host: process.env.POSTGRES_SERVER,
port: 5432,
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: getPassword(),
});
pool
.query(
`CREATE TABLE IF NOT EXISTS heroes (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
secret_name TEXT NOT NULL,
age INTEGER
)`,
)
.catch(console.error);
app.get("/", (_req: Request, res: Response) => {
res.json({ message: "Hello World" });
});
app.get("/health", (_req: Request, res: Response) => {
res.json({ status: "ok" });
});
app.post("/heroes/", async (req: Request, res: Response) => {
const { name, secret_name, age } = req.body as {
name: string;
secret_name: string;
age?: number;
};
const result = await pool.query(
"INSERT INTO heroes (name, secret_name, age) VALUES ($1, $2, $3) RETURNING *",
[name, secret_name, age],
);
res.json(result.rows[0]);
});
app.get("/heroes/", async (_req: Request, res: Response) => {
const result = await pool.query("SELECT * FROM heroes");
res.json(result.rows);
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
nodejs-docker-example/package.json (수정됨):
{
"name": "nodejs-docker-example",
"version": "1.0.0",
"description": "A minimal Node.js TypeScript application.",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"express": "^4.21.2",
"pg": "^8.16.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@types/pg": "^8.11.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3"
}
}
Docker 자산 업데이트하기
Dockerfile과 compose.yaml을 다음으로 교체해요.
nodejs-docker-example/Dockerfile (수정됨):
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Development stage: install all dependencies, compile TypeScript, and
# serve with hot-reload. Used directly in development via compose.yaml.
FROM dhi.io/node:24-alpine3.23-dev AS dev
WORKDIR /app
# Install dependencies as a separate step to take advantage of Docker's
# caching. Leverage a cache mount to /root/.npm to speed up subsequent
# builds. Leverage a bind mount to package.json to avoid having to copy
# it into this layer.
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install
# Once you create a package-lock.json by running npm install locally, switch to npm ci and bind both files:
# RUN --mount=type=cache,target=/root/.npm \
# --mount=type=bind,source=package.json,target=package.json \
# --mount=type=bind,source=package-lock.json,target=package-lock.json \
# npm ci
# Copy the source code into the container and compile TypeScript.
COPY . .
RUN npm run build
# Expose the port that the application listens on.
EXPOSE 3000
# Run the application in development mode.
CMD ["npm", "run", "dev"]
# Deps stage: install production dependencies only.
FROM dhi.io/node:24-alpine3.23-dev AS deps
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install --omit=dev
# Once you create a package-lock.json by running npm install locally, switch to npm ci and bind both files:
# RUN --mount=type=cache,target=/root/.npm \
# --mount=type=bind,source=package.json,target=package.json \
# --mount=type=bind,source=package-lock.json,target=package-lock.json \
# npm ci --omit=dev
# Runner stage: minimal runtime image with compiled app and production deps.
FROM dhi.io/node:24-alpine3.23 AS runner
ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=dev --chown=node:node /app/dist ./dist
# Expose the port that the application listens on.
EXPOSE 3000
# Run the application.
CMD ["node", "dist/index.js"]
nodejs-docker-example/compose.yaml (수정됨):
services:
# Application service. The `target: dev` line builds the development
# image (includes tsx and dev tooling); the runner stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: dev
ports:
- 3000:3000
변경 사항에 대해 (About these changes)
컨테이너화 단계의 builder 스테이지는 dev로 이름이 바뀌고 EXPOSE 3000과 CMD ["npm", "run", "dev"]를 얻었으며, 이는 핫 리로드를 위해 tsx watch를 실행해요. deps와 runner 스테이지는 그대로예요.
compose.yaml에서 새 target: dev 줄은 개발 중 Compose가 dev 스테이지를 빌드·실행하도록 지시해요. 프로덕션 이미지와 달리 개발 이미지에는 tsx와 기타 개발 도구가 포함돼요. 실행 중인 프로덕션 컨테이너에서 셸이 필요하다면 Docker Debug를 대신 사용해요.
빌드 단계는 tsc를 실행하는데, 이는 각 TypeScript 파일을 해당 JavaScript 파일로 컴파일해요. esbuild는 모든 것을 단일 출력 파일로 번들하고 훨씬 빠르게 빌드하는 인기 있는 대안이에요. 전환하려면 package.json의 tsc 호출을 esbuild 명령으로 바꾸고 runner 스테이지의 COPY --from=dev 경로를 esbuild의 출력에 맞게 업데이트해요.
로컬 데이터베이스 추가 및 데이터 유지
컨테이너를 사용해 데이터베이스 같은 로컬 서비스를 구성할 수 있어요. 이 섹션에서는 compose.yaml 파일을 수정해 데이터베이스 서비스와 데이터를 유지할 볼륨을 정의하고, 데이터베이스 비밀번호를 담은 db/password.txt 파일을 추가할 거예요.
nodejs-docker-example/compose.yaml (수정됨):
services:
# Application service. The `target: dev` line builds the development
# image (includes tsx and dev tooling); the runner stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: dev
ports:
- 3000:3000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
# Database service. Reads the password from a Docker secret mounted at
# /run/secrets/db-password. Compose waits for the healthcheck to pass
# before starting the server, via the server's depends_on.
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
nodejs-docker-example/db/password.txt (새 파일):
mysecretpassword
[!NOTE] Compose 파일의 지시사항에 대해 더 알아보려면 Compose file reference를 참고해요.
이제 다음 docker compose up 명령을 실행해 어플리케이션을 시작해요.
$ docker compose up --build
브라우저에서 http://localhost:3000을 열어 {"message":"Hello World"} 를 확인해요.
POST 요청을 보내 영웅을 추가해요:
$ curl -X 'POST' \
'http://localhost:3000/heroes/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "my hero",
"secret_name": "austing",
"age": 12
}'
다음과 같은 응답을 받게 돼요:
{
"id": 1,
"name": "my hero",
"secret_name": "austing",
"age": 12
}
GET 요청을 보내 영웅을 나열해요:
$ curl http://localhost:3000/heroes/
다음과 같은 응답을 받게 돼요:
[
{
"id": 1,
"name": "my hero",
"secret_name": "austing",
"age": 12
}
]
서비스 자동 업데이트
Compose Watch를 사용하면 코드를 수정·저장할 때 실행 중인 Compose 서비스를 자동으로 갱신할 수 있어요.
compose.yaml을 IDE나 텍스트 편집기로 연 다음 Compose Watch 지시사항을 추가해요. 다음은 업데이트된 compose.yaml 파일이에요.
services:
# Application service. The `target: dev` line builds the development
# image (includes tsx and dev tooling); the runner stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: dev
ports:
- 3000:3000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
# Database service. Reads the password from a Docker secret mounted at
# /run/secrets/db-password. Compose waits for the healthcheck to pass
# before starting the server, via the server's depends_on.
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
다음 명령을 실행해 Compose Watch로 어플리케이션을 실행해요.
$ docker compose watch
터미널에서 어플리케이션을 curl로 호출해 응답을 받아요.
$ curl http://localhost:3000
{"message":"Hello World"}
로컬 머신의 어플리케이션 소스 파일에 가한 어떤 변경이든, 이제 실행 중인 컨테이너에 즉시 반영돼요.
nodejs-docker-example/src/index.ts를 IDE나 텍스트 편집기로 열고 Hello World 문자열에 느낌표를 몇 개 더 추가해요.
- res.json({ message: 'Hello World' });
+ res.json({ message: 'Hello World!!!' });
src/index.ts의 변경 사항을 저장하고 몇 초 동안 어플리케이션이 다시 로드될 때까지 기다려요. 어플리케이션을 다시 curl로 호출하고 업데이트된 텍스트가 나타나는지 확인해요.
$ curl http://localhost:3000
{"message":"Hello World!!!"}
터미널에서 ctrl+c를 눌러 어플리케이션을 중지해요.
어플리케이션 디버깅하기
tsx watch는 Node.js inspector 프로토콜을 지원하므로, VS Code나 Chrome DevTools에서 디버거를 연결하고 TypeScript 소스 파일에 직접 중단점(breakpoint)을 설정할 수 있어요.
package.json의 dev 스크립트를 업데이트해 inspector를 시작해요. --inspect=0.0.0.0:9229 플래그는 Node.js가 모든 네트워크 인터페이스의 9229 포트에서 디버거 연결을 수신하도록 지시해요. 컨테이너 밖에서 디버거에 도달하려면 localhost가 아니라 0.0.0.0을 사용해야 해요. 또한 compose.yaml에서 디버그 포트를 노출하고, 실행 중인 inspector에 VS Code가 어떻게 연결할지 알려주는 .vscode/launch.json 파일을 추가해요.
nodejs-docker-example/package.json (수정됨):
{
"name": "nodejs-docker-example",
"version": "1.0.0",
"description": "A minimal Node.js TypeScript application.",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch --inspect=0.0.0.0:9229 src/index.ts"
},
"dependencies": {
"express": "^4.21.2",
"pg": "^8.16.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@types/pg": "^8.11.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3"
}
}
nodejs-docker-example/.vscode/launch.json (새 파일):
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Docker Container",
"type": "node",
"request": "attach",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"protocol": "inspector",
"restart": true,
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"]
}
]
}
nodejs-docker-example/compose.yaml (수정됨) — server 서비스에 9229:9229 추가:
services:
# Application service. The `target: dev` line builds the development
# image (includes tsx and dev tooling); the runner stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: dev
ports:
- 3000:3000
- 9229:9229
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
이제 docker compose watch를 실행하고 VS Code의 Run and Debug 패널에서 "Attach to Docker Container" 구성을 선택하면 디버거가 실행 중인 컨테이너의 inspector에 연결돼요.
컨테이너에서 Node.js 테스트 실행하기
준비 사항 (Prerequisites)
이 가이드의 이전 섹션을 전부 완료해요 — Node.js 어플리케이션 컨테이너화부터 시작해서요.
테스트 추가하기
어플리케이션과 테스트를 위한 파일들을 추가하기 위해 src/index.ts를 app을 내보내도록 수정하고 src/index.test.ts를 만들어요. 테스트는 서버를 시작하지 않고 루트 엔드포인트를 검증해요.
nodejs-docker-example/src/index.ts (수정됨):
// Express application backed by a PostgreSQL database.
// Creates a heroes table at startup.
// Endpoints: GET / (greeting), GET /health (health check), POST /heroes/ (create), GET /heroes/ (list).
// See https://expressjs.com/ and https://node-postgres.com/
import express, { type Request, type Response } from "express";
import { Pool } from "pg";
import { readFileSync } from "fs";
export const app = express();
const port = parseInt(process.env.PORT ?? "3000", 10);
app.use(express.json());
function getPassword(): string {
const passwordFile = process.env.POSTGRES_PASSWORD_FILE;
if (passwordFile) {
return readFileSync(passwordFile, "utf8").trim();
}
return process.env.POSTGRES_PASSWORD ?? "";
}
const pool = new Pool({
host: process.env.POSTGRES_SERVER,
port: 5432,
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: getPassword(),
});
if (process.env.POSTGRES_SERVER) {
pool
.query(
`CREATE TABLE IF NOT EXISTS heroes (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
secret_name TEXT NOT NULL,
age INTEGER
)`,
)
.catch(console.error);
}
app.get("/", (_req: Request, res: Response) => {
res.json({ message: "Hello World" });
});
app.get("/health", (_req: Request, res: Response) => {
res.json({ status: "ok" });
});
app.post("/heroes/", async (req: Request, res: Response) => {
const { name, secret_name, age } = req.body as {
name: string;
secret_name: string;
age?: number;
};
const result = await pool.query(
"INSERT INTO heroes (name, secret_name, age) VALUES ($1, $2, $3) RETURNING *",
[name, secret_name, age],
);
res.json(result.rows[0]);
});
app.get("/heroes/", async (_req: Request, res: Response) => {
const result = await pool.query("SELECT * FROM heroes");
res.json(result.rows);
});
// Only start the server when this file is run directly.
if (require.main === module) {
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
}
nodejs-docker-example/src/index.test.ts (새 파일):
// Unit tests for the Express application.
// Tests the root endpoint without starting a server.
// See https://vitest.dev/ for the test framework reference.
import { describe, it, expect } from "vitest";
import request from "supertest";
import { app } from "./index";
describe("GET /", () => {
it("returns a JSON greeting", async () => {
const response = await request(app).get("/");
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: "Hello World" });
});
});
nodejs-docker-example/package.json (수정됨):
{
"name": "nodejs-docker-example",
"version": "1.0.0",
"description": "A minimal Node.js TypeScript application.",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"test": "vitest run"
},
"dependencies": {
"express": "^4.21.2",
"pg": "^8.16.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@types/pg": "^8.11.0",
"supertest": "^7.0.0",
"@types/supertest": "^6.0.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"vitest": "^3.0.0"
}
}
로컬에서 테스트 실행하기
로컬에서 테스트를 실행하려면 다음 명령을 실행해요:
$ npm install
$ npm test
다음과 같은 출력을 볼 수 있어요:
RUN v3.0.0 /app
✓ src/index.test.ts (1)
✓ GET / (1)
✓ returns a JSON greeting
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 12:00:00
Duration 500ms
컨테이너에서 테스트 실행하기
Dockerfile의 dev 스테이지를 사용해 테스트를 실행해요:
$ docker compose run --build --rm --no-deps server npm test
--no-deps 플래그는 단위 테스트에 필요 없으므로 데이터베이스 시작을 건너뛰어요. --rm 플래그는 테스트가 끝나면 컨테이너를 제거해요.
로컬에서 실행할 때와 같은 테스트 출력을 볼 수 있어요.
빌드할 때 테스트 실행하기
Docker 빌드 과정 중 테스트를 실행하려면 dev 스테이지 다음에 실행되는 test 스테이지를 Dockerfile에 추가해요.
FROM dhi.io/node:24-alpine3.23-dev AS dev
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "dev"]
FROM dhi.io/node:24-alpine3.23-dev AS deps
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install --omit=dev
FROM dhi.io/node:24-alpine3.23 AS runner
ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=dev --chown=node:node /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
FROM dev AS test
ENV CI=true
CMD ["npm", "test"]
그런 다음 test 스테이지를 빌드·실행해요:
$ docker build --target test -t nodejs-app-test .
$ docker run --rm nodejs-app-test
요약 (Summary)
이 섹션에서는 로컬에서 개발할 때와 컨테이너 안에서 테스트를 실행하는 방법을 배웠어요.
관련 정보: