개인 프로젝트에서 프론트와 백엔드를 한 서버에서 함께 운영하는 구조가 흔하다.
이번 글에서는 NestJS 백엔드와 Vite 기반 프론트엔드를 각각 도커 이미지로 빌드하고,
하나의 Docker Compose와 Nginx + HTTPS로 통합 배포한 전체 과정을 정리한다.
프로젝트 구조 (서버 상)
~/project/converter
├── docker-compose.yml # FE + BE 통합 Compose
├── be/ # NestJS 백엔드
├── fe/ # Vite + React 프론트엔드
Docker Compose 파일은 백엔드 디렉터리와 분리된 루트 경로에서 프론트와 백엔드 컨테이너를 함께 관리한다.
초기에는 한 서버에서 간단히 운영할 수 있도록 통합형 구조로 설정했다.
나중에 트래픽이 많아지거나 인프라가 커지면, 프론트와 백엔드를 각각의 리포지토리와 서버로 분리하는 것도 고려할 예정이다.
그 전까지는 관리 효율을 위해 하나의 Compose 파일로 통합 운영한다.
1. 프론트엔드 Dockerfile (개발 환경)
fe/Dockerfile
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
프론트엔드 코드를 빌드한 뒤, Nginx 이미지로 결과물을 서빙하는 구조다.
멀티 스테이지 빌드를 사용해 빌드 환경과 운영 환경을 분리했다.
dist 디렉토리의 정적 파일만 Nginx로 전달하므로 이미지 크기도 줄고 실행 속도도 빠르다.
fe/nginx.conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
React의 SPA 특성상 새로고침 시에도 index.html을 기본 진입점으로 설정해야 한다.
try_files 옵션으로 존재하지 않는 경로는 전부 index.html로 fallback 되도록 설정했다.
Dockerfile에서 이 설정을 함께 복사해 Nginx가 정적으로 잘 서빙할 수 있도록 했다.
2. docker-compose.yml (개발 환경)
be/docker-compose.yml
services:
nest:
image: ghcr.io/bingki/be:latest
container_name: nest-app
restart: unless-stopped
ports:
- "3002:3002"
networks:
- app-network
fe:
image: ghcr.io/bingki/fe:latest
container_name: fe
restart: unless-stopped
ports:
- "8080:80"
networks:
- app-network
networks:
app-network:
driver: bridge
프론트엔드(fe)와 백엔드(nest)를 각각 독립된 Docker 이미지로 구성하고,
docker-compose.yml 하나로 함께 실행되도록 설정했다.
- restart: unless-stopped로 서버 재시작 시 자동 복구 가능
- ports는 각 서비스의 내부 포트를 외부로 노출
- FE는 Nginx 기본 포트(80)를 8080으로 매핑
- BE는 NestJS의 3002 포트를 그대로 노출
- app-network라는 공용 네트워크를 설정해 두 컨테이너가 내부적으로 통신 가능하도록 구성했다.
3. Nginx 설정 (서버 상)
(서브도메인 분리: api.example.com / example.com)
프론트엔드와 백엔드를 명확히 구분하기 위해 서브도메인을 분리했다.
- example.com: 프론트엔드 서비스
- api.example.com: 백엔드 API
이렇게 분리하면 보안 정책 설정, 트래픽 분산, 쿠키 설정 분리 등 다양한 이점이 있다.
또한 나중에 프론트와 백엔드를 다른 서버로 확장할 때도 유연하게 대응할 수 있다.
/etc/nginx/sites-available/example.com
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://localhost:8080; # 프론트엔드 컨테이너
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
프론트엔드는 React 정적 파일을 Nginx가 서빙하고 있으며,
HTTPS 요청은 Nginx가 SSL을 처리하고 8080 포트의 컨테이너로 프록시한다.
80 포트 접근은 모두 443으로 리다이렉트된다.
/etc/nginx/sites-available/api.example.com
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://localhost:3002; # 백엔드 컨테이너
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
NestJS 백엔드는 3002 포트에서 동작 중이고,
Nginx가 SSL을 종료한 뒤 내부로 요청을 전달한다.
이 역시 HTTP 요청은 전부 HTTPS로 리다이렉션된다.
설정 후 심볼릭 링크 추가:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo ln -s /etc/nginx/sites-available/api.example.com /etc/nginx/sites-enabled/api.example.com
sudo nginx -t && sudo systemctl reload nginx
sites-available 디렉토리에는 모든 Nginx 설정 파일이 들어가고,
sites-enabled에 연결된 설정만 실제로 적용된다.
따라서 설정 파일을 작성한 후 ln -s 명령어로 심볼릭 링크를 생성해야 활성화된다.
이후 nginx -t로 설정 문법 검사를 하고, 문제가 없다면 systemctl reload nginx로 적용한다.
4. HTTPS 인증서 발급 (Let’s Encrypt + Certbot)
혹시 아직 발급받지 않았다면 등록하자.
sudo certbot --nginx -d example.com
sudo certbot --nginx -d api.example.com
api 이름의 레코드도 DNS에 등록해야 한다.
방법은 아래 게시물을 참조하면 된다. 이름에 api 추가하고 동일하게 작업하면 된다.
[Contabo] 도메인 연결하기: 콘타보 서버에서 프로젝트 운영하기 (feat. Clouldflare)
앞선 글에서 콘타보 인스턴스에 Docker와 프로젝트를 배포하는 과정을 정리했다.이제 다음 단계로 도메인 연결, Nginx 설정, 방화벽 설정을 마무리해보자. 1. 도메인 연결 (Cloudflare) 이미 Cloudflare에
iwillcomplete.tistory.com
최종 확인
curl https://example.com # 프론트엔드 접속
curl https://api.example.com # 백엔드 접속
드디어 내 사이트를 어디서든 확인할 수 있다. 😆