목차
이전 글에서는 RDS를 생성하고 SSH 터널링을 이용해 데이터베이스에 안전하게 접근하는 환경을 구축했다.
이제 애플리케이션 서버와 데이터베이스까지 모두 준비되었으므로 서비스를 실제로 운영할 수 있는 기본적인 인프라가 완성되었다.
하지만 서버와 데이터베이스를 구축했다고 해서 배포까지 끝난 것은 아니다. 서비스를 운영하다 보면 새로운 기능을 추가하거나 버그를 수정해야 하고, 수정된 코드를 서버에 반영하는 작업이 반복적으로 발생한다.
프로젝트 초기에는 서버에 직접 접속하여 코드를 업데이트하고 애플리케이션을 재시작하는 방식으로도 충분히 운영할 수 있다. 하지만 개발이 계속 진행될수록 배포 횟수가 증가하고, 매번 같은 작업을 반복하는 과정에서 실수가 발생할 가능성도 높아진다. 특히 사이드 프로젝트나 개인 프로젝트라고 하더라도 GitHub를 통해 소스 코드를 관리하고 있다면 코드 변경 사항을 자동으로 배포할 수 있는 환경을 구축해두는 것이 훨씬 효율적이다.
이번 글에서는 Docker와 GitHub Actions를 활용하여 간단한 자동 배포 환경을 구축해본다. 개발자가 GitHub 저장소에 코드를 Push하면 GitHub Actions가 자동으로 실행되고, EC2 서버에서 최신 코드를 반영한 뒤 Docker 컨테이너를 재시작하도록 구성할 예정이다.
1. 배포 구조 설계
이번 글에서 구축할 배포 환경은 Docker와 Github Actions를 기반으로 동작한다.
개발자가 Github 저장소에 코드를 Push 하면 Github Actions가 실행되고, EC2 서버에 접속하여 최신 코드를 반영한 뒤 Docker 이미지를 다시 빌드하고 컨테이너를 재시작한다.
즉, 배포 흐름은 다음과 같다.
GitHub Push
↓
GitHub Actions 실행
↓
EC2 접속
↓
최신 코드 반영
↓
Docker Image Build
↓
Container 재시작
↓
배포 완료
이러한 배포 자동화 과정은 CI/CD 파이프라인의 일부이다.
이번 글에서는 복잡한 테스트 자동화보다는 Github Actions를 이용한 CD 환경 구축을 해보는 것이 목표이다.
2. Docker를 이용한 배포 환경 구성
배포 자동화를 구성하기 전에 먼저 애플리케이션을 Docker 환경에서 실행할 수 있도록 설정해야 한다.
Docker는 애플리케이션과 실행에 필요한 라이브러리, 설정 파일 등을 하나의 이미지로 패키징하여 어느 환경에서나 동일하게 실행할 수 있도록 도와주는 컨테이너 플랫폼이다. Docker를 사용하면 애플리케이션 실행 환경까지 함께 이미지로 관리할 수 있기 때문에 개발 환경과 운영 환경의 차이를 최소화할 수 있다.
만약 Docker에서 더 궁금한 부분이 있으면, 아래 이전에 작성한 글을 참고하면 좋을 것 같다.
[Docker] 01. 도커(Docker)란 ?
도커 (Docker)의 등장 배경서버의 운영 구조도커의 구조를 살펴보기 전, 서버를 운영하는 4가지 구조에 대해서 살펴보자.Bare Metal : 하드웨어 위에 바로 운영체제를 설치하고, 그 위에로 바로 애플
baby-developer77.tistory.com
2.1 Dockerfile 생성
Docker 이미지는 Dockerfile을 기반으로 생성되며, 애플리케이션 실행에 필요한 운영 환경, 의존성 패키지, 빌드 과정, 실행 명령 등을 정의할 수 있다.
프로젝트 루트 디렉터리에 Dockerfile을 생성한 뒤 사용하는 기술 스택에 맞게 내용을 작성한다.
Dockerfile은 프로젝트마다 조금씩 다르지만 일반적으로 다음과 같은 과정을 포함한다.
- 베이스 이미지 선택
- 작업 디렉터리 설정
- 의존성 설치
- 애플리케이션 빌드
- 실행 명령 정의
(Node.js 예시)
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
RUN npx prisma generate
EXPOSE 3000
CMD ["npm", "run", "start"]
Node.js 애플리케이션은 `package.json` 에 정의된 의존성을 설치한 뒤 애플리케이션을 빌드하고 실행한다.
또한, ``prisma``를 사용하는 만큼 빌드 전, `npx prisma generate`를 해줘야 한다.
⚠️ Prisma를 사용하는 경우 `prisma generate` 과정에서 DATABASE_URL 환경 변수가 필요할 수 있다.
따라서 Docker Build 시 ARG와 ENV를 이용해 DATABASE_URL을 전달하도록 설정하였다.
(Spring Boot 예시)
# ===== 빌드 환경 =====
FROM gradle:8.7-jdk17 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY gradle gradle
RUN gradle dependencies --no-daemon || true
COPY src src
RUN gradle clean build -x test --no-daemon
# ===== 실행 환경 =====
FROM eclipse-temurin:17-jdk
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
각 명령어의 역할을 간단히 설명하자면 다음과 같다.
- FROM : Docker 이미지 생성 시 사용할 기본 이미지를 지정한다.
- WORKDIR : 컨테이너 내부에서 작업할 디렉터리를 설정한다.
- COPY : 애플리케이션 실행에 필요한 파일을 컨테이너 내부로 복사한다.
- RUN : 이미지 빌드 과정에서 필요한 명령어를 실행한다. ( 의존성 설치, 애플리케이션 빌드 등 )
- EXPOSE : 컨테이너가 사용할 포트를 명시한다.
- CMD : 컨테이너 시작 시 기본으로 실행할 명령어를 지정한다.
- ENTRYPOINT : 컨테이너가 시작될 때 반드시 실행할 명령어를 지정한다.
Node.js 예제에서는 `npm install`, `npm run build`와 같은 작업을 수행하기 위해 `RUN` 명령어를 사용한다.
반면 Spring Boot 예제는 이미 빌드된 JAR 파일을 실행하는 방식이므로 별도의 빌드 과정 없이 `ENTRYPOINT`를 통해 애플리케이션을 실행한다.
2.2 .dockerignore 파일 생성
Docker 이미지를 빌드할 때는 현재 디렉터리의 파일들이 Docker Engine으로 전달된다.
하지만 실제 애플리케이션 실행에 필요하지 않은 파일까지 함께 포함되면 이미지 크기가 불필요하게 커지고 빌드 시간도 증가할 수 있다. 따라서 Docker 이미지에 포함하지 않은 파일은 ``.dockerignore``파일을 통해 제외하는 것이 좋다.
프로젝트 루트 디렉터리에 ``.dockerignore`` 파일을 생성한 뒤 다음과 같이 작성한다.
( Node.js 예시 )
node_modules
dist
.git
.env
( Spring Boot 예시 )
# Gradle
.gradle
build/
# IntelliJ / IDE
.idea/
*.iml
# OS
.DS_Store
# Git
.git
.gitignore
# Environment
.env
# Logs
logs/
*.log
# Test
test/
프로젝트 환경에 따라 추가로 제외해야 할 파일이 있을 수 있지만, 일반적으로 위 항목들은 대부분의 프로젝트에서 제외하는 편이다.
2.3 docker-compose.yml 파일 생성
Dockerfile 이 하나의 애플리케이션 이미지를 정의하는 역할이라면, `docker-compose.yml`은 실제로 컨테이너를 어떻게 실행할지 정의하는 역할을 한다. 현재 EC2 서버에서 컨테이너를 실행하는 용도로 Docker를 사용할 것이고, Github Actions를 통해 배포할 때도 동일한 설정을 이용해 컨테이너를 재생성하게 된다.
( Node.js 예시 )
services:
app:
build:
context: .
args:
DATABASE_URL: ${DATABASE_URL}
env_file:
- .env
ports:
- "3000:3000"
⚠️ Dockerfile에서 ARG DATABASE_URL을 사용하고 있으므로, Compose에서도 빌드 시점에 DATABASE_URL을 전달하도록 설정한다.
( Spring Boot 예시 )
version: "3.8"
services:
app:
build: .
container_name: spring-app
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
DB_USER: ${DB_USERNAME}
DB_PW: ${DB_PASSWORD}
JWT_SECRET_KEY: ${JWT_SECRET}
JWT_ACCESS_EXPIRATION: ${JWT_ACCESS_EXPIRATION}
restart: always
env_file:
- .env
실제 운영 환경에서는 데이터베이스, Redis, Nginx 등을 함께 Compose로 관리하기도 한다.
하지만 이번 글에서는 이미 RDS를 별도로 구축해두었으므로 애플리케이션 컨테이너만 실행하는 단순한 형태로 구성한다.
💡 참고로, Dockerfile, .dockerignore, docker-compose.yml 파일은 각각 어떤 프레임워크를 사용했는지, 어떤 구조, 어떤 언어를 사용했는지에 따라 다르기 때문에 각자의 환경에 맞추어 설정하는 것을 권장한다. (단순한 복붙 금지 !!! 100% 오류 납니다 ! )
2.4 컨테이너 실행 테스트
Dockerfile과 Docker Compose 설정이 완료되었다면 실제로 컨테이너가 정상적으로 실행되는지 확인해보자.
먼저 아래 명령어를 실행해 Docker와 Docker Compose가 설치되어 있는지 확인하자.
docker --version && docker compose version
버전 정보가 출력된다면, 정상적으로 설치되어 있는 것이다.
만약 설치되어 있지 않다면, 설치하고 오자 ........... (총총)
다만, 버전 정보가 출력된다고 하더라도 Docker Engine이 실행중이라는 뜻은 아니다.
아래 명령어를 실행하여 Docker Engine이 정상적으로 실행되고 있는지 확인한다.
docker info
정상적으로 실행 중이라면 Docker Client와 Server 정보가 출력된다.
Docker Desktop을 사용하는 경우, 실행되어 있지 않다면 Docker Desktop을 먼저 실행한 뒤 다시 시도하면 된다.
Docker가 정상적으로 동작하는 것을 확인했다면 이제 애플리케이션 이미지를 빌드해보자.
docker compose build
빌드가 완료되면 다음 명령어로 컨테이너를 실행한다.
docker compose up -d
실행 중인 컨테이너는 아래 명령어로 확인할 수 있다.
docker ps
애플리케이션 로그를 확인하고 싶다면 다음 명령어를 사용한다.
docker compose logs -f
더 많은 도커 명령어가 알고싶다면, 이전에 작성해둔 블로그를 참고하면 좋을 것 같다.
[Docker] 02. 도커 (Docker) 명령어 및 실습 해보기
도커 (Docker)의 실행 흐름docker-cli (도커 클라이언트) : 사용자가 터미널에서 도커 명령어를 입력한다.dockerd (도커 데몬) : 도커 API 요청을 처리하는 백그라운드 프로세스containerd : 컨테이너 실행 및
baby-developer77.tistory.com
정상적으로 실행되었다면 브라우저 또는 API 테스트 도구를 이용해 애플리케이션에 접속해보자.

