집에 나스가 있어서 기존에 만든 NestJS 앱을 올려서 개발서버용으로 구축을 진행해보았다.
상당한 고난 끝에 해결되었지만, 나스는 워낙 느리기 때문에 고생을 좀 했는데 나스 처음 구축부터 배포까지 자세히 적어볼 생각이다.
1. DDNS 설정
우선 나스에서 DDNS 설정을 해야 외부에서 접근이 가능해진다. (웹에서 나스로 접속하기 위함)
제어판 - 외부 액세스로 접속한 뒤 추가버튼을 누르거나 기존에 있는 DDNS를 편집한다.
서비스 공급자를 선택하면 호스트 이름 뒤에 붙는다.
예를 들어 호스트 이름 앞이 test01이고, 서비스 공급자를 Synology로 선택하면 test01.synology.me 가 된다.
외부 주소가 자동으로 선택된 상태에서 연결 테스트를 누르면 특별히 문제가 있지 않는한 정상이 뜬다.
기본 인증서를 잘 선택하고 확인을 눌러 마무리한다. (따로 설명하지 않겠음)
2. Docker 패키지 설치
패키지 센터에서 docker를 검색 후 설치하여 실행한다.
3. SSH 활성화
SSH는 보안된 채널을 통해 네트워크 상의 다른 컴퓨터에 접속하고 명령을 실행하거나 파일을 복사하는 데 사용되는 프로토콜을 의미하는데, 좀 더 쉽게 생각하면 외부 터미널(맥북이나 윈도우)을 통해 나스에 접속하는 것을 의미한다.
제어판 - 터미널 및 SNMP
에서 SSH 서비스 활성화를 체크하고 적용하면 된다.
ssh 시놀로지아이디@호스트이름
터미널에서 위와 같이 입력하면 되는데,
ssh NasID01@test01.synology.me
이런 식으로 터미널에 입력하면 패스워드를 입력 후 접속이 가능해진다.
4. 도커 방화벽 설정
5. 도커의 실행 사이클
- 명령어 실행
- docker-compose up -d --build
- Dockerfile 실행
- docker-compose.yaml 적용
- Docker Container 실행 (App, DB, Redis 등)
뜬금없이 실행 사이클을 보는 이유는 앞으로 Dockerfile과 docker-compose.yaml 파일을 생성하고 이것들의 실행동작 여부와 도커 내의 컨테이너가 어떻게 실행되는지를 미리 알아야 앞으로의 작업이 편하기 때문이다.
6. pnpm 패키지로 설정
NAS는 굉장히 느리기 때문에 도커 컨테이너로 만드는 과정에서 패키지를 install 하는 과정에서 좀 더 빠른 패키지 매니저가 필요하게 되었다.
결과적으로 속도면에서 가장 빠른 pnpm을 적용하게 되었는데, 속도 측면에서 자세한 사항은 아래 블로그에서 확인이 가능하다.
https://hackle.io/ko/blog/post/frontend-pnpm/
핵클 블로그(Hackle Blog) : 핵클팀이 pnpm을 도입한 이유
hackle.io
크게 어려울건 없고 아래 명령어를 통해 pnpm을 설치할 수 있다.
npm install -g pnpm
만약 기존에 yarn이 설치되어있다면 제거하고 install 하여야 한다.
# 기존 파일 제거
rm -rf node_modules package-lock.json yarn.lock
# pnpm으로 설치
pnpm install
# 기존 스크립트 그대로 사용
pnpm run dev
pnpm run build
pnpm test
7. 프로젝트 git clone
cd /volume1/docker
ssh로 나스에 접속한 후 위와 같은 명령어로 docker 폴더 내부로 진입한다.
git clone https://github.com/.../.git
그리고 git clone을 하여 프로젝트를 받아온다.
성공적으로 마무리되면 docker 폴더 내부에 프로젝트가 생성된 것을 볼 수 있다.
8. Dockerfile, docker-compose.yaml, .env 파일 생성
DB, Redis, App 등을 컨테이너에 올리고 실행하기 위한 환경설정 파일들을 생성해야 한다.
8.1 프로젝트로 이동
cd 프로젝트명
// ex)
cd /volume1/docker/NestJS_SNS
8.2. 기존 로컬에서 사용하던 .env 파일 설정
cat > src/configs/env/.dev.env << 'EOF'
JWT_SECRET=jwtsecret
HASH_ROUNDS=10
PROTOCOL=http
HOST=...:3000
DB_HOST=postgres
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=postgres
DB_SYNC=true
SWAGGER_USER=junny
SWAGGER_PASSWORD=junny5432
GOOGLE_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=...
GOOGLE_CALLBACK_URL=http://...
KAKAO_CLIENT_ID=...
KAKAO_CALLBACK_URL=http://.../auth/kakao/callback
KAKAO_LOGOUT_REDIRECT_URI=http://...:3000/auth/kakao/logout/callback
REDIS_HOST=redis
REDIS_PORT=6379
EOF
기존 로컬에서 사용하는 .env와 개발서버에서 사용할 .env의 내용은 IP 주소같은 부분들이 달라지게 된다.
8.3. Dockerfile 작성
cat > Dockerfile << 'EOF'
FROM node:20-alpine
# 공식 npm 레지스트리 사용
RUN npm config set registry https://registry.npmjs.org/
# pnpm 설치
RUN npm install -g pnpm
RUN pnpm config set registry https://registry.npmjs.org/
# 기본 빌드 도구 설치
RUN apk add --no-cache python3 make g++
WORKDIR /app
# package.json과 pnpm-lock.yaml 먼저 복사
COPY package.json pnpm-lock.yaml ./
# 의존성 설치
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store \
pnpm config set store-dir /root/.pnpm-store && \
pnpm install --frozen-lockfile
# 소스코드 복사
COPY . .
# 필요한 디렉토리 생성 및 권한 설정 (Compodoc 하위 폴더 포함)
RUN mkdir -p logs uploads dist docs tmp && \
mkdir -p docs/graph docs/modules docs/images docs/js docs/css && \
chmod -R 777 logs uploads dist docs tmp src && \
chown -R node:node /app
# 시작 스크립트 생성 (node 사용자로 실행하도록 수정)
RUN echo '#!/bin/sh' > /start-services.sh && \
echo 'echo "🚀 Starting NestJS + Compodoc services..."' >> /start-services.sh && \
echo '' >> /start-services.sh && \
echo '# NestJS 개발 서버 시작 (백그라운드)' >> /start-services.sh && \
echo 'echo "📡 Starting NestJS API server on port 3000..."' >> /start-services.sh && \
echo 'pnpm run start:dev &' >> /start-services.sh && \
echo 'NESTJS_PID=$!' >> /start-services.sh && \
echo '' >> /start-services.sh && \
echo '# NestJS 서버가 시작될 때까지 대기' >> /start-services.sh && \
echo 'echo "⏳ Waiting for NestJS to initialize..."' >> /start-services.sh && \
echo 'sleep 30' >> /start-services.sh && \
echo '' >> /start-services.sh && \
echo '# docs 폴더 권한 재확인' >> /start-services.sh && \
echo 'chmod -R 777 /app/docs' >> /start-services.sh && \
echo '' >> /start-services.sh && \
echo '# Compodoc 문서 서버 시작 (백그라운드)' >> /start-services.sh && \
echo 'echo "📚 Starting Compodoc documentation server on port 8080..."' >> /start-services.sh && \
echo 'pnpm run docs:watch &' >> /start-services.sh && \
echo 'DOCS_PID=$!' >> /start-services.sh && \
echo '' >> /start-services.sh && \
echo '# 프로세스 상태 확인' >> /start-services.sh && \
echo 'sleep 10' >> /start-services.sh && \
echo 'echo "✅ Services Status:"' >> /start-services.sh && \
echo 'echo " 📡 NestJS API : http://localhost:3000"' >> /start-services.sh && \
echo 'echo " 📚 Compodoc Docs : http://localhost:8080"' >> /start-services.sh && \
echo 'echo " 🔍 Health Check : http://localhost:3000/health"' >> /start-services.sh && \
echo 'echo " 📋 Swagger API : http://localhost:3000/api-docs"' >> /start-services.sh && \
echo 'echo " 🚀 GraphQL : http://localhost:3000/graphql"' >> /start-services.sh && \
echo '' >> /start-services.sh && \
echo '# 프로세스들이 계속 실행되도록 대기' >> /start-services.sh && \
echo 'wait $NESTJS_PID $DOCS_PID' >> /start-services.sh && \
chmod +x /start-services.sh && \
chown node:node /start-services.sh
# non-root 사용자로 전환
USER node
EXPOSE 3000 8080
# 멀티 서비스 시작 스크립트 실행
CMD ["/start-services.sh"]
EOF
Dockerfile은 기본 이미지 선택, 필요한 파일 복사, 명령어 실행, 포트 설정 등을 지정하여, 컨테이너 이미지를 만들 수 있도록 하여 사용자마다 다른 의존성 차이를 없도록 하는 역할을 한다.
Dockerfile은 docker-compose.yaml의 build 구문을 통해서 실행된다.
8.4. docker-compose.yaml 작성
cat > docker-compose.yaml << 'EOF'
version: '3.8'
services:
app:
build: .
container_name: sns-dev-app
restart: unless-stopped
ports:
- "3000:3000" # NestJS API
- "8080:8080" # Compodoc Documentation
environment:
- NODE_ENV=dev
env_file:
- src/configs/env/.dev.env
volumes:
- ./src:/app/src
- ./logs:/app/logs
- ./uploads:/app/uploads
- pnpm-store:/root/.pnpm-store
- /app/node_modules
dns:
- 168.126.63.1
- 8.8.8.8
- 8.8.4.4
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- dev-network
healthcheck:
test: ["CMD", "sh", "-c", "wget --quiet --tries=1 --spider http://localhost:3000/health && wget --quiet --tries=1 --spider http://localhost:8080"]
interval: 30s
timeout: 15s
retries: 3
start_period: 90s
postgres:
image: postgres:15-alpine
container_name: sns-dev-postgres
restart: always
ports:
- "5433:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
volumes:
- ./postgres-data:/var/lib/postgresql/data
networks:
- dev-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
redis:
image: redis:7.4-alpine
container_name: sns-dev-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- dev-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 3
volumes:
pnpm-store:
driver: local
redis-data:
driver: local
networks:
dev-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
EOF
위와 같이 docker-compose.yaml을 작성하면, sudo docker-compose up -d --build 를 통해 설정된 컨테이너로 만들 수 있다.
각 컨테이너별로 API 서버, 레디스 서버, DB 서버 등으로 구축이 가능한 것이다.
참고로 Dockerfile에서 권한을 설정했다면, docker-compose.yaml의 volumes 에는 제외하여야 권한 설정이 제대로 진행된다.
9. 배포 실행
9.1. 서비스 실행
# Docker Compose 실행
sudo docker-compose up -d --build
# 빌드 진행 상황 확인
sudo docker-compose logs -f app
# 모든 서비스 상태 확인
sudo docker-compose ps
# 예상 결과:
# Name Command State Ports
# sns-dev-app npm run start:dev Up 0.0.0.0:3000->3000/tcp
# sns-dev-postgres docker-entrypoint.sh Up 0.0.0.0:5433->5432/tcp
# sns-dev-redis docker-entrypoint.sh Up 0.0.0.0:6379->6379/tcp
9.3. 서비스별 로그 확인
# NestJS 앱 로그
sudo docker-compose logs app
# PostgreSQL 로그
sudo docker-compose logs postgres
# Redis 로그
sudo docker-compose logs redis
# 실시간 앱 로그
sudo docker-compose logs -f app
참고로 NAS의 Docker 컨테이너 내부에서도 동일하게 로그를 볼 수 있다.
10. 접속 테스트
10.1. API 서버 접속
# 맥북에서 테스트 (SSH 종료 후)
exit
# 헬스체크 (엔드포인트 있다면)
curl http://...:3000/health
# 기본 API 테스트
curl http://...:3000
# Swagger UI 접속 (브라우저에서)
# http://.../api
# ID: ..., PW: ....
10.2. 데이터베이스 접속 테스트
# PostgreSQL 직접 접속 (DBeaver, TablePlus 등에서)
Host: ...
Port: 5433
Database: postgres
Username: postgres
Password: postgres
10.3. Redis 접속 테스트
# Redis CLI 테스트 (Redis 클라이언트에서)
Host: ...
Port: 6379
# 패스워드 없음
11. 성공 결과
성공하면 ssh 터미널에서 위와 같이 보여진다.
NAS 내부의 Docker 에서도 실행 중인 컨테이너를 확인할 수 있다.
12. Postman 테스트
로그에도 특별히 이상이 없었고, 접속 테스트까지 문제가 없었다면 이제 본격적으로 Postman 테스트를 진행해볼 수 있다.
우선 개발서버용 컬렉션을 하나 만든다.
작업을 수행할 Request를 하나 추가한다.
당연히 개발 DB는 비어있으므로, 회원가입하는 API 엔드포인트를 만들어야 한다.
로컬에서 구현하고 git으로 올리고 나스에서 git pull 받고 해당 프로젝트의 소스코드가 컨테이너로 만들어져 서버가 구동되고 있는 것이기 때문에, 로컬에서 테스트한 API 엔드포인트를 그대로 사용 가능하다.
다만 환경변수에서 localhost:3000이 아닌 나스에서 접속 가능한 호스트(ip)를 입력하여야 한다.
정상적으로 진행된다면 로컬 테스트와 동일하게 반환될 것이다.
13. DB 연결 (pgAdmin4)
각종 다양한 DB가 있겠지만, 나는 Postgres 를 사용하였고 로컬도 pgAdmin4 를 사용하였기에 이것으로 연결해보도록 하겠다.
상단 Object - Register - Server
General 과 Connection 부분에 적절히 잘 입력을 하면 된다. 참고로 Port는 5432가 충돌나는 경우가 있어서 5433으로 진행하면 된다. (docker-compose.yaml 참고)
연결이 완료되면 위와 같이 생성된다.
Postman으로 신규 회원을 생성하였으니 문제가 없다면 정상적으로 조회가 된다.
나스 Docker의 DB 컨테이너에서도 로그가 기록된다.
14. ssh 기타 명령어
# 로그 확인
sudo docker-compose logs -f app
# 모든 것 정리
sudo docker-compose down
sudo docker system prune -af
# 새로 빌드
sudo docker-compose up -d --build
# 로그 확인
sudo docker-compose logs -f app
sudo docker-compose up -d # 기존 이미지 사용
sudo docker-compose up -d --build # 강제로 다시 빌드
# 10분 타임아웃으로 실행
sudo COMPOSE_HTTP_TIMEOUT=600 DOCKER_CLIENT_TIMEOUT=600 docker-compose up -d
15. 에러 해결
15.1. bcrypt 이슈
Error: Cannot find module '/app/node_modules/.pnpm/bcrypt@5.1.1/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node'
Require stack:
위와 같은 문제는 기존 bcrypt를 지우고 bcryptjs 를 설치하면 해결된다.
pnpm remove bcrypt @types/bcrypt
pnpm add bcryptjs @types/bcryptjs
15.2. 권한 없음 이슈
sns-dev-app | node:events:502
sns-dev-app | throw er; // Unhandled 'error' event
sns-dev-app | ^
sns-dev-app |
sns-dev-app | Error: EACCES: permission denied, open 'logs/error-2025-06-18.log'
sns-dev-app | Emitted 'error' event at:
sns-dev-app | at WriteStream.<anonymous> (/app/node_modules/.pnpm/file-stream-rotator@0.6.1/node_modules/file-stream-rotator/FileStreamRotator.js:697:15)
sns-dev-app | at WriteStream.emit (node:events:524:28)
sns-dev-app | at emitErrorNT (node:internal/streams/destroy:169:8)
sns-dev-app | at emitErrorCloseNT (node:internal/streams/destroy:128:3)
sns-dev-app | at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
sns-dev-app | errno: -13,
sns-dev-app | code: 'EACCES',
sns-dev-app | syscall: 'open',
sns-dev-app | path: 'logs/error-2025-06-18.log'
sns-dev-app | }
sns-dev-app |
sns-dev-app | Node.js v20.19.2
이 부분은 Dockerfile 및 docker-compose.yaml 에서 권한 설정으로 해결이 가능하다. 만약 문제가 있다면 Dockerfile에서 권한 설정을 했지만 docker-compose.yaml의 volumes에서 권한을 덮어버리는 이슈일 가능성이 높다.
# logs 디렉터리를 UID 1000:GID 1000 소유로 변경
sudo chown -R 1000:1000 /path/to/your/nas/share/logs
# 필요하다면 퍼미션도 조정
sudo chmod -R 755 /path/to/your/nas/share/logs
만약 ssh에서 해당 폴더에 권한을 주고싶다면 위와 같은 명령어로 권한 설정이 가능하다.
'NestJS' 카테고리의 다른 글
Github Actions를 사용한 CI/CD - 롤백, 모니터링, 로그 정리 (0) | 2025.07.02 |
---|---|
Github Actions를 사용한 CI/CD 구축 (0) | 2025.06.30 |
[NestJS] winston 로그 구현하기 (1) | 2025.06.05 |
[NestJS] 단위테스트 (1) | 2025.06.04 |
[NestJS] 자동 문서화 가이드 (Compodoc) (1) | 2025.05.30 |