ConoHa VPSでDocker RegistryをセルフホストしてGitHubからコンテナサービスを簡単デプロイする

:::note info
この記事は [ConoHaのカレンダー | Advent Calendar 2023](https://qiita.com/advent-calendar/2023/conoha) の7日目の記事です。
:::

VPS上にアプリケーションを簡単にデプロイしたい。
そういう気持ちで何年も色々試行錯誤してきましたので、現状の最適解をまとめて共有しようと思います。

GitHubへプッシュした後、GitHubのGUIからデプロイアクションのボタンをクリックすると、
イメージビルド、レジストリへのプッシュ、サービスの再起動が自動で行われるようになります。

また、GitHub Actionsは実行タイミングを設定できるので、
mainブランチへコミットが発生したタイミングで自動デプロイや、毎日午前0時に自動デプロイなども可能です。

デプロイイメージ

![adventcalendar.2023.9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/38d13e27-35f8-a8d3-407a-69b0ba6f4457.png)

自動デプロイがセットアップされたリポジトリ

https://github.com/a10adotapp/2023.adventcalendar.a10a.app

# アプリケーションを作成する

![adventcalendar.2023.1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/c9377083-519b-cc45-5103-acdcd2a97897.png)

まずVPSで動かしたいアプリケーションを作成します。
今回はホスト名を返却するだけの簡単なWebサーバを作成しました。

<details>
<summary><b>main.go</b></summary>

``` go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.ListenAndServe(":3000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		fmt.Fprintf(w, "{\"message\":\"hello from %s\"}\n", r.Host)
	}))
}
```

</details>

``` shell
2023.adventcalendar.a10a.app % curl http://localhost:3000 -v
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Sat, 02 Dec 2023 10:54:44 GMT
< Content-Length: 40
<
{"message":"hello from localhost:3000"}
* Connection #0 to host localhost left intact
```

# GitHubのリポジトリへプッシュする

![adventcalendar.2023.2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/1f1a627a-b8c0-46a9-76ed-9b4e265649ed.png)

作成したリポジトリはGitHubへプッシュしておきます。

``` shell
2023.adventcalendar.a10a.app % git push origin main
```

# ConoHa VPSサーバーを立ち上げる

![adventcalendar.2023.3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/4ce9e10f-77fe-418e-aa9f-4d61fd2eada0.png)

ConoHaのダッシュボードからVPSサーバを追加します。
今回は使い捨てるので設定は適当です。

![スクリーンショット 2023-12-02 21.17.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/118790e1-f9d9-b0b5-bb0c-2930c6ff5e8d.png)

![スクリーンショット 2023-12-04 8.43.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/a59b8e8f-0f5d-63b0-567d-86baa604380a.png)

![スクリーンショット 2023-12-04 8.44.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/df98aac1-af7d-fe88-536d-3602752aa68e.png)

https://docs.docker.jp/engine/installation/linux/docker-ce/centos.html

VPSサーバが起動したらSSHでログインします。

``` shell
2023.adventcalendar.a10a.app % ssh root@133.130.91.35
```

VPSにDockerをインストールします。

``` shell
[root@133-130-91-35 ~]# yum install \
>	device-mapper-persistent-data \
>	lvm2 \
>	yum-utils
```

``` shell
[root@133-130-91-35 ~]# yum-config-manager \
>	--add-repo \
>	https://download.docker.com/linux/centos/docker-ce.repo
```

``` shell
[root@133-130-91-35 ~]# yum install docker-ce
```

``` shell
[root@133-130-91-35 ~]# systemctl start docker
```

# Nginx Proxyコンテナを立ち上げる

![adventcalendar.2023.4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/785c7212-24e5-c0ef-cccd-fd3c2ee3a4d5.png)

Nginx ProxyがVPSサーバへのHTTPリクエストを適切なコンテナサービスへルーティングします。

今回の場合、registry.a10a.appのホスト名で来たリクエストはDockerレジストリのサービスへ、adventcalendar.a10a.appのホスト名で来たリクエストは最初に作成したWebサーバのサービスへ流れるようにしていきます。

:::note warn
今回使用しているa10a.appのドメインはcloudflareで管理しており、cloudflareがHTTPSのリクエストをいい感じにしてくれています。
そのためVPSサーバではHTTPSの設定は行いませんが、必要であれば[nginx-proxy/acme-companion](https://github.com/nginx-proxy/acme-companion)を使用して簡単にHTTPSに対応することができます。
:::

https://github.com/nginx-proxy/nginx-proxy

``` shell
[root@133-130-91-35 ~]# mkdir nginxproxy
```

``` shell
[root@133-130-91-35 ~]# cd nginxproxy
```

``` shell
[root@133-130-91-35 nginxproxy]# mkdir certs htpasswd vhost.d
```

``` shell
[root@133-130-91-35 nginxproxy]# vim compose.yaml
```

<details>
<summary><b>compose.yaml</b></summary>

``` yaml
services:
  nginx-proxy:
    image: nginxproxy/nginx-proxy
    container_name: nginxproxy-nginx-proxy
    volumes:
      - ./certs:/etc/nginx/certs
      - ./htpasswd:/etc/nginx/htpasswd
      - ./vhost.d:/etc/nginx/vhost.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
    ports:
      - 80:80
      - 443:443
    networks:
      nginxproxy-network:

networks:
  nginxproxy-network:
    name: "nginxproxy-network"
```

</details>

``` shell
[root@133-130-91-35 nginxproxy]# docker compose up -d
```

``` shell
[root@133-130-91-35 nginxproxy]# docker compose ps
NAME                     IMAGE                    COMMAND                                       SERVICE       CREATED              STATUS              PORTS
nginxproxy-nginx-proxy   nginxproxy/nginx-proxy   "/app/docker-entrypoint.sh forego start -r"   nginx-proxy   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp
```

# Docker Registryコンテナを立ち上げる

![adventcalendar.2023.5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/4cc6ce0c-2a2f-0270-e009-2357e3af1318.png)

DockerレジストリサービスはプライベートなDocker Hubのようなものです。
これに対して`docker push`や`docker pull`を行うことができます。

Docker HubやGitHub Packagesを使用することもできますが、制約が多いので自分用のレジストリを立ててしまった方が気が楽です。

とはいえ認証が全く行われない場合、攻撃を受ける可能性が大きくなるので、一応Basic認証を付けておきます。

compose.yamlにて指定している環境変数`VIRTUAL_HOST`, `VIRTUAL_PORT`は、Nginx Proxyが使用するためのものです。

``` shell
[root@133-130-91-35 ~]# mkdir registry.a10a.app
```

``` shell
[root@133-130-91-35 ~]# cd registry.a10a.app
```

``` shell
[root@133-130-91-35 registry.a10a.app]# mkdir registry
```

``` shell
[root@133-130-91-35 registry.a10a.app]# vim compose.yaml
```

<details>
<summary><b>compose.yaml</b></summary>

``` yaml
services:
  app:
    image: registry:2
    container_name: registry-a10a-app
    environment:
      VIRTUAL_HOST: registry.a10a.app
      VIRTUAL_PORT: 5000
    volumes:
      - ./registry:/var/lib/registry
    networks:
      nginxproxy-network:

networks:
  nginxproxy-network:
    name: "nginxproxy-network"
    external: true
```

</details>

``` shell
[root@133-130-91-35 registry.a10a.app]# docker compose up -d
```

``` shell
[root@133-130-91-35 registry.a10a.app]# docker compose ps
NAME                IMAGE        COMMAND                                            SERVICE   CREATED         STATUS         PORTS
registry-a10a-app   registry:2   "/entrypoint.sh /etc/docker/registry/config.yml"   app       4 seconds ago   Up 3 seconds   5000/tcp
```

Basic認証を設定します。

``` shell
[root@133-130-91-35 registry.a10a.app]# cd ~/nginxproxy/
```

``` shell
[root@133-130-91-35 nginxproxy]# cd htpasswd/
```

``` shell
[root@133-130-91-35 htpasswd]# yum install httpd-tools
```

``` shell
[root@133-130-91-35 htpasswd]# htpasswd -c registry.a10a.app a10a
New password:
Re-type new password:
Adding password for user a10a
```

``` shell
[root@133-130-91-35 htpasswd]# cat registry.a10a.app
a10a:$apr1$u5KO1fT0$vHYnN4aENEhEeN1qVKN8U.
```

`docker push`でプッシュするDockerイメージは結構サイズが大きいので、`413 Payload Too Large`エラーを避けるために設定を追加します。

``` shell
[root@133-130-91-35 htpasswd]# cd ../vhost.d/
```

``` shell
[root@133-130-91-35 vhost.d]# echo "client_max_body_size 512m;" >> registry.a10a.app
```

``` shell
[root@133-130-91-35 vhost.d]# cat registry.a10a.app
client_max_body_size 512m;
```

Nginx Proxyを再起動します。

``` shell
[root@133-130-91-35 vhost.d]# cd ..
```

``` shell
[root@133-130-91-35 nginxproxy]# docker compose restart
```

ローカルからDockerレジストリへリクエストを送信して、Basic認証が効いているか確認します。

``` shell
2023.adventcalendar.a10a.app % curl https://registry.a10a.app/v2/_catalog
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.25.2</center>
</body>
</html>
```

``` shell
2023.adventcalendar.a10a.app % echo -n a10a:password | base64 | xargs -I {} curl -H "Authorization: Basic {}" https://registry.a10a.app/v2/_catalog
{"repositories":[]}
```

# Docker Registryへアプリケーションイメージをプッシュする

![adventcalendar.2023.6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/ff179b8a-95ea-acd0-270e-f1d52de2957a.png)

先ほど立てたDockerレジストリへWebサーバのDockerイメージをプッシュします。

``` shell
2023.adventcalendar.a10a.app % docker build -t registry.a10a.app/2023.adventcalendar.a10a.app:latest . 
```

``` shell
2023.adventcalendar.a10a.app % docker login registry.a10a.app
Username: a10a
Password:
Login Succeeded
```

``` shell
2023.adventcalendar.a10a.app % docker push registry.arakaki.app/2023.adventcalendar.a10a.app:latest
```

``` shell
2023.adventcalendar.a10a.app % echo -n a10a:password | base64 | xargs -I {} curl -H "Authorization: Basic {}" https://registry.a10a.app/v2/_catalog
{"repositories":["2023.adventcalendar.a10a.app"]}
```

# プッシュしたイメージからコンテナサービスを起動する

![adventcalendar.2023.7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/08656ffe-cc3b-8aa8-1301-b07caf1bb8ce.png)

プッシュしたイメージからコンテナサービスを起動します

``` shell
[root@133-130-91-35 nginxproxy]# cd ~/
```

``` shell
[root@133-130-91-35 ~]# mkdir adventcalendar.a10a.app
```

``` shell
[root@133-130-91-35 ~]# cd adventcalendar.a10a.app
```

``` shell
[root@133-130-91-35 adventcalendar.a10a.app]# vim compose.yaml
```

<details>
<summary><b>compose.yaml</b></summary>

``` yaml
services:
  app:
    image: registry.a10a.app/2023.adventcalendar.a10a.app:latest
    container_name: adventcalendar-a10a-app
    environment:
      VIRTUAL_HOST: adventcalendar.a10a.app
      VIRTUAL_PORT: 3000
    networks:
      nginxproxy-network:

networks:
  nginxproxy-network:
    name: "nginxproxy-network"
    external: true
```

</details>

``` shell
[root@133-130-91-35 adventcalendar.a10a.app]# docker login registry.a10a.app
```

``` shell
[root@133-130-91-35 adventcalendar.a10a.app]# docker compose up -d
```

``` shell
[root@133-130-91-35 adventcalendar.a10a.app]# docker compose ps
NAME                           IMAGE                                                      COMMAND   SERVICE   CREATED          STATUS          PORTS
adventcalendar-a10a-app   registry.a10a.app/2023.adventcalendar.a10a.app:latest   "/app"    app       31 seconds ago   Up 30 seconds   3000/tcp
```

# クライアントからコンテナサービスを利用する

![adventcalendar.2023.8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/72a52248-c11d-ed67-01eb-ec97559b2ea5.png)

クライアントから接続確認を行います。
レスポンスのホスト名が`adventcalendar.a10a.app`に変わっています。

``` shell
2023.adventcalendar.a10a.app % curl https://adventcalendar.a10a.app
{"message":"hello from adventcalendar.a10a.app"}
```

# GitHub Actionsからデプロイを実行する

![adventcalendar.2023.9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/38d13e27-35f8-a8d3-407a-69b0ba6f4457.png)

VPSサーバでSSHキーを作成しておきます。

``` shell
[root@133-130-91-35 ~]# cd .ssh
```

``` shell
[root@133-130-91-35 .ssh]# ssh-keygen -t ed25519
```

``` shell
[root@133-130-91-35 .ssh]# cat id_ed25519.pub >> authorized_keys
```

GitHub Actionsの設定は.github/workflows/deploy.ymlへ書いていきます。

<details>
<summary><b>deploy.yml</b></summary>

``` yaml
name: Deploy

on:
  workflow_dispatch:

permissions: write-all

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: docker login
        run: |
          echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" \
           | docker login \
              --username ${{ secrets.DOCKER_REGISTRY_USERNAME }} \
              --password-stdin \
              ${{ secrets.DOCKER_REGISTRY_HOST }}

      - name: docker build
        run: |
          docker build \
              --tag ${{ secrets.DOCKER_REGISTRY_HOST }}/2023.adventcalendar.a10a.app:latest \
              .

      - name: docker push
        run: docker push ${{ secrets.DOCKER_REGISTRY_HOST }}/2023.adventcalendar.a10a.app:latest

      - name: setup ssh
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
          echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519
          chmod 700 ~/.ssh/id_ed25519

      - name: ssh deploy
        run: |
          ssh ${{ secrets.SSH_USER}}@${{ secrets.SSH_HOST }} ' \
              cd ~/2023.adventcalendar.a10a.app \
               && docker compose pull \
               && docker compose build \
               && docker compose up -d \
          '
```

</details>

deploy.ymlで参照するシークレットを設定します。

![スクリーンショット 2023-12-06 14.44.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/4600efdf-e9e3-3f37-b77d-079ea4c736ca.png)

設定するシークレットは以下です。

``` plain
+--------------------------+----------------------------------------+
| Name                     | Secret                                 | 
+--------------------------+----------------------------------------+
| DOCKER_REGISTRY_HOST     | registry.a10a.app                      | Docker Registryのホスト名
| DOCKER_REGISTRY_PASSWORD | password                               | Docker RegistryのBasic認証パスワード
| DOCKER_REGISTRY_USERNAME | a10a                                   | Docker RegistryのBasic認証ユーザー名
| SSH_HOST                 | 133.130.91.35                          | VPSサーバーIPアドレス
| SSH_KEY                  | -----BEGIN OPENSSH PRIVATE KEY-----... | SSHキー（~/.ssh/id_ed25519）の値
| SSH_USER                 | root                                   | SSHログインユーザー名
+--------------------------+----------------------------------------+
```

あとはGitHubのGUIからRun workflowのボタンをクリックすると、ビルドからデプロイ、再起動まで自動で行われます。

![スクリーンショット 2023-12-06 15.03.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/cc3be995-0343-3151-825b-ca8a5f4195e6.png)

# おわり

最初にNginx ProxyやらDocker Registryやらをセットアップするのが若干面倒ですが、
設定さえしてしまえばデプロイ作業がとても楽になります。

冒頭でも書きましたが、GitHub Actionsは実行タイミングを設定できるので、
mainブランチへコミットが発生したタイミングで自動デプロイや、毎日午前0時に自動デプロイなども可能です。

さらにGitHub Actionsで使用するシークレットは検証環境用と本番用に分けることができ、
アクション実行時にどの環境を使用するか選べたりします。

![スクリーンショット 2023-12-06 16.12.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/80fdf7a5-cdb6-bd69-faca-156d4f2eea56.png)

これもまたとても便利なので、調べて使ってみるともしかしたら幸せになれるかもしれません。

それではここまで読んでいただきありがとうございます。お疲れ様でした。