수강신청 연습 사이트의 개발 서버가 필요했다.
Spring Boot + mariadb 백엔드 개발 서버를 docker container로 띄우고, nginx를 통해 외부에 expose하는 방법을 단계별로 정리한다.
.
├── build.gradle
├── docker-compose.yml
├── docker-files
│ ├── backend
│ └── nginx
├── gradle
│ └── wrapper
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
└── test
컨테이너들을 관리하는 파일이다.
services:
backend:
build:
context: . # 현재 디렉토리를 빌드 컨텍스트로 사용
dockerfile: docker-files/backend/Dockerfile # 백엔드 도커파일 위치 지정
container_name: cr-backend
ports:
- "${BACKEND_PORT}:8080" # 호스트의 환경변수 포트를 컨테이너의 8080포트와 매핑
volumes:
- .:/workspace/app # 현재 디렉토리를 컨테이너의 /workspace/app에 마운트하여 실시간 코드 반영
networks:
- app-network # 컨테이너들 간의 통신을 위한 네트워크 설정
restart: unless-stopped # 컨테이너가 중단되면 자동으로 재시작 (수동 중단 제외)
nginx:
image: nginx:alpine # 경량화된 nginx 알파인 리눅스 버전 사용
container_name: cr-nginx
ports:
- "${NGINX_PORT}:80" # 호스트의 환경변수 포트를 nginx의 80포트와 매핑
volumes:
- ./docker-files/nginx/nginx.conf:/etc/nginx/conf.d/default.conf # nginx 설정 파일 마운트
depends_on:
- backend # 백엔드 컨테이너가 시작된 후에 nginx 컨테이너 시작
networks:
- app-network # 다른 서비스들과 통신하기 위한 네트워크 연결
restart: unless-stopped # 컨테이너 장애 시 자동 재시작
mariadb:
image: mariadb:latest # 최신 버전의 MariaDB 이미지 사용
container_name: cr-mariadb
environment: # MariaDB 설정을 위한 환경변수들
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} # root 비밀번호
MARIADB_DATABASE: ${DB_NAME} # 생성할 데이터베이스 이름
MARIADB_USER: ${DB_USER} # 생성할 사용자 이름
MARIADB_PASSWORD: ${DB_PASSWORD} # 사용자 비밀번호
ports:
- "${DB_PORT}:3306" # 호스트의 환경변수 포트를 MariaDB의 3306포트와 매핑
volumes:
- mariadb-data:/var/lib/mysql # 데이터 영속성을 위한 볼륨 마운트
networks:
- app-network # 다른 서비스들과의 통신을 위한 네트워크 연결
restart: unless-stopped # 컨테이너 장애 시 자동 재시작
networks:
app-network:
driver: bridge # 컨테이너들이 서로 통신할 수 있게 해주는 브릿지 네트워크 사용
volumes:
mariadb-data: # MariaDB 데이터를 영구적으로 저장하기 위한 도커 볼륨 정의
Backend: Spring Boot 애플리케이션
Web Server: Nginx
Database: MariaDB
ports
: 외부포트:내부포트 형식
예: "8081:8080"은 컴퓨터의 8081포트로 접속하면 컨테이너의 8080포트로 연결
volumes
: 데이터를 저장하거나 파일을 공유할 때 사용. 컨테이너가 삭제되어도 데이터는 유지됨
networks
: 컨테이너들이 서로 통신할 수 있게 해주기 위해 설정. 여기서는 모든 프로그램이 같은 네트워크를 사용
depends_on
: 특정 서비스가 시작된 후에 실행
예: nginx는 backend가 시작된 후에 실행
공유기 포트포워딩을 설정하고 nginx를 통해 외부 요청을 프록시했다.
외부 요청 -> 집 공유기(XXXX) -> 내 노트북(XXXX) -> Docker의 nginx 컨테이너(80) -> Docker의 backend 컨테이너(8080)
# 빌드 스테이지
# 빌드 스테이지
FROM amazoncorretto:17-alpine as build
WORKDIR /workspace/app
COPY gradle gradle
COPY build.gradle settings.gradle gradlew ./
COPY src src
RUN chmod +x ./gradlew
RUN ./gradlew bootJar
RUN mkdir -p build/libs
# 실행 스테이지
FROM amazoncorretto:17-alpine
VOLUME /tmp
COPY --from=build /workspace/app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "/app.jar"]
위 Dockerfile은 두 단계로 나누어져 있다. Spring Boot 애플리케이션을 실행 가능한 jar 파일로 만드는 '빌드 단계'와 만들어진 jar 파일을 실제로 실행하는 '실행 단계'이다.
FROM amazoncorretto:17-alpine as build
as build
로 이 스테이지에 이름을 지정하여 다음 스테이지에서 참조할 수 있게 했다.WORKDIR /workspace/app
COPY
명령어들
RUN
명령어들
FROM amazoncorretto:17-alpine
VOLUME /tmp
COPY --from=build /workspace/app/build/libs/*.jar app.jar
--from=build
로 이전 스테이지의 파일 참조)EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "/app.jar"]
Docker 컨테이너의 실행 명령어를 설정하는 방법으로 CMD와 ENTRYPOINT가 있다.
CMD는 컨테이너 실행 시 다른 명령어로 오버라이드가 가능하다. 대신 ENTRYPOINT는 오버라이드가 불가능하다.
컨테이너 실행 시점에 명령어를 다른 명령어로 바꿀 수 있냐, 없냐의 차이다.
예시)
# CMD를 사용한 경우
CMD ["java", "-jar", "/app.jar"]
docker run myapp other-command # java -jar /app.jar 대신 other-command 실행
# ENTRYPOINT를 사용한 경우
ENTRYPOINT ["java", "-jar", "/app.jar"]
docker run myapp -Xmx512m # java -jar /app.jar -Xmx512m 실행
# 추가로 ENTRYPOINT와 CMD를 같이 사용할 수 있다.
# 이 경우 CMD는 ENTRYPOINT의 기본 파라미터가 된다.
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
docker run myapp # nginx -g daemon off; 실행
그래서 ENTRYPOINT는 컨테이너가 시작될 때 항상 실행돼야 하는 명령어를 설정할 때 쓰인다.
이 경우 jar 파일 실행은 항상 필요하므로 ENTRYPOINT를 사용한다.
최종 이미지는 실행에 필요한 파일만 포함하게 되어 이미지 크기가 크게 감소하고 보안성도 향상된다.
server {
listen 80;
server_name ${DOMAIN_NAME};
location / {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
listen 80
: 80포트로 들어오는 HTTP 요청을 받는다.
server_name ${DOMAIN_NAME}
: 이 도메인으로 들어오는 요청을 처리한다.
location /
: 모든 경로의 요청을 처리한다.
proxy_pass http://backend:8080
: 받은 요청을 backend 컨테이너의 8080 포트로 전달한다.
proxy_set_header Host $host
: 클라이언트가 요청한 원래 호스트 정보를 백엔드로 전달. 백엔드 애플리케이션이 어떤 도메인으로 요청이 들어왔는지 알 수 있음.
proxy_set_header X-Real-IP $remote_addr
: 실제 클라이언트의 IP 주소를 백엔드로 전달. 로깅이나 차단 목록 관리 등에 사용.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
: 요청이 거쳐온 모든 프록시 서버의 IP 주소 목록을 전달. 클라이언트의 실제 IP를 포함한 전체 요청 경로 추적 가능.
proxy_set_header X-Forwarded-Proto $scheme
: 클라이언트가 사용한 프로토콜(http/https)을 백엔드로 전달. 백엔드에서 보안 관련 처리시 필요.
이러한 헤더 설정들은 프록시 서버를 통해 요청이 전달될 때 원래 요청의 정보를 잃지 않고 백엔드 서버로 전달하기 위해 필요하다.
예를 들어, 이런 헤더가 없다면 백엔드 서버는 모든 요청이 Nginx(프록시 서버)에서 온 것으로만 인식하게 된다. 실제 클라이언트의 IP나 프로토콜 등의 정보를 알 수 없게 된다.
프록시 헤더 없는 경우:
클라이언트(1.1.1.1) -> Nginx(2.2.2.2) -> 백엔드
↳ "요청이 2.2.2.2에서 왔네?"
프록시 헤더 설정한 경우:
클라이언트(1.1.1.1) -> Nginx(2.2.2.2) -> 백엔드
↳ X-Real-IP: 1.1.1.1
↳ X-Forwarded-For: 1.1.1.1
↳ "아, 실제 요청은 1.1.1.1에서 왔구나!"
docker-compose.yml을 보면 nginx 서비스는 Dockerfile 없이 공식 이미지를 직접 사용했다:
nginx:
image: nginx:alpine # nginx 공식 이미지 사용
volumes:
- ./docker-files/nginx/nginx.conf:/etc/nginx/conf.d/default.conf # 설정 파일만 주입
이렇게 한 이유는:
커스터마이징이 거의 필요없음
공식 이미지의 신뢰성
간단한 설정 주입
반면 cr-backend는 커스텀 Spring Boot 애플리케이션이라 소스 코드를 빌드하고 실행하는 과정이 필요했기 때문에 Dockerfile이 필요했다.
이처럼 상황에 따라 Dockerfile을 직접 작성할지, 공식 이미지를 사용할지 선택할 수 있다. 커스터마이징이 많이 필요한 경우 Dockerfile을, 그렇지 않은 경우 잘 관리되는 공식 이미지를 사용하는 것이 효율적이다.
mariadb:
image: mariadb:latest # mariadb 공식 이미지 사용
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
volumes:
- mariadb-data:/var/lib/mysql # 데이터 영속성을 위한 볼륨
잘 된다!
이 글에서는 도커를 사용해 개발 서버를 구축하는 방법을 살펴보았다. 각 서비스별로 서로 다른 컨테이너화 전략을 적용했다:
이제 다음은 라즈베리파이에다가 도커 실행하기..!!
라즈베리파이야 얼른 와