0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

メモリに余裕のない VPS へ GitHub Actions と GHCR で自動デプロイする(ビルドはランナー、VPS は pull だけ)

0
Last updated at Posted at 2026-10-25

この記事でできること

GitHub Actions と GitHub Container Registry(GHCR)で、VPS への自動デプロイを作ります。main ブランチに push すると、GitHub Actions のランナーで Docker イメージをビルドして GHCR に push し、VPS ではそのイメージを pull して入れ替えるだけです。私の環境では、ビルドのキャッシュが使えるときは push してから約1分でサイトに反映されます。題材は VPS で公開している Next.js のポートフォリオサイトです。

VPS のメモリに余裕がなく、VPS の上ではビルドしたくない人に向けて書いています。GitHub Actions と Docker Compose の基本は知っている前提です。

前提と環境

2026年10月に構築したときの環境です。

項目 内容
VPS Ubuntu 26.04 LTS、x86_64、メモリ 7.7GB
VPS のソフトウェア Docker と Docker Compose、nginx(リバースプロキシ)、certbot(HTTPS)
アプリ Next.js 16.4(output: "standalone")、イメージのベースは node:24-alpine
手元 Apple Silicon の Mac
Actions actions/checkout@v4、docker/setup-buildx-action@v3、docker/login-action@v3、docker/build-push-action@v6、appleboy/ssh-action@v1

VPS では、ほかのサービス(ゲームサーバーなど)がメモリを約4GB 使っていて、空きは約3GB です。Next.js のビルドでほかのサービスに影響を出したくないので、VPS ではビルドしないことにしました。

nginx は portfolio.example.com へのリクエストを 127.0.0.1:3000 のコンテナへ流すように設定してあります。nginx と HTTPS の設定は、この記事では扱いません。

記事の中の名前は次のように置き換えています。

  • ドメイン: example.com
  • GitHub のアカウント: takibito
  • VPS への SSH: ~/.ssh/config に Host vps として書いてある前提で ssh vps

構成

git push(main)
   │
   ▼
GitHub Actions
 ├ build-and-push:linux/amd64 でビルドし、GHCR に push
 │     ghcr.io/takibito/portfolio:latest と :<コミットの SHA>
 ▼
 └ deploy:SSH で VPS に入り、docker compose pull && docker compose up -d
   │
   ▼
https://portfolio.example.com に反映

ジョブは build-and-push と deploy の2つに分けています。deploy には needs: build-and-push を書き、ビルドと push が成功したときだけ動くようにしています。

自動化する前の手動デプロイ

自動化する前は、Mac でイメージをビルドし、docker save の出力を ssh で VPS の docker load に流し込んでいました。Mac は Apple Silicon(arm64)で VPS は x86_64 なので、--platform linux/amd64 を付けてビルドします。

docker buildx build --platform linux/amd64 -t portfolio:latest --load .
docker save portfolio:latest | gzip | ssh vps 'gunzip | docker load'
ssh vps 'cd ~/portfolio && docker compose up -d && docker image prune -f'

この方法ならレジストリがなくても公開できますが、更新のたびに Mac でビルドしてイメージを送る必要がありました。この3行を GitHub Actions に置き換えます。手動の方法は、Actions が止まったときのために今も使えるようにしてあります。

手順

1. デプロイ用の SSH 鍵を作って VPS に登録する

GitHub Actions から VPS に入るための専用の鍵を作ります。普段使っている鍵と分けておくと、デプロイ用の鍵だけを消したり作り直したりできます。

ssh-keygen -t ed25519 -f ~/.ssh/portfolio_deploy -C "gha-deploy portfolio" -N ""
ssh vps 'cat >> ~/.ssh/authorized_keys' < ~/.ssh/portfolio_deploy.pub
pbcopy < ~/.ssh/portfolio_deploy   # 秘密鍵をクリップボードへ(次の手順で Secrets に貼る)

sshd_config の AuthorizedKeysFile で鍵のファイルを変えている場合は、そのファイルに追記します。

2. GitHub の Secrets を登録する

リポジトリの Settings → Secrets and variables → Actions で、次の3つを登録します。

