목차
- GitHub Actions + Docker + GHCR
- 작업 flow
- 트래비스 CI + AWS codedeploy 시도 (구성 변경 전)
- 본격적인 작업 과정 (구성 변경 후)
- ✅ GHCR 이미지 생성 시 주의할 점
- ✅ port mapping
- ✅ pm2 start?
- ✅ 환경변수를 Docker container에 어떻게 집어넣지?
- ✅ EC2 용량이 부족하다면?
GitHub Actions + Docker + GHCR
작업 flow
- main 브랜치 merge(push)
- 테스트 진행
- GHCR에 이미지 build 및 push
- 이미지 pull 및 컨테이너 실행

GHCR은 GitHub Container Registry를 의미한다.
docker hub와 마찬가지로 docker 이미지를 저장하는 공간이다.
올라간 이미지는 Github 내 Packages에서 확인할 수 있다.
트래비스 CI + AWS codedeploy 시도 (구성 변경 전)
처음에는 CI툴을 GitHub Actions가 아니라 travis와 codedeploy를 이용해서 구현하려고 시도했다.
다른 블로거 분들이 했던 방식을 많이 참고했다.
travis.ci → 테스트 → Docker Hub push → S3에 코드 파일 전송 → codedeploy → EC2 배포
그러나 아래와 같은 이유로 구성을 바꿔서 다시 작업했다.
- GitHub Actions가 Github과 같이 사용하기 편리하고 접근이 용이하기 때문에 사용자 커뮤니티가 늘어나고 있음
- travis는 무료 플랜 1달로 사용에 제한이 있음
- Dokcer를 이용해서 컨테이너 배포의 장점을 누리고 싶은데 codedeploy로 빌드된 코드를 직접 배포하는 방식을 사용하면 docker 컨테이너를 사용하지 않는 것이 아닌가 생각
- codedeploy에서 겪는 에러가 많음 (해외 개발자들 사이에서도 업데이트가 느리기로 악명이 높은 모양)
- codedeploy-agent를 ec2에서 실행시켜야 하는데 ruby 버전 이슈로 정상적으로 설치되지 않는다. 다른 방식으로 우회해서 설치하는 방법이 있으나 실제로 codedeploy를 돌리는 단계에서 계속 ruby 버전 오류가 발생해서 배포가 불가능했다.

작성해두었던 .travis.yml 파일은 필요가 없어서 삭제했지만 기록을 위해 남겨둔다.
# .travis.yml
language: generic
sudo: required
branches:
only:
- feature/docker
services:
- docker
before_install: # 패키지를 다운로드 받기 전
- docker build -t eunsongpark/test-app -f ./Dockerfile.dev ./
script:
- docker run -e CI=true eunsongpark/test-app npm run test # 현재 test 하지않고 빠져나오게 코드 작성
after_success:
- docker build -t eunsongpark/project-gift-server ./
- echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_ID" --password-stdin
- docker push eunsongpark/project-gift-server
before_deploy:
- zip -r project-gift *
- mkdir -p deploy
- mv project-gift.zip deploy/project-gift.zip
deploy:
- provider: s3
access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
bucket: project-gift-s3
region: ap-northeast-2
skip_cleanup: true
local_dir: deploy
wait-until-deployed: true
on:
repo: projectGift/server
branch: feature/docker
- provider: codedeploy
access_key_id: $AWS_ACCESS_KEY
secret_access_key: $AWS_SECRET_KEY
bucket: project-gift-s3
key: project-gift.zip
bundle_type: zip
application: gift-deploy-application
deployment_group: gift-deploy-group
region: ap-northeast-2
wait-until-deployed: true
on:
repo: projectGift/server
branch: feature/docker
도커를 사용하고 싶었던 이유는 다음과 같다.
- 어떤 호스트 환경에서도 동일한 어플리케이션을 신속하게 구축 및 배포 가능
- 레지스트리에 이미지를 올려놓으면 간단히 해당 이미지를 pull 하는 것으로 유연한 서버 확장이 가능
- 관리자는 컨테이너가 어떻게 구성되어 있는지 신경 쓸 필요 없음
본격적인 작업 과정 (구성 변경 후)
아무튼 다시 부족하지만 내가 배포자동화를 구현한 방식으로 돌아와서 flow를 살펴보자.

