본문 바로가기
서버, 인프라

[월 1만원으로 내 서비스 올리기] Next.js + Spring Boot, Hetzner 서버에 직접 올려봤습니다 — Docker Compose + Nginx 세팅 4편

by 요즘IT 2026. 5. 24.

GitHub SSH Key 등록부터 Docker Compose로 Next.js·Spring Boot 올리고 Nginx 리버스 프록시까지 실제 삽질 기반으로 정리했어요.


이전 글:  [서버, 인프라] - Docker + Jenkins, 서버에 직접 올려봤습니다 — Hetzner 실전 세팅 3편

 


3편에서 Docker 설치하고 Jenkins 띄웠잖아요.

이번엔 드디어 실제 서비스를 올리는 단계예요.

프론트엔드(Next.js)랑 백엔드(Spring Boot) 둘 다 Docker로 올리고, Nginx로 묶는 것까지 할 거거든요. 근데 생각보다 변수가 꽤 있었어요. 삽질한 부분도 다 넣었으니까 그냥 따라오시면 돼요.


GitHub Private 레포 — SSH Key로 연결하기

서버에서 GitHub 코드를 가져와야 하는데, 레포가 Private이거든요.

HTTPS + Personal Access Token 방식도 있는데, SSH Key 방식이 훨씬 편해요. 한 번만 설정해두면 나중에 Jenkins CI/CD 연동할 때도 그대로 써먹을 수 있거든요.

서버에서 SSH Key 생성

ssh-keygen -t ed25519 -C "hetzner-server"

엔터 세 번 누르면 기본값으로 생성돼요.

공개키 확인

cat ~/.ssh/id_ed25519.pub

ssh-ed25519 AAAA... 로 시작하는 값 전체 복사해두세요.

GitHub 레포 Settings → Deploy keys 화면에서 SSH 키를 등록
GitHub 레포 Settings → Deploy keys 화면에서 SSH 키를 등록

GitHub에 등록

여기서 잠깐. Deploy key는 레포 하나에만 등록할 수 있어요. 프론트/백엔드 레포가 각각 있으면 같은 키를 두 레포에 못 쓰거든요.

Key is already in use 에러 나면 계정 레벨에서 등록하면 돼요.

GitHub → 프로필 → Settings → SSH and GPG keys → New SSH key

  • Title: hetzner-server
  • Key type: Authentication Key
  • Key: 공개키 붙여넣기

이러면 해당 계정의 모든 레포에 접근 가능해요.

Clone 테스트

git clone git@github.com:계정명/프론트레포.git
git clone git@github.com:계정명/백엔드레포.git

처음 연결 시 Are you sure you want to continue connecting? 나오면 yes 입력하면 돼요.


프론트엔드 — Next.js Docker 빌드

Clone 받은 프론트 폴더로 이동해서 Dockerfile 확인했더니 CMD ["yarn", "start"] 로 돼있었어요. SSR 방식이라 Node 서버가 필요한 구조거든요.

두 가지 수정이 필요했어요.

1. 포트 지정

yarn start는 기본이 3000포트인데, 저는 10000으로 쓸 거라 명시해줬어요.

CMD ["yarn", "start", "-p", "10000"]

2. 환경변수 처리

여기서 진짜 중요한 포인트가 있어요.

NEXT_PUBLIC_ 변수는 빌드 시점에 번들에 박혀요. 런타임에 주입이 안 된다는 거거든요. 그래서 .env에 localhost로 돼있으면 컨테이너 안에서 자기 자신만 바라보게 돼요.

Dockerfile에 ARG/ENV로 넣어줘야 해요.

ARG NEXT_PUBLIC_API_URL=http://서버IP/api
ARG NEXT_PUBLIC_FRONT_URL=http://서버IP
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_FRONT_URL=$NEXT_PUBLIC_FRONT_URL

RUN yarn build

빌드 전에 선언해야 번들에 반영돼요. 순서 중요해요.

이미지 빌드

docker build -t front .

빌드 시간이 8~9분 걸렸어요. yarn install이랑 yarn build가 시간을 많이 잡아먹거든요.


백엔드 — Spring Boot Docker 빌드

Spring Boot는 Dockerfile이 JAR 파일을 복사하는 방식이라 Gradle 빌드 먼저 해야 해요.

Gradle 빌드

chmod +x gradlew
./gradlew clean build -x test

-x test 는 테스트 스킵이에요. 서버에서 DB 연결 없이 테스트 돌리면 실패하거든요.

빌드 완료되면 build/libs/ 아래 JAR 파일 생겼는지 확인해요.

ls build/libs/

Docker 이미지 빌드

근데 여기서 삽질이 있었어요.

Dockerfile에 FROM openjdk:17-jdk-slim 으로 돼있었는데, 이 이미지가 Docker Hub에서 deprecated 돼서 없어졌거든요.