Secret 名 値
SSH_HOST VPS のホスト名
SSH_USER VPS で docker を実行できるユーザー
SSH_KEY 手順1で作った秘密鍵の中身(-----BEGIN の行から -----END の行まで)

GHCR に push するためのトークンは登録しません。ワークフローに自動で発行される GITHUB_TOKEN に packages: write の権限を与えて使います1。

3. VPS の docker-compose.yml を GHCR のイメージに切り替える

手動デプロイのときは、docker load したイメージ portfolio:latest を使っていました。docker-compose.yml を書き換えて、GHCR のイメージを pull するようにします。

~/portfolio/docker-compose.yml
services:
  portfolio:
    image: ghcr.io/takibito/portfolio:latest
    pull_policy: always
    ports:
      - "127.0.0.1:3000:3000"
    restart: unless-stopped
    mem_limit: 512m

pull_policy: always にすると、Compose は起動のたびにレジストリからイメージを pull します2。mem_limit はコンテナが使えるメモリの上限で、ほかのサービスのメモリを圧迫しないように 512MB にしています。ポートは 127.0.0.1 にだけ公開し、外からは nginx を通して届くようにしています。

この時点では GHCR にまだイメージがないので、docker compose up はしません。

4. ワークフローを追加して push する

.github/workflows/deploy.yml を作ります。

.github/workflows/deploy.yml
name: Deploy portfolio

on:
  push:
    branches: [main]
  workflow_dispatch:        # 手動実行も可能にする

concurrency:
  group: deploy-portfolio
  cancel-in-progress: true  # 連続 push 時は最新だけデプロイ

