Dev 서버와 Docker Compose 구성 - 100-hours-a-week/KTB4-2nd-Cloud GitHub Wiki
App EC2에는 Nginx와 MySQL을 Host에 직접 설치하고, Frontend와 Backend는 컨테이너로 띄웁니다. Worker EC2에는 Docker만 설치하고 AI 컨테이너 하나를 띄웁니다.
서버 설정은 콘솔이나 SSH에서 손으로 하지 않고, Cloud 저장소의 스크립트를 SSM으로 실행해서 적용했습니다.
| 스크립트 | 하는 일 |
|---|---|
prepare-mysql-volume.sh |
MySQL EBS 확인, 파일시스템 생성, UUID 기준 마운트 |
bootstrap-app-host.sh |
Swap, Docker, Nginx, MySQL, CloudWatch Agent 설치 |
bootstrap-worker-host.sh |
Swap, Docker 설치 |
디스크 포맷과 패키지 설치를 한 스크립트에 넣지 않았습니다. 그래서 패키지 설정만 다시 적용할 때는 포맷 경로를 거치지 않습니다.
스크립트는 로컬에서 문법 검사를 한 뒤 base64로 SSM에 넘기고, 서버에서 SHA256이 로컬 값과 같은지 확인한 다음 실행했습니다.
AWS에서는 /dev/sdf로 붙였지만 서버 안에서는 /dev/nvme1n1로 보였습니다. 이 이름은 재연결 순서에 따라 바뀔 수 있어서 /etc/fstab에는 UUID를 썼습니다.
UUID=915d7f75-... /var/lib/mysql ext4 defaults,nofail,x-systemd.device-timeout=60s 0 2
포맷 전에 스크립트가 다음을 모두 확인합니다.
- 입력한 EBS Volume ID와 디스크 시리얼이 같은지
- Root 디스크가 아닌지
- 기존 파티션이나 파일시스템이 없는지
- 다른 곳에 마운트되어 있지 않은지
모두 통과해도 --format-empty-volume 옵션을 줘야만 포맷합니다. 포맷 뒤 옵션 없이 다시 실행해서 기존 파일시스템을 알아보고 그냥 넘어가는 것도 확인했습니다.
nofail은 EBS 문제로 서버 부팅과 SSM 접속까지 막히지 않게 하려고 넣었습니다. 대신 MySQL에는 마운트 의존성을 걸었습니다.
[Unit]
RequiresMountsFor=/var/lib/mysqlEBS 마운트에 실패하면 MySQL이 Root 디스크의 빈 /var/lib/mysql로 떠버리는 걸 막기 위해서입니다.
App은 메모리 약 3.7GiB 안에서 Nginx, MySQL, FE, BE가 함께 돕니다. 순간적으로 메모리가 부족할 때 바로 프로세스가 죽지 않도록 2GiB Swap을 만들고 vm.swappiness=10으로 낮췄습니다.
Swap은 여유분으로만 쓰고, 계속 사용된다면 메모리가 부족하다는 신호로 봅니다. 150장 부하 테스트 중 App 메모리는 최대 47% 정도였습니다.
| 구성 요소 | 버전 |
|---|---|
| Docker Engine | 29.8.1 (공식 APT 저장소, 버전 고정) |
| Docker Compose Plugin | 5.5.1 |
| Nginx | 1.24.0 |
| MySQL Community Server | 9.7.2 |
| CloudWatch Agent | 1.300072.0b1766 |
앱을 올리기 전에는 Ubuntu 기본 페이지 대신 503을 반환하게 했습니다.
server {
listen 80 default_server;
return 503 "Yeodam service is not deployed yet.\n";
}처음 설치했을 때는 설정을 바꿨는데도 외부에서 기본 페이지가 200으로 계속 보였습니다. 패키지 설치 과정에서 Nginx가 먼저 떠 있었고, systemctl enable --now는 이미 실행 중인 서비스를 reload하지 않았기 때문입니다.
스크립트에서 설정 검사 뒤 실행 상태에 따라 reload 또는 start를 고르도록 고쳤고, 외부 Elastic IP로 503이 나오는 것을 확인했습니다.
설정을 마친 뒤 EC2를 재부팅해서 다음이 자동으로 돌아오는지 확인했습니다.
- MySQL EBS가 같은 UUID로 마운트됨
- Swap과
swappiness유지 - Docker, Nginx, MySQL 실행
- 외부 요청에 503 응답
flowchart LR
subgraph HOST["App EC2 Host"]
NG["Nginx"]
DB[("MySQL :3306")]
subgraph NET["yeodam-app-network 172.28.0.0/24"]
FE["Frontend"]
BE["Backend"]
end
end
NG -- "127.0.0.1:3000" --> FE
NG -- "127.0.0.1:8080" --> BE
BE -- "host.docker.internal<br/>= 172.28.0.1" --> DB
Frontend와 Backend는 127.0.0.1에만 포트를 열어 Nginx를 통해서만 들어오게 했습니다.
Backend가 Host의 MySQL에 붙는 부분에서 신경 쓸 게 있었습니다. MySQL의 앱 계정은 컨테이너 네트워크에서만 접속하도록 [email protected].%로 만들었는데, host.docker.internal을 host-gateway로 두면 기본 브리지의 172.17.0.1로 연결될 수 있었습니다.
그래서 네트워크 대역과 게이트웨이를 고정하고, host.docker.internal을 그 게이트웨이로 직접 지정했습니다.
networks:
app-network:
ipam:
config:
- subnet: 172.28.0.0/24
gateway: 172.28.0.1
services:
backend:
extra_hosts:
- "host.docker.internal:172.28.0.1"| 항목 | Frontend | Backend |
|---|---|---|
| health check | /api/health |
/api/actuator/health |
| 재시작 | unless-stopped |
unless-stopped |
| 종료 대기 | 기본값 | 90초 |
| 로그 | json-file (크기 제한) | CloudWatch Logs (awslogs) |
Backend 종료 대기를 90초로 둔 건 150MB 업로드 요청 하나가 끝날 시간을 주기 위해서입니다.
Backend 경로에 /api가 붙은 뒤에는 health check 경로도 같이 바꿔야 했습니다. 앱은 정상인데 예전 경로를 검사하면 컨테이너가 unhealthy로 표시됩니다.
운영 값은 배포 때 /run/yeodam/app.env로 만들어지지만, 컨테이너에는 Compose의 environment:에 적힌 이름만 들어갑니다. 파일에 값이 있다고 컨테이너에 들어가지는 않습니다.
장소 검색이 500을 반환한 일로 이걸 확인했습니다. 과정은 환경 파일에 있는 값이 컨테이너에 없음에 있습니다.
Worker Compose는 AI 컨테이너 하나와 Qdrant 데이터용 Volume 하나만 관리합니다.
| 항목 | 값 |
|---|---|
| 재시작 | unless-stopped |
| 종료 대기 | 7분 |
| health check | 127.0.0.1:8000/health |
| Volume | qdrant-data:/data |
| 외부 포트 | 8000 (App SG에서만 접근) |
재시작 정책은 설계와 다르게 바꿨습니다. 이유는 AI Worker 온디맨드 운영에 있습니다.
Worker에서 이전 AI 이미지가 쌓여 새 이미지 pull이 실패한 적이 있어서, 배포 스크립트가 성공한 뒤에 사용하지 않는 이미지를 지우도록 했습니다. 과정은 배포 파이프라인에 있습니다.