예를 들어 Node.js 예제에서 `3000:3000`으로 포트를 매핑했다면,
브라우저에서 `http://localhost:3000` 으로 접속하거나 Postman을 통해 API를 호출해 정상 응답이 반환되는지 확인해보자.
정상적으로 응답이 반환된다면 Docker 환경 구성은 완료된 것이다.
3. CI/CD 파이프라인 구성 (Github Actions)
Docker 환경 구성이 완료되었다면 이제 Github Actions를 이용해 배포 자동화를 구성해보자.
이제부터는 Github 저장소에 코드를 push 하면 EC2 서버에 자동으로 최신 코드를 반영하도록 설정한다.
3.1 EC2 Docker 환경 사전 준비
CI/CD를 구성하기 전에 반드시 EC2에서 아래 3가지는 먼저 확인해야 한다.
먼저 EC2 인스턴스에 접속하자.
01 Docker 설치 여부
아래 명령어를 통해 Docker 설치 여부를 확인한다.
docker --version
정상적으로 출력되지 않는다면, 아래 명령어를 이용해 Docker를 설치하자.
sudo apt update
sudo apt install -y docker.io
02 Docker 권한 설정
EC2에서 Docker 명령어는 기본적으로 ``sudo`` 권한이 필요하다.
그래서 반드시 아래 명령어를 실행해야 한다.
sudo usermod -aG docker ubuntu
newgrp docker
03 Docker Compose 설치 (v2 기준)
요즘 Ubuntu 에서는 `docker-compose-plugin` 방식으로 설치해야 한다.
아래 명령어를 통해 Docker Compose 설치 여부를 확인해준다.
docker compose version
마찬가지로, 정상 출력이 안된다면, 아래 명령어로 Docker Compose를 설치한다.
sudo apt install -y docker-compose-plugin
3.2 EC2 프로젝트 초기 세팅
CI/CD가 동작하려면 EC2에 최소 1회는 프로젝트가 존재해야 한다.
아래 명령어로 EC2 인스턴스 내부에 배포할 프로젝트를 클론 받도록 하자.
mkdir -p /home/ubuntu/app
cd /home/ubuntu/app
git clone https://github.com/wldmsdl7/nodejs-deploy-test .
⚠️ 이때, Github 저장소 뒤에 "." 을 빼먹으면 안된다 !!!!