env:
  IMAGE: ghcr.io/takibito/portfolio

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write        # GHCR へ push するため
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          push: true
          tags: |
            ${{ env.IMAGE }}:latest
            ${{ env.IMAGE }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy over SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd ~/portfolio
            docker compose pull
            docker compose up -d
            docker image prune -f
  • concurrency の cancel-in-progress: true は同じグループで実行中のワークフローを取り消す設定です3。続けて push したときは、最後の push だけがデプロイされます。
  • イメージには latest とコミットの SHA の2つのタグを付けます。SHA のタグはあとで特定の版に戻すときに使います。
  • cache-to の type=gha でビルドのキャッシュを GitHub Actions のキャッシュに保存し、cache-from で次のビルドのときに読み込みます4。
  • イメージ名は小文字で書きます。イメージ名に使えるのは英小文字・数字・区切り文字だけです5。

2026年10月にこのワークフローを動かしたときは、警告が出ました6。actions/checkout@v4、docker/setup-buildx-action@v3、docker/login-action@v3、docker/build-push-action@v6 の4つが Node.js 20 向けのため、Node.js 24 で動かしたという内容です。ジョブは成功しましたが、これから使う場合は、各アクションの新しいメジャーバージョンを確かめてください。

Next.js の Dockerfile は output: "standalone" の出力7を使う3段のマルチステージビルドにしています。

Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:24-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

ワークフローは push した時点で動くので、Secrets と VPS 側の準備が済んでから push します。

git add .github/workflows/deploy.yml
git commit -m "ci: GitHub Actions による自動デプロイを追加"
git push -u origin main

5. GHCR のパッケージの公開範囲を確かめる

初めての push で、GHCR にパッケージ portfolio ができます。GitHub のドキュメントには、初めて公開したパッケージは Private になり、リポジトリと紐付いていても公開範囲は引き継がれないと書かれています8。

ただ、私の場合は公開範囲の設定を変えていないのに、パッケージは最初から Public で、ログインしなくても pull できる状態でした。個人アカウントの公開リポジトリから GITHUB_TOKEN で push した場合です。ドキュメントと違う結果なので、初めて push したあとは、パッケージのページで公開範囲を確かめてください。

GHCR のパッケージのページ。Public と表示され、latest とコミットの SHA の2つのタグが付いている

Private だった場合は、Public にすれば VPS から匿名で pull できるので1、VPS で docker login をしなくて済みます。公開範囲は GitHub のプロフィールの Packages → portfolio → Package settings → Change visibility で変えられます。一度 Public にしたパッケージは Private に戻せないので8、公開してよいイメージかを先に確かめてください。

Private のままにする場合は、read:packages だけを付けたアクセストークンを作り、VPS で一度だけ docker login ghcr.io をしておきます。

6. 動作確認

push したあとは、次の3点を確かめます。

  • GitHub の Actions タブで、build-and-push と deploy の2つのジョブが成功している
  • VPS で docker compose ps を実行すると、コンテナが動いている
  • curl -I https://portfolio.example.com が 200 を返す

GitHub Actions の実行結果。build-and-push が45秒、deploy が16秒で、どちらも成功している

私の環境では、ワークフローを追加した最初の push は、ビルドのキャッシュがないので2分50秒かかりました。そのあと小さな変更を1つ push すると、キャッシュが使えて1分8秒で終わり、サイトにも反映されました(上の画面)。これ以降は main に push するだけで公開されます。

最初の push の前に確かめること

ワークフローのファイルをコミットしたか

最初の push では、ワークフローが動きませんでした。.github/ などのファイルをコミットしておらず、GitHub には Create Next App の最初のコミットしか届いていなかったのが原因です。git status で未コミットのファイルが残っていないかを確かめてから push してください。

空の public/ が Git に入っているか

Dockerfile の最後の段では COPY --from=builder /app/public ./public で public/ をコピーします。私は Create Next App が置いた SVG を消していたので、public/ は空でした。Git は空のフォルダを記録しないため、そのまま push すると、ランナーで checkout したソースには public/ がなく、この COPY が失敗します。Mac でビルドしていたときは手元に空の public/ があったので、問題になりませんでした。ワークフローを push する前に気づき、public/.gitkeep を追加しました。

deploy ジョブの終了ステータス

deploy のスクリプトは、途中の cd や docker compose pull が失敗しても、最後の docker image prune -f が成功すればジョブ全体が成功になります。シェルのスクリプトの終了ステータスは、最後に実行したコマンドのステータスになるためです。appleboy/ssh-action の README でも、途中で止めたいときはスクリプトの先頭に set -e を書くよう案内されています9。

          script: |
            set -e
            cd ~/portfolio
            docker compose pull
            docker compose up -d
            docker image prune -f

私のワークフローには入れていません。Actions が成功しているのにサイトが変わらないときは、ここを疑うようにしています。

特定の版に戻す

latest のほかにコミットごとの SHA のタグが残るので、VPS で特定の版に戻せます。docker-compose.yml の image を ghcr.io/takibito/portfolio:<戻したいコミットの SHA> に書き換えて、docker compose up -d を実行します。

この状態で次に push しても、docker compose pull は SHA のタグを pull するので、サイトは戻した版のままです。問題を直したコミットを push したあと、image を latest に戻して docker compose pull と docker compose up -d を実行します。ずっと戻しておきたいときは、問題のあるコミットを git revert して push し、正しい版をビルドし直すのが安全です。

さらに安全にするなら

  • デプロイ専用のユーザーを作って docker グループに入れ、SSH_USER をそのユーザーにする
  • authorized_keys でデプロイ用の鍵の行頭に from= や command= を付け、鍵の用途を絞る(GitHub Actions の IP の範囲は広いので、from= の運用には手間がかかります)
  • latest を追うのをやめ、SHA のタグを指定してデプロイする(どの版が動いているかが分かりやすくなります)
  1. GitHub Docs「Working with the Container registry」 https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry ↩ ↩2

  2. Docker Docs「Define services in Docker Compose」(pull_policy、mem_limit) https://docs.docker.com/reference/compose-file/services/ ↩

  3. GitHub Docs「Control workflow concurrency」 https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency ↩

  4. Docker Docs「Cache management with GitHub Actions」 https://docs.docker.com/build/ci/github-actions/cache/ ↩

  5. distribution/reference の文法の定義(path-component、alpha-numeric) https://github.com/distribution/reference/blob/main/reference.go ↩

  6. GitHub Changelog「Deprecation of Node 20 on GitHub Actions runners」 https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ ↩

  7. Next.js Docs「output」 https://nextjs.org/docs/app/api-reference/config/next-config-js/output ↩

  8. GitHub Docs「Configuring a package's access control and visibility」 https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility ↩ ↩2

  9. appleboy/ssh-action の README https://github.com/appleboy/ssh-action ↩

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?