remote 메인 브랜치에서 merge가 되면
Actions에서 자동으로 테스트와 빌드를 진행한다.
빌드된 이미지는 container repository에 올라가고
여기까지의 과정이 문제가 없으면 깃헙에서 runner로 등록해둔 ec2 인스턴스에서 해당 이미지를 run 동작시켜 어플리케이션이 실행된다.
처음에는 복잡하게 느껴지지만 몇 가지 간단한 작업만 처리해주면 된다.
- Docker 이미지 빌드를 위한 Dockerfile 생성
- GitHub Actions 자동 작업 수행을 위한 yml 파일 생성
- aws ec2 생성 후 actions runner 설정
참고로 Nest.js, EC2(ubuntu22.04), RDS를 사용했다.
전체적인 세부 과정은 아래 블로그에서 깔끔하게 설명이 되어있다.
배포자동화(CI/CD) - Github Actions/Nuxtjs/Docker/EC2
다음은 이 글의 유튜브 영상입니다. https://youtu.be/E3i9qt0SS-I 프로젝트를 진행할때 많은 시간을 들여야 하는 것 중에 하나가 바로 배포입니다. 형상관리(Git)에 커밋을 하고, 서버에 파일을 업로드
codegear.tistory.com
나는 다음의 부분들을 조금 다르게 진행했다.
- 자동 테스트 과정 추가
- RDS DB 연결을 위한 환경변수 설정
- ec2 내 어플리케이션 pm2 백그라운드 실행
작성한 Dockerfile, Dockerfile.dev, main.yml 이다.
Dockerfile.dev는 테스트를 위한 것이다.
# Dockerfile.dev
FROM node:18
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install
COPY ./ ./
CMD [ "npm", "run", "start" ]
# Dockerfile
FROM node:18
WORKDIR /usr/src/app
# 종속성 먼저 다운로드 받아서 소스코드 일부만 변경된 것으로 불필요하게 모듈을 다시 다운로드받지 않게 하기
COPY package.json ./
RUN npm install
# pm2 설치
RUN npm install -g pm2
COPY ./ ./
RUN npm run build
# 컨테이너의 3000번 포트 열기
EXPOSE 3000
# github actions에서 끌어온 환경변수를 docker container 내에서 저장
RUN --mount=type=secret,id=PORT \\
--mount=type=secret,id=DB_URL \\
--mount=type=secret,id=DB_USERNAME \\
--mount=type=secret,id=DB_PASSWORD \\
--mount=type=secret,id=DB_HOST \\
--mount=type=secret,id=DB_NAME \\
--mount=type=secret,id=DB_PORT \\
--mount=type=secret,id=JWT_SECRET \\
--mount=type=secret,id=JWT_EXPIRESIN \\
export PORT=$(cat /run/secrets/PORT) && \\
export DB_URL=$(cat /run/secrets/DB_URL) && \\
export DB_USERNAME=$(cat /run/secrets/DB_USERNAME) && \\
export DB_PASSWORD=$(cat /run/secrets/DB_PASSWORD) && \\
export DB_HOST=$(cat /run/secrets/DB_HOST) && \\
export DB_NAME=$(cat /run/secrets/DB_NAME) && \\
export DB_PORT=$(cat /run/secrets/DB_PORT) && \\
export JWT_SECRET=$(cat /run/secrets/JWT_SECRET) && \\
export JWT_EXPIRESIN=$(cat /run/secrets/JWT_EXPIRESIN) && \\
printenv > .env
# pm2를 docker 컨테이너에서 돌리기 위해서는 pm2-runtime으로 실행
CMD [ "pm2-runtime", "start", "dist/main.js" ]
# main.yml
name: CI/CD
# 트리거를 수행할 브랜치 지정
on:
push:
branches: [main]
# 환경설정
env:
DOCKER_IMAGE: ghcr.io/${{ github.actor }}/project-gift-server
VERSION: ${{ github.sha }}
NAME: go_cicd # 컨테이너 이름
jobs:
# 테스트 Job
test:
name: Test
runs-on: ubuntu-latest
steps:
# github repository에서 checkout (코드 가져오기)
- uses: actions/checkout@v2
- name: Install modules
run: docker build -t nest-test-app -f ./Dockerfile.dev ./
- name: Run tests
run: docker run nest-test-app npm run test
# 빌드 Job
build:
needs: test # 테스트 후에 실행되도록 정의
name: Build
runs-on: ubuntu-latest
steps:
# github repository에서 checkout (코드 가져오기)
- uses: actions/checkout@v2
# docker build 수행
- name: Set up docker buildx # 여러 다른 실행환경에서 호환 사용할 수 있도록 빌드
id: buildx
uses: docker/setup-buildx-action@v1
- name: Cache docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ env.VERSION }}
restore-keys: |
${{ runner.os }}-buildx-
# GitHub 컨테이너 레지스트리에 로그인 후 빌드 & 푸시
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: ${{ env.DOCKER_IMAGE }}:latest
secrets: |
"PORT=${{ secrets.PORT }}"
"DB_URL=${{ secrets.DB_URL }}"
"DB_USERNAME=${{ secrets.DB_USERNAME }}"
"DB_PASSWORD=${{ secrets.DB_PASSWORD }}"
"DB_HOST=${{ secrets.DB_HOST }}"
"DB_NAME=${{ secrets.DB_NAME }}"
"DB_PORT=${{ secrets.DB_PORT }}"
"JWT_SECRET=${{ secrets.JWT_SECRET }}"
"JWT_EXPIRESIN=${{ secrets.JWT_EXPIRESIN }}"
# 배포 Job
deploy:
needs: build # build 후에 실행되도록 정의
name: Deploy
runs-on: [self-hosted, label-go] # AWS ./configure에서 사용할 label명
steps:
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
# host 80번 포트로 유입되는 트래픽을 도커 컨테이너의 3000번 포트로 전달
- name: Docker run
run: |
docker stop ${{ env.NAME }} && docker rm ${{ env.NAME }} && docker rmi -f ${{ env.DOCKER_IMAGE }}:latest
docker run -d -p 80:3000 --name ${{ env.NAME }} --restart always ${{ env.DOCKER_IMAGE }}:latest
이후로는 배포를 진행하면서 만난 오류나 팁을 소개하려고 한다.
✅ GHCR 이미지 생성 시 주의할 점
actions yml 파일에서 DOCKER_IMAGE를 설정한다.
DOCKER_IMAGE: ghcr.io/${{ github.actor }}/project-gift-server
ghcr에 올라가는 이미지 이름인데 ${{ github.actor }} 는 자신의 깃헙 username이 들어간다.
대문자는 허용되지 않으므로 username에 대문자가 들어갔다면 소문자로만 수정해야한다.
✅ port mapping
호스트와 그 위에 띄워진 컨테이너 간 연결해주는 단계에서 포트 맵핑이 필요하다.
포트를 맵핑해주는 명령어는 헷갈리지 않도록 한다.
$ docker run -p <host port number>:<container port number>/<protocol> [IMAGE NAME] [OTHER OPTIONS...]
예를 들어 -p 80:3000/tcp 옵션을 적용하면, 호스트 시스템의 80번 TCP 포트로 유입되는 트래픽은 모두 도커 컨테이너의 3000번 TCP 포트로 전달된다.
도커(Docker) : 포트 포워딩 설정(포트 맵핑)하기
도커(Docker) : 포트 포워딩 설정(포트 맵핑)하기 본 글에서는 도커 컨테이너 내부에서 동작하는 서버로 컨테이너 외부에서 접속할 수 있도록 포트 포워딩(Port forwarding) 설정 또는 포트 맵핑(Port mapp
tttsss77.tistory.com
✅ pm2 start?
컨테이너 실행 시 pm2 start <파일명>으로 돌렸을 때
컨테이너 status가 up이 되지 않고 자꾸 restarting이 된다.
다음 명령어로 로그를 살펴보자.
docker logs --tail 50 --follow --timestamps <이미지>
pm2가 제대로 실행되지 않는다.
알고보니 docker 컨테이너에서 pm2 실행 시 pm2-runtime 명령어를 써줘야 한다.
docker container에서 pm2가 시작하자마자 종료될 때
도커 컨테이너에서 pm2를 실행하는 데 바로 꺼질 때 node 프로젝트를 프로덕션 환경에 pm2로 올려보도록 한다. node docker image를 이용해서 docker container에서 로 시작하도록 올려본다. 하지만 갑자기
dotorimook.github.io
✅ 환경변수를 Docker container에 어떻게 집어넣지?
지금까지 DB 연결설정에 필요한 환경변수는 .env에 저장하고 노출되지 않도록 .gitignore에 기재했었다.
하지만 이렇게 하면 빌드된 컨테이너가 배포환경에서 돌아갈 때 .env 파일이 컨테이너 내에 없기 때문에 DB 연결이 불가능하다.
그렇다고 중요한 정보를 하드코딩하거나 github에 올릴 수는 없다.
어떻게 해야할까?
내가 사용한 방법은 깃헙 actions secrets에 원하는 변수를 넣어두고

워크플로우 yml 파일 docker/build-push-action 작업의 secrets에 할당한 다음

Dockerfile에서 사용함으로써 이미지가 생성될 때 해당 변수들이 .env파일에 저장되도록 했다.

도커, 리눅스, 깃헙 actions가 어떻게 돌아가는지 이해해야 했기 때문에 상당 시간이 소요됐다.
해당 문제에 대해 다음 내용을 참고했다.
- GitHub Actions : docker/build-push-action 이슈

How to add github secrets to env variable · Issue #390 · docker/build-push-action
Below is the yml file, I hope to add github secrets to env variable. name: Publish Docker image on: push: branches: [ main ] jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: u...
github.com
- stackoverflow : How do I set environment variables during the build in docker

How do I set environment variables during the build in docker
I'm trying to set environment variables in docker container during the build but without success. Setting them when using run command works but I need to set them during the build. Dockerfile FROM ...
stackoverflow.com
✅ EC2 용량이 부족하다면? (+23/2/7 추가)

deploy 과정 중에 위와 같은 에러가 발생했다.
docker: failed to register layer: Error processing tar file(exit status 1): write /usr/lib/x86_64-linux-gnu/libc.a: no space left on device.
ec2의 용량이 부족하다는 의미다.
직접 df -hT 명령어로 확인해보면

가용 용량이 얼마 남지 않았다.

23년 2월 기준 aws ec2 프리티어는 12개월 간 30GB까지 무료다.
처음 ec2 인스턴스를 생성할 때 default로 8GB가 되어있으므로 볼륨을 늘려줘야 한다.

위 사진은 볼륨을 늘린 후에 캡쳐한 것이다.
원래는 8GiB 였다.
우측 상단에 “작업”을 클릭하고 볼륨을 수정한다.

볼륨 상태에서 optimizing이 끝나면 lsblk 명령어로 확인해본다.

xvda1 파티션의 크기도 늘리자.
sudo growpart <볼륨> <파티션번호>
# ex) sudo growpart /dev/xvda 1
/dev/root의 타입은 ext4이다.

ext4 파일 시스템의 크기도 늘려주자.
sudo resize2fs <파티션>

/dev/root 의 용량이 25G로 늘어났다.
참고
참고 자료
- 인프런 John Ahn 님의 도커 강의 학습 및 실습
따라하며 배우는 도커와 CI환경 - 인프런 | 강의
이 강의를 통해 도커에 대해서 배울 수 있으며, CI 환경을 구성할 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
- 블로그 참조
- https://jojoldu.tistory.com/265
- https://codegear.tistory.com/84
- https://velog.io/@jeff0720/Travis-CI-AWS-CodeDeploy-Docker-%EB%A1%9C-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94-%EB%B0%8F-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0
- https://velog.io/@suyeonpi/Dimelo-Project-Travis-CI-Code-Deploy-Docker%EB%A1%9C-%EB%AC%B4%EC%A4%91%EB%8B%A8%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0
공식문서