ERROR: openjdk:17-jdk-slim: not found

이렇게 나오면 eclipse-temurin으로 바꾸면 돼요.

sed -i 's/FROM openjdk:17-jdk-slim/FROM eclipse-temurin:17-jdk-jammy/' Dockerfile

그리고 이 변경사항은 GitHub에도 바로 올려두는 게 좋아요. 나중에 Jenkins에서 또 같은 에러 만나거든요.

git add Dockerfile gradlew
git commit -m "fix: openjdk -> eclipse-temurin, gradlew 실행권한 추가"
git push origin main

이미지 빌드

docker build -t api .

18초면 끝나요. JAR 파일 복사만 하면 되니까 프론트보다 훨씬 빠르거든요.


Nginx — 리버스 프록시 설정

프론트/백엔드 이미지 다 만들어졌으면 Nginx로 묶을 차례예요.

어? 왜 Nginx가 필요하냐고요?

지금은 프론트가 10000포트, 백엔드가 10081포트로 각각 떠있거든요. 사용자 입장에서 서버IP:10000, 서버IP:10081 이렇게 포트 붙여서 접속해야 하는 게 이상하잖아요.

Nginx가 80포트로 들어오는 요청을 받아서 URL 경로에 따라 프론트/백엔드로 나눠주는 거예요.

http://서버IP/        → Next.js (10000포트)
http://서버IP/api/    → Spring Boot (10081포트)

한마디로 Nginx가 교통정리 해주는 거예요.

근데 여기서 포인트가 있어요. 백엔드 API 경로에 /api prefix가 없으면 Nginx가 프론트/백엔드 요청을 구분 못해요.

그래서 Spring Boot application.yml에 context-path 추가했어요.

server:
  servlet:
    context-path: /api

이러면 모든 백엔드 API가 /api/reserve, /api/pay 이런 식으로 바뀌어요. Nginx에서 /api/ 로 시작하는 건 백엔드로, 나머지는 프론트로 보내면 깔끔하게 라우팅돼요.

그리고 이게 CORS 문제도 자연히 해결해줘요. 프론트/백엔드가 같은 http://서버IP origin으로 통신하게 되거든요.

Nginx 설정 파일 생성

mkdir -p ~/app/nginx

cat > ~/app/nginx/default.conf << 'EOF'
server {
    listen 80;

    location / {
        proxy_pass http://front:10000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /api/ {
        proxy_pass http://api:10081/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
EOF

proxy_pass에서 컨테이너 이름(front, api)으로 연결하는 게 포인트예요. Docker Compose로 띄우면 컨테이너끼리 이름으로 통신할 수 있거든요.

Nginx가 80포트에서 요청을 받아 프론트/백엔드 컨테이너로 라우팅하는 구조도
Nginx가 80포트에서 요청을 받아 프론트/백엔드 컨테이너로 라우팅하는 구조도


Docker Compose — 한 번에 묶기

설정 다 됐으면 docker-compose.yml 만들어서 한 번에 올릴게요.

cat > ~/app/docker-compose.yml << 'EOF'
services:
  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - front
      - api

  front:
    image: front
    container_name: front
    restart: unless-stopped
    ports:
      - "10000:10000"

  api:
    image: api
    container_name: api
    restart: unless-stopped
    ports:
      - "10081:10081"
EOF

depends_on으로 프론트/백엔드가 뜨고 나서 Nginx가 시작되게 순서 잡아뒀어요.

실행

cd ~/app
docker compose up -d

확인

docker ps

nginx, front, api 세 개 다 Up 상태면 완료예요.

브라우저에서 http://서버IP 접속하면 Next.js 화면 나오고, API 호출도 /api/ 경로로 백엔드로 잘 붙는 거 확인했어요.


현재 서버 구성 최종

[Hetzner CX33 - Helsinki]
├── Nginx          → 80포트 (리버스 프록시)
│   ├── /          → Next.js (10000)
│   └── /api/      → Spring Boot (10081)
├── Jenkins        → 8090포트
└── [AWS RDS]      → MySQL 외부 연결

결국 핵심은 세 가지예요.

NEXT_PUBLIC_ 변수는 빌드 시점에 번들에 박히니까 Dockerfile에서 처리해야 하고, openjdk:17-jdk-slim은 deprecated됐으니까 eclipse-temurin으로 바꿔야 하고, 백엔드에 /api context-path 붙여야 Nginx 라우팅이 깔끔하게 돼요.

다음 편에서는 Jenkins 파이프라인 구성해서 GitHub push하면 자동으로 빌드/배포되게 만들어볼게요.

 

다음 글: [서버, 인프라] - GitHub push 하면 자동으로 배포된다고? Jenkins CI/CD 파이프라인 직접 구성해봤습니다 — Hetzner 실전 세팅 5편