Docker 및 Docker Compose 도입 - 7/1

들어가며

저희 서비스의 운영 서버는 2GB인 한 대로 운영하고 있습니다. 하지만 성능을 업그레이드를 하면서 스펙이 점점 올라가기 시작했습니다. NGINX, Spring Boot, PostgreSQL, 그리고 관측성을 위한 Grafana Alloy까지 올라갔습니다

여기에 검색 성능을 위한 Redis 고려와 CI/CD 파이프라인까지 붙이려는 시점이 되게 되었습니다.

서비스가 이 정도로 늘어나니 서버에 그때그때 손으로 설치하고 실행하는 방식이 한계에 부딪혔습니다. 이 글은 그 한계를 어떻게 느꼈고 왜 Docker와 Docker Compose를 선택했으며 2GB라는 제약 안에서 어떤 선택을 했는지 에 대한 기록입니다.

 

1. 왜 Docker인가

1.1 서비스가 늘어나면 수동 관리가 무너진다

서버에 현재 다음을 전부 설치한다고 생각해보았을 때

  • NGINX 설치 · 설정
  • JDK 설치 + Spring 실행 스크립트
  • PostgreSQL 설치 · 버전 관리
  • Grafana Alloy 바이너리
  • (예정) Redis

버전이 충돌하고 설정 파일이 서버 곳곳에 흩어지고 이 서버에 뭐가 깔려 있더라를 기억에 의존하게 됩니다.

서비스가 1~2개면 그냥 설치해도 되지만 6개쯤 되면 격리와 조합이 필요한 임계점이 오게 됩니다.

Docker는 각 서비스를 컨테이너로 격리하고 그 실행 환경을 이미지 하나에 담는다. 이 서버에 뭐가 깔려 있는가가 기억이 아니라 코드(docker-compose.yml)로 남게 됩니다.

 

1.2 CI/CD의 배포 단위가 필요했다

사실 Docker 도입의 가장 정직한 방아쇠는 CI/CD였습니다. CI/CD를 제대로 하려면 "빌드 산출물"을 서버로 옮겨 실행해야 하는데 그 산출물이 JAR 파일이면 여전히 "서버에 JDK 버전 맞고, 환경변수 맞고…"를 신경 써야 합니다. 반면 Docker 이미지는 실행 환경까지 통째로 담은 배포 단위입니다.

  • CI에서 docker build → 레지스트리에 push
  • 서버에서 docker pull → docker compose up -d

CI에선 됐는데 서버에선 안 됨이 구조적으로 사라집니다. 롤백도 이전 이미지 태그로 다시 up 하면 끝나게 됩니다.

 

1.3 로컬 = 운영 환경 일치

새로 Redis를 구축하기 전에 이 이점이 특히 와닿았습니다. Compose 파일에 몇 줄 추가하는 것만으로 로컬에도 운영과 같은 버전의 Redis가 뜨게 됩니다. "내 로컬에 Redis 깔고 버전 맞추고…" 하는 온보딩 비용이 사라집니다.

 

2. 왜 Docker Compose인가

  • Docker(컨테이너화)를 왜? → 환경을 코드로 고정하고 어디서든 똑같이 뜨게 만들 수 있습니다.
  • Compose를 왜? → 그 컨테이너가 여러 개(NGINX + Spring + PG + Redis + Alloy)니까 한 파일로 묶어 한 번에 관리할 수 있습니다.

Compose는 다중 서비스 스택을 하나의 선언적 파일로 정의하고 docker compose 명령 하나로 전체를 재현할 수 있습니다.

서비스 간 통신도 서비스명으로 해결되고(db:5432, redis:6379),
기동 순서(depends_on), 전용 네트워크, 볼륨, 리소스 상한까지 전부 코드로 남는다. 특히 저희 서비스는 리소스 상한이 되게 중요하기 때문에 이것이 가장 도움이 될 것이라고 생각했습니다.

 

3. 2GB와의 싸움

서비스를 컨테이너로 나누는 것과 별개로 총 메모리는 여전히 2GB입니다. 현실적인 예산을 짜보게 되었습니다.

컴포넌트 대략 RAM
OS + Docker 데몬 200~400MB
Spring Boot (JVM) 400~700MB
PostgreSQL 200~400MB
NGINX 20~50MB
Grafana Alloy 100~200MB
Redis (캐시) 50~150MB

총합은 대략 1.2~1.8GB 나오게 됩니다. 2GB에서 돌아가긴 하지만 여유가 거의 없습니다. 트래픽이 튀거나 JVM GC가 돌 때 OOM으로 컨테이너가 죽을 수 있게 됩니다.

그래서 다음 세 가지를 안전장치로 걸게 되었습니다.

 

1. 스왑(swap)

2GB에선 스왑이 사실상 필수다. 순간 스파이크에 프로세스가 즉사하는 걸 막는다.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# /etc/fstab 에 등록해 재부팅 후에도 유지

 

 

2. JVM 힙 명시적 제한 & 서비스별 메모리 상한

  • 한 프로세스가 폭주하는 걸 막을 수가 있습니다.
  • 컨테이너 안의 JVM은 호스트 전체 메모리를 보고 힙을 크게 잡으려다 터지게됩니다. 그래서 힙을 명시하였습니다
spring:
  environment:
    - JAVA_OPTS=-Xms256m -Xmx512m -XX:MaxMetaspaceSize=128m
  mem_limit: 700m
postgres:
  mem_limit: 400m
redis:
  mem_limit: 150m
  command: redis-server --maxmemory 120mb --maxmemory-policy allkeys-lru

 

마치며

돌아보면 모든 결정이 "2GB 단일 서버"라는 하나의 제약에서 나오게 되었습니다.

  • 서비스가 늘어 수동 관리가 한계 → Docker
  • 6개 서비스를 한 파일로, 리소스 상한까지 → Compose
  • 메모리가 빠듯해서 → JVM 힙 제한, mem_limit, swap

제약이 오히려 선택을 명확하게 만들어주게 되었습니다.

'프로젝트 > toIT' 카테고리의 다른 글

2GB 서버를 지키는 가장 싼 방법 Rate Limiter 도입기  (0) 2026.07.04
Rate Limiter  (0) 2026.07.04
통합검색 성능 개선기  (0) 2026.07.02
Spring의 요청 처리 순서  (0) 2026.07.02
OSIV  (0) 2026.07.02