EC2에 프로젝트를 클론했다고 해서 모든 설정이 자동으로 완료되는 것은 아니다.
특히 데이터베이스 연결 정보, JWT Secret, API Key와 같은 민감한 정보는 보안상 Github 저장소에 포함하지 않기 때문에 `git clone`으로는 전달되지 않는다. 따라서 EC2 서버에서는 별도로 `.env` 파일을 직접 생성해야 한다.
아래 명령어로 `.env` 파일을 생성한 뒤, 애플리케이션 실행에 필요한 환경 변수를 직접 입력해준다.
cd /home/ubuntu/app
nano .env
`.env` 파일은 Github 저장소에 업로드하지 않는 것이 일반적이며, 운영 서버 내부에서 별도로 관리한다.
이때, ``DATABASE_URL``은 배포된 RDS의 주소를 작성해줘야 한다는 점 주의해야 한다.
⚠️ 운영 서버에서는 SSH 터널링을 사용하지 않는다.
애플리케이션과 RDS가 같은 VPC 내부에 존재하므로, DATABASE_URL에는 RDS Endpoint를 직접 사용하면 된다.
3.3 SSH Key 생성
Github Actions가 EC2 서버에 접속하려면 SSH 인증이 필요하다.
먼저 Github Actions 전용 SSH Key를 생성한다.
EC2 서버가 아닌 로컬 PC에서 아래 명령어를 실행하자.
ssh-keygen -t rsa -b 4096 -C "github-actions"
터미널에 다음과 같이 출력되면, 파일명을 입력한다.
Generating public/private rsa key pair.
Enter file in which to save the key
(/Users/user/.ssh/id_rsa):
이후, 비밀번호를 입력하는 란이 등장할텐데, 우리는 비밀번호를 별도로 설정하지 않을 것이다.
``Enter`` 키를 2번 눌러 비밀번호를 설정하지 않고 SSH Key를 생성한다.
❓ SSH 키 중요한 거 아니야 ?? 왜 비밀번호를 설정하지 않지 ??
간단히 말하면, 자동화 때문이다. 만약 SSH Key 생성 시 비밀번호를 설정하면, SSH 접속할 때 마다 비밀번호를 요구하게 된다.
사람이 직접 접속하는 경우에는 비밀번호를 입력할 수 있지만, Github Actions를 통해 자동으로 SSH 접속해 배포할 때에는 비밀번호를 입력할 사람이 없다.
따라서, CI/CD 용 배포 키는 일반적으로 별도의 SSH Key를 생성, Github Secret에 저장, 배포 전용 계정으로 배포 방식을 사용한다. 대신 보안을 위해 개인 PC에서 사용하던 SSH Key를 Github Secret에 올리는 것이 아니라, 배포 전용 키를 새로 생성하는 것이 중요하다.
생성이 완료되면, `github-actions` 와 `github-actions.pub` 두 파일이 생성된다.
아래 명령어를 통해 생성된 공개 키 내용을 확인한다.
cat github-actions.pub
출력된 내용을 복사한다.

