NestJS・MySQL・Prisma・Dockerを使用してAWS EC2へGitLab CI/CDでデプロイする方法
ソフトウェア開発において、アプリケーションを手動でサーバーへデプロイする作業は時間がかかり、ミスが発生しやすいです。そのため、**CI/CD(Continuous Integration / Continuous Deployment)**によるデプロイの自動化は、作業効率の向上とシステムの安定性確保において非常に重要です。
本記事では、NestJSアプリケーションをAWS EC2へDockerを用いてデプロイし、GitLab CI/CDを使ってtest → build → deployまでの一連の流れを自動化する、完全なCI/CDパイプラインの構築方法を解説します。
1. システム構成
本記事で扱うシステムは、以下の主要コンポーネントで構成されています:
-
AWS EC2(Ubuntu):アプリケーションを実行するサーバー
- RAM: 8GB
- Storage: 30GB
-
NestJS:APIを構築するバックエンドフレームワーク。
-
Prisma:MySQLデータベースと連携するためのORM。
-
MySQL:データベース管理システム。
-
Nginx:トラフィックをアプリケーションへ振り分けるリバースプロキシ。
-
GitLab CI/CD:ビルドおよびデプロイの自動化。
-
Docker & Docker Compose:各サービスをコンテナとしてパッケージ化・管理。
全体アーキテクチャ
Developer
│
│ push code
▼
GitLab Repository
│
│ GitLab CI/CD Pipeline
▼
Build Docker Image
│
▼
GitLab Container Registry
│
▼
AWS EC2
│
└── docker compose pull + up
│
├── NestJS App
├── MySQL
├── Nginx
└── phpMyAdmin
2. ソースコードの準備
サンプル用のリポジトリを用意しています。こちらからご利用ください:
https://gitlab.com/vmhfpt/nestjs-project-deploy/-/tree/main
まずは、ローカル環境にクローンします:
git clone https://gitlab.com/vmhfpt/nestjs-project-deploy.git
クローンが完了したら、以下の手順を進めてください:
- 自身の GitLab アカウントで新しいリポジトリを作成します。
- 作成したリポジトリにソースコードを Push し、CI/CD の設定を開始しましょう。
3. AWS で EC2 インスタンスを作成する
詳細な手順については、以下のガイドを参考にしてください:
https://qiita.com/tuanphan/items/56cba7619ca0e3cf0d4d
まずは AWS コンソール → EC2 → インスタンスを起動 にアクセスします。
推奨構成(設定例):
- OS: Ubuntu
-
インスタンスタイプ:
t2.large - メモリ (RAM): 8GB
- ストレージ: 30GB
インスタンスの作成が完了したら、SSH でサーバーに接続します:
chmod 400 "nestjs.pem"
ssh -i "nestjs.pem" ubuntu@ec2-54-196-78-128.compute-1.amazonaws.com
4. EC2上の環境構築
Dockerのインストール
詳細な手順はこちらを参照してください:
-> 注意:後々の権限(permission)エラーを防ぐため、sudoなしでDockerを実行できるように設定してください。
簡単な確認:
docker -v
docker ps -a
GitLab Runnerのインストール
GitLab Runnerは、パイプライン内のジョブを実行する役割を担うコンポーネントです。
sudo apt update
curl -L [https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh](https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh) | sudo bash
sudo apt install gitlab-runner -y
確認:
gitlab-runner --version
その後、GitLabプロジェクトに**ランナーを登録(register)**します。
以下のコマンドを実行します:
sudo gitlab-runner register
その後、GitLabから要求される情報を入力します:
- GitLab instance URL: https://gitlab.com
- Registration token:
Project
→ Settings
→ CI/CD
→ Runners
→ New project runner
GitLab上でランナーを1つ作成し、トークンをコピーしてターミナルに貼り付けます。
- Runner name: (空白のまま)Enterキーを押します。
- Runner description: (空白のまま)Enterキーを押します。
- Executor:
dockerと入力します。 - Please enter the default Docker image (e.g. ruby:2.7):
node:20と入力します。
ステータスを確認し、GitLab RunnerがGitLabに正常に接続されていることを確認します。
sudo gitlab-runner status
sudo gitlab-runner verify
デプロイ用のSSHキーの作成
SSHキーを作成します:
ssh-keygen -t ed25519 -C "hung.vu@tomosia.com"
サーバー自身を「許可」するための設定。
スクリプトはSSHを使用してそのサーバー自体(または別のサーバー)を制御するため、公開鍵(Public Key)を信頼できるリストに追加する必要があります:
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
秘密鍵(Private Key)を取得してGitLabに貼り付ける
cat ~/.ssh/id_ed25519
CI/CDへの追加方法:
- その内容をすべてコピーします(BEGIN行とEND行を含みます)。
- GitLab Project -> Settings -> CI/CD -> Variables に移動します。
- Add Variable をクリックします:
- Key: DEPLOY_SSH_KEY
- Value: 先ほどコピーした内容を貼り付けます
- Type: Fileを選択します(スクリプトで
chmod 600 $DEPLOY_SSH_KEYコマンドを使用するため、これは非常に重要です。このコマンドは、変数がランナー上の物理ファイルである場合にのみ機能します)
-> 注意:キーを貼り付けた後、エラーを防ぐために最後に空白行を追加してください。
サーバー上での設定ファイルの準備
以下のファイルを準備する必要があります:
nginx/default.conf
docker-compose.yml
.env
デプロイ用ディレクトリの作成:
mkdir -p /home/ubuntu/app
cd /home/ubuntu/app
1. Nginxのセットアップ
mkdir nginx
vim nginx/default.conf
Vimの使い方:
- 挿入モード(Insert)に入るには
iを押します。 - 保存してVimを終了するには:
Escを押してから:wq!と入力してEnterを押します。
以下の内容を貼り付けて保存します\
server {
listen 80;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
2. docker-compose.ymlのセットアップ
vim docker-compose.yml
version: "3.9"
services:
mysql:
image: mysql:8
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: nestdb
MYSQL_USER: nest
MYSQL_PASSWORD: nest123
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
nginx:
image: nginx:latest
container_name: nginx_proxy
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
restart: always
phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
restart: always
ports:
- "8080:80"
environment:
PMA_HOST: mysql
PMA_PORT: 3306
depends_on:
- mysql
app:
image: ${CI_REGISTRY_IMAGE}:latest
container_name: nest-app
restart: always
ports:
- "3000:3000"
environment:
DATABASE_URL: "mysql://nest:nest123@mysql:3306/nestdb?allowPublicKeyRetrieval=true"
depends_on:
mysql:
condition: service_healthy
volumes:
mysql_data:
3. .envのセットアップ
vim .env
DATABASE_URL="mysql://root:root@localhost:3307/nestdb"
ディレクトリの概要:
必要なポートの開放
**EC2のセキュリティグループ(Security Group)**で以下のポートを開放します:
- 時的にすべてのトラフィック(All traffic)を許可するルールを1つ作成します
| Port | Purpose |
|---|---|
| 80 | Nginx |
| 8080 | phpMyAdmin |
| 3307 | MySQL(リモートアクセスが必要な場合 |
5. GitLab CI/CD 変数の設定
GitLab上:
Settings → CI/CD → Variables
以下の変数を追加します:
DEPLOY_HOST
DEPLOY_USER
DEPLOY_PATH
上記3つの環境変数すべてに対して、Type、Environments、Visibility、Flagsを設定します。
- 対応する値(value)の取得元
DEPLOY_PATH
/home/ubuntu/app
-> 注意:サーバー上のデプロイ先ディレクトリと一致する必要があります。
事前に実行したコマンド:
mkdir -p /home/ubuntu/app
6. GitLab CI/CD パイプラインの設定
ファイルの作成:
.gitlab-ci.yml
パイプラインは3つのステージで構成されます:
stages:
- test
- build
- deploy
GitLab CI/CD Pipeline
# GitLab CI/CD for Nest app (Docker + docker-compose + docker-entrypoint.sh)
#
# Required CI/CD variables for deploy (Settings > CI/CD > Variables):
# DEPLOY_SSH_KEY - Private SSH key to access the deploy server (type: File or Variable, protected)
# DEPLOY_HOST - Deploy server hostname or IP
# DEPLOY_USER - SSH user on deploy server
# DEPLOY_PATH - Path on server where docker-compose runs (must use app image from registry)
#
# On deploy server: ensure docker-compose uses image from registry, e.g.:
# app: image: ${CI_REGISTRY_IMAGE}:latest (and run docker login before compose)
stages:
- test
- build
- deploy
variables:
NODE_VERSION: "20"
# -----------------------------------------------------------------------------
# CI: Lint, build, test (unit tests; no DB required)
# -----------------------------------------------------------------------------
test:
stage: test
image: node:${NODE_VERSION}
tags:
- ec2-node-runner
cache:
key: ${CI_COMMIT_REF_SLUG}-npm
paths:
- node_modules/
before_script:
- npm ci
- npx prisma generate
script:
- npm run lint
- npm run build
- npm run test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# -----------------------------------------------------------------------------
# CD: Build Docker image and push to GitLab Container Registry
# -----------------------------------------------------------------------------
build:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# -----------------------------------------------------------------------------
# CD: Deploy (example: pull image and run with docker-compose on a runner/server)
# Configure CI/CD variables: DEPLOY_SSH_KEY, DEPLOY_HOST, etc. as needed.
# -----------------------------------------------------------------------------
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- chmod 600 $DEPLOY_SSH_KEY
- cat $DEPLOY_SSH_KEY
- ssh-add $DEPLOY_SSH_KEY
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts 2>/dev/null || true
script:
- |
ssh $DEPLOY_USER@$DEPLOY_HOST "
cd $DEPLOY_PATH &&
echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY &&
export CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE &&
docker compose pull &&
docker compose up -d
"
environment:
name: production
url: https://$DEPLOY_HOST
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
7. Dockerfileの準備
# Dockerfile
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npx prisma generate
RUN npm run build
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
RUN chmod +x /app/docker-entrypoint.sh
EXPOSE 3000
ENTRYPOINT ["/app/docker-entrypoint.sh"]
docker-entrypoint.sh
# docker-entrypoint.sh
#!/bin/sh
set -e
# Generate Prisma client (already done at build, safe to run again)
npx prisma generate
# Apply schema: use migrations if present, otherwise db push (e.g. when prisma/migrations is not in image)
if [ -d "prisma/migrations" ] && [ -n "$(find prisma/migrations -maxdepth 2 -name 'migration.sql' 2>/dev/null)" ]; then
npx prisma migrate deploy
else
npx prisma db push
fi
# Run seed (skip failure so app still starts if DB is not ready or already seeded)
npm run prisma:seed || true
exec node dist/src/main.js
docker-compose.yml
# docker-compose.yml
version: "3.9"
services:
mysql:
image: mysql:8
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: nestdb
MYSQL_USER: nest
MYSQL_PASSWORD: nest123
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
nginx:
image: nginx:latest
container_name: nginx_proxy
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
restart: always
phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
restart: always
ports:
- "8080:80"
environment:
PMA_HOST: mysql
PMA_PORT: 3306
depends_on:
- mysql
app:
build: .
container_name: nest-app
restart: always
ports:
- "3000:3000"
environment:
DATABASE_URL: "mysql://nest:nest123@mysql:3306/nestdb?allowPublicKeyRetrieval=true"
depends_on:
mysql:
condition: service_healthy
volumes:
mysql_data:
9. GitLabのネームタグ設定
- GitLabの画面を開く -> プロジェクトを選択 -> CI/CD設定(CI/CD setting) -> 作成したランナーを選択 -> 編集(edit)
-> 注意:name tagは .gitlab-ci.yml ファイルの tags キーと一致する必要があります。
8. 結果の確認
mainブランチにコードをプッシュ(push)すると、GitLab Pipelineが自動的に実行されます。
以下の場所から進捗を確認できます:
GitLab → Build → Pipelines
デプロイが成功した後、以下にアクセスします:
http://EC2_IP
アプリケーションの動作確認を行います。
-> 注意:https から http に変更してください。
例:https://your-ip -> http://your-ip
結果:
http://your-ip:8080 (phpMyAdmin)
http://your-ip/posts (ランダムなPostsを取得)

結論
完全なCI/CDパイプラインの構築に成功しました:
Code
↓
Test
↓
Build Docker Image
↓
Push Registry
↓
Deploy to EC2
Dockerを使用することで開発(development)環境と本番(production)環境の同一性を保証し、同時にGitLab CI/CDがデプロイプロセス全体を自動化してくれます。
参考リポジトリ:
[https://gitlab.com/vmhfpt/nestjs-project-deploy/-/tree/main](https://gitlab.com/vmhfpt/nestjs-project-deploy/-/tree/main)
実際のデプロイプロセスにおいて、例えば以下のようなよくあるエラーに遭遇する可能性があります:
Permission denied (publickey)
これは通常、GitLab RunnerとEC2サーバー間のSSH設定に関連するエラーです。接続が正常に確立されていることを確認するために、SSHキー、アクセス権限(authorized_keys)、およびGitLab CI/CDの環境変数の設定を再確認してください。
この記事が、実践的なCI/CDシステムの構築方法をより深く理解し、ご自身のプロジェクトに効果的に適用する助けになれば幸いです。






















