- name: Copy compose files to EC2
uses: appleboy/[email protected] # Secure Copy - SSH 연결을 이용해 해당 repo의 파일을 복사한다.
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_KEY }}
source: "docker-compose.yml,low-mem.cnf" # low-mem.cnf는 mysql 용량을 줄이기 위한 설정 파일
target: "/home/${{ secrets.EC2_USERNAME }}/app" # EC2안의 디렉터리 경로. scp가 파일을 어디에 놓을지 지정하는 값
- name: Application Run
uses: appleboy/[email protected] # Secure Shell - 원격 서버에 접속해서 script 명령어를 실행한다.
env:
ENV_FILE: ${{ secrets.ENV_FILE }}
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_KEY }}
envs: ENV_FILE
script: |
# 중간에 실패하면 중단한다.
set -e
# scp의 target과 동일한 경로여야 compose가 파일을 찾는다.
cd /home/${{ secrets.EC2_USERNAME }}/app
# EC2 내부에서 도커 허브 로그인 (private 레포를 pull 하기위해)
sudo docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
# .env 파일은 소유자만 읽고 쓸 수 있게 한다. (600)
printf '%s\n' "$ENV_FILE" > .env
chmod 600 .env # chmod는 파일의 접근 권한을 바꾸는 명령어
sudo docker compose pull app # app 컨테이너 안에 새 이미지 ID를 받아온다.
sudo docker compose up -d app # compose 파일에 적힌 상태와 실제 실행 중인 컨테이너를 비교해서, 차이가 나는 것만 새로 만드는 명령어. -d(detached)는 백그라운드 실행
sudo docker image prune -f # 교체된 구 이미지를 정리한다. 명령이 없으면 EC2 디스크가 부족해진다.
만약, 애플리케이션과 MySQL 두 개의 컨테이너를 EC2 안에 올린다고 했을 때, CI에 두 개의 컨테이너를 모두 pull 할 필요는 없다. → 즉, 애플리케이션 컨테이너만 CI 단계에서 지속적으로 docker compose pull app을 하면 된다.
물론, docker volume을 통해서 지워지지 않게 할 수는 있다.) 하지만, 매 배포마다 DB가 몇 초씩 끊기고 재시작도 하기 때문에.docker compose up -d: 핵심은 “차이가 나는 컨테이너만 재생성한다는 것이다”
docker-compose.yml의 app에 depends_on:mysql이 걸려있기 때문이다. compose는 의존 서비스를 함께 기동시킨다. compose는 필요한 이미지가 없을 때 자동으로 받아오기 때문에.scp단계의 - source와 target
source의 파일(레포에 존재)을 target(EC2의 지정 디렉토리)으로 복사한다.
/home/ubuntu는 Ubuntu AMI의 기본 사용자 ubuntu의 홈 디렉터리이다. SSH로 EC2에 접속했을 때 처음 떨어지는 위치이고, pwd(Print Working Directory- 현재 사용중인 디렉토리의 절대경로)를 치면 나오는 경로이다.
/app을 붙인 이유는 홈 디렉터리가 어질러지는 걸 막기 위해서이다.
name: juby # 컨테이너에 태그(juby)를 붙인다. "이 컨테이너, 볼륨, 네트워크는 한 묶음이라는 소속 표시이다.
services:
app:
image: ${DOCKERHUB_USERNAME}/${PROJECT_NAME}:latest # 이미지 이름
container_name: app
restart: unless-stopped # stopped는 사용자가 직접 멈춘 경우를 말한다. 즉, "내가 명시적으로 멈추지 않았다면 재시작"
ports:
- "8080:8080" # 호스트의 nginx는 compose 네트워크 안을 못 보기 때문에, 루프백에 바인딩한다.
env_file:
- .env # dev-deploy.yml에서 만든 env 파일
environment:
DB_URL: ${DB_URL}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
SPRING_PROFILES_ACTIVE: dev
TZ: Asia/Seoul
# 힙 제한
JAVA_TOOL_OPTIONS: "-Xms256m -Xmx384m -Xss512k -XX:MaxMetaspaceSize=160m -XX:+UseSerialGC"
depends_on:
mysql:
condition: service_healthy # mysql이 완전히 뜬 뒤에 app이 시작한다. 이게 없으면 재부팅 직후 app이 커넥션 실패(mysql과)로 죽는다.
redis:
condition: service_started
networks:
- juby-net
mysql:
image: mysql:8.0
container_name: mysql
restart: unless-stopped
ports:
- "127.0.0.1:3306:3306" # EC2 내부에서만 접근 가능하고, 외부에서는 SSH를 통해서만 닿는다.
environment:
# mysql 공식 이미지가 인식하는 초기화용 환경변수이다.
# 컨테이너가 처음 기동할 때 (볼륨이 비어있을때) 이 이름으로 DB를 하나 만들어 준다.
# MYSQL_USER에게는 MYSQL_DATABASE에 대한 전체 권한이 자동으로 부여된다.
# 최초 1회만 동작한다. 볼륨에 데이터가 이미 있으면 무시된다.
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} # root 비밀번호 (임의설정)
MYSQL_DATABASE: ${MYSQL_DATABASE} # 생성할 DB 이름 (juby)
MYSQL_USER: ${DB_USERNAME} # 생성할 일반 계정 (kkangmen)
MYSQL_PASSWORD: ${DB_PASSWORD} # 그 계정의 pw
TZ: Asia/Seoul
volumes:
- mysql-data:/var/lib/mysql
- ./low-mem.cnf:/etc/mysql/conf.d/low-mem.cnf:ro
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p\"$$MYSQL_ROOT_PASSWORD\" --silent"]
interval: 10s
timeout: 5s
retries: 10
start_period: 40s
networks:
- juby-net
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis-data:/data
volumes:
mysql-data:
redis-data:
networks:
juby-net:
name: juby-net
보통 한 EC2 안에서는 compose 파일 하나에 앱 컨테이너, mysql 컨테이너, redis 컨테이너를 다 넣는다.
docker-compose.yml + docker-compose.dev.yml / docker-compose.prod.yml 이렇게 두 개를 겹쳐서 실행한다.CloudWatch는 보통 컨테이너로 띄우지 않고, compose에서 각 서비스에 logging: driver: awslogs 지정. 컨테이너 stdout이 바로 CloudWatch Logs로 간다.
도커 볼륨이란?
docker compose up을 하면 프로젝트 이름을 딴 기본 네트워크 (예: 디렉터리가 app이면 app_default)가 자동으로 생성되고, 모든 서비스가 거기에 붙는다. 같은 네트워크 안이니 서비스 이름이 그대로 DNS로 풀린다.
docker network ls로 확인해보면 도커 네트워크 인프라를 확인할 수 있다.
GitHub Secrets를 compose가 인식하지 못하는 이유
GitHub Secrets가 존재하는 곳과 compose가 실행되는 곳이 다른 기계이다. ${{ secrets.DOCKERHUB_USERNAME }} 문법은 GitHub Actions가 워크플로 파일을 해석할 때 치환하는 것이고, docker-compose.yml은 GitHub가 읽지 않는다. 그래서 compose의 ${DOCKERHUB_USERNAME}은 EC2의 같은 디렉토리의 .env에서만 값을 찾는다. 그래서 ENV_FILE 시크릿 본문에 compose가 쓰는 키가 전부 들어있어야 한다.
전체 흐름
로컬 .env (gitignore, CI는 못 봄)
↓ 사람이 내용을 복사
GitHub Secrets의 ENV_FILE
↓ ssh-action의 envs: ENV_FILE → SSH 세션 환경변수로
EC2에서 echo "$ENV_FILE" > .env → 파일로 복원
↓
docker compose가 이 .env를 읽음 ← 여기가 주체
├─ ${...} 치환 (compose 파일 자체를 가공)
└─ env_file로 컨테이너에 주입
sudo docker compose ps # 둘 다 Up, mysql은 (healthy)
sudo docker compose logs app # 기동 로그