이후, 배포한 EC2 서버에 접속한다.
나는 터미널을 이용하지 않고, AWS Console을 이용해 EC2 인스턴스에 직접 접속할 것이다.
접속했다면, 아래 명령어를 이용해 `~/.ssh/authorized_keys`에 복사한 SSH Key 를 맨 아래에 추가한다.
nano ~/.ssh/authorized_keys
아래 사진처럼 기존에 존재하는 키 아래에 추가하고, 저장 후 종료한다.

이제 아래 명령어를 이용해 로컬에서 해당 SSH 키로 EC2 인스턴스에 접속해보자.
ssh -i github-actions ubuntu@EC2_PUBLIC_IP

3.4 Github Secrets 등록
SSH Key 생성과 EC2 등록이 완료되었다면 이제 Github Actions 에서 해당 키를 사용할 수 있도록 Github Secrets에 등록해야 한다.
Github Secrets 는 비밀번호, API Key, SSH Private Key와 같이 외부에 노출되면 안 되는 민감한 정보를 안전하게 저장할 수 있는 기능이다.
우리가 조금 전에 생성한 SSH Key 중 Github Actions가 실제로 사용할 것은 공개 키(.pub)가 아니라 개인 키 이다.
따라서 로컬 PC에서 아래 명령어를 실행하여 개인 키 내용을 확인한다.
cat github-actions
출력된 내용을 전체 복사한다.
이후 Github 저장소로 이동한 뒤 ``Repository > Settings > Secrets and variables > Actions > New repository secret`` 으로 이동해 아래와 같이 Secret 을 생성한다.
| Name | Value |
| EC2_SSH_KEY | github-actions 개인 키 전체 내용 |

다음으로 EC2 접속에 필요한 서버 정보도 함께 등록한다.
| Name | Value |
| EC2_HOST | EC2 Public IP |
| EC2_USER | ubuntu |

이렇게 등록한 값들은 Github Actions 실행 시 환경 변수처럼 사용할 수 있으며, Workflow 파일에서 `${{ secrets.SECRET_NAME }}` 형태로 참조할 수 있다.
⚠️ Github Actions에 등록하는 것은 반드시 개인 SSH 키이다.
`github-actions.pub` 파일의 내용을 등록하는 것이 아니라 `github-actions` 파일의 내용을 등록해야 정상적으로 SSH 접속이 가능하다.
이제 Github Actions 가 사용할 SSH 인증 정보와 서버 접속 정보가 모두 준비되었다.
다음 단계에서는 실제로 Github 저장소에 코드가 Push 되었을 때 자동으로 EC2 서버에 배포가 수행되도록 Workflow 를 작성해보자.
3.5 Github Actions Workflow 작성
이제 실제로 GitHub 저장소에 코드가 push 되었을 때 EC2 서버로 자동 배포가 수행되도록 Workflow를 작성해보자.
이 단계부터는 단순한 설정이 아니라, 앞에서 구성한 Docker 환경과 EC2 SSH 연결이 실제로 하나의 배포 파이프라인으로 연결되는 부분이다.
이제 본격적으로 Workflow를 작성해보자.
Github Actions Workflow는 저장소의 `.github/workflows` 디렉터리에 YAML 파일 형태로 작성한다.

나는 다음과 같이 `deploy.yml` 이라는 파일을 생성해주었다.
이제 아래 코드를 넣어준다.
name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Connect to EC2 via SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
echo "Start Deployment"
echo "Move to project directory"
cd /home/ubuntu/app
echo "Pull latest code from GitHub"
git pull origin main
echo "Stop existing containers"
docker compose down
echo "Build Docker image"
docker compose build
echo "Start containers"
docker compose up -d
echo "Deployment Success"
GitHub Actions에서는 하나의 SSH Step으로 EC2에 접속한 뒤,
서버 내부에서 Docker 기반 배포 파이프라인을 순차적으로 실행하도록 구성한다.
이렇게 구성하면 Github push 만으로 EC2 서버에 자동 배포되는 환경이 완성된다.
04. 배포 테스트
이제 아래 사진처럼 Github에 push 해준다.

그럼 이렇게 아래처럼 배포를 성공한 것을 볼 수 있다.

실제 배포된 스웨거를 접속해보면, 아래처럼 잘 뜨는 것까지 확인할 수 있다 !

성공 !
'DevOps' 카테고리의 다른 글
| [Infra/DevOps] 사이드 프로젝트를 위한 AWS 인프라 구축 가이드 (2026 ver.) #2- RDS, SSH 터널링 (0) | 2026.06.14 |
|---|---|
| [Infra/DevOps] 사이드 프로젝트를 위한 AWS 인프라 구축 가이드 (2026 ver.) #1- VPC, EC2, Security Group (1) | 2026.06.13 |
| [AWS] ECS 이용하여 ECR에 수동으로 배포해보기 (+ ECS, ECR 개념 정리) (0) | 2025.11.05 |
| [AWS] IAM 권한 : 그룹 권한과 인라인 권한 차이 정리 (0) | 2025.10.26 |
| [AWS] AWS의 정의와 주요 서비스 (0) | 2025.10.18 |