個人開発サービスを自宅サーバーでデプロイ（docker-compose＋cloudflare tunnel）

UTokyo AdventCalendarの24日目です
12月24日、クリスマスイブですね
街を歩くと幸せそうな人が多く微笑ましいです、はい
　　

ハッカソンや大学の実験で開発したbackendサービスを自宅のサーバーからデプロイしたのでその方法を紹介します。

# TL;DR
- 自宅のUbuntu Serverを使ってHTTPSからのアクセス可能なbackendサーバーをデプロイしたよ
- fastapi, nginx, cloudflare tunnelの3つのコンテナをdocker-composeで接続したよ
- cloudflare tunnelは無料で自宅のbackendをデプロイできて便利だよ

今回デプロイしたサービスはこれだよ。よければ遊んでみてね！

https://netrend.vercel.app/overview.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927259/46b0a5dc-1e97-1712-6723-b6420fcc14c7.png)


# 事前準備
- Ubuntu Server等linuxが使えるPC（ミニPCを使用）
- 独自ドメインの取得（Xserverで取得）
- Dockerの基本的な知識
※これらの知識がない方も導入できるよう準備手順の章を下記に用意していますので、参考になれば幸いです。

# 背景
個人開発でサービスを作った場合、やっぱりサービスをデプロイして一般に公開したいですよね。僕はしたいです。

Webサービスを開発した場合、構成と無料でのデプロイは以下のようになる場合が多いのではないでしょうか。
- frontend: Web画面の機能
    [Github Pages](https://docs.github.com/ja/pages/getting-started-with-github-pages/about-github-pages)、[Netlify](https://www.netlify.com/)、[Vercel](https://vercel.com/)、[Cloudflare Pages](https://pages.cloudflare.com/)
- backend: Webサーバー（APIサーバー）
    [Railway](https://render.com/docs/deploy-rails)、[Heroku](https://jp.heroku.com/)、[Google Cloud Platform](https://cloud.google.com/deploy/docs/deploying-application?hl=ja)、[Amazon Web Service](https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/deploy-backend.html)

これは個人的な感覚ですが、frontendは充分な無料枠があり簡単にデプロイできるサービスが多い一方、backendは無料枠では月当たりのアクセス数、スリープ時間、などの制約が多く、デプロイの手順も複雑なものが多いです。

特に、スリープ時間についてはかなり問題になると感じています。個人開発で公開するサービスはそれほど頻繁にアクセスされるものではありません。そのため、数時間ぶりにアクセスされた場合、サーバーが起動するまでに数秒かかってしまいます。（スリープ条件を解除する方法などもあるのかもしれませんが、そこまでは調べておらず、本記事では言及しません）

これらの問題を解決する方法、それが「自宅サーバー」です。

# 事前準備のための章
事前準備のお助け的な章です。既に事前準備が完了している方は読み飛ばしていただいて構いません。

## 自宅サーバー編
「そんなこと言われても自宅サーバーなんてねぇよ！」という方も多いと思います。
しかし、昔使っていたPCなどがある場合、そのPCにUbuntu Serverをインストールすることで簡単に自宅サーバーが完成します。
以下のサイトを参考にしてインストールしました。

https://qiita.com/Shiori-Ikezoe/items/c059d79bfe07c90abfea

## 独自ドメイン取得編
「そんなこと言われても独自ドメインなんて持ってねえよ！」という方も多いと思います。
しかし、思っているよりも簡単に、年間1600円程度で使えたりします。
今後自分のサイトなどを持ちたい、など思っている方は取得しておいて損はないと思います。
有名なサイトとしては、[Xserver](https://www.xserver.ne.jp/), [お名前ドットコム](https://www.onamae.com/), [さくらのドメイン](https://domain.sakura.ad.jp/?gad_source=1&gclid=Cj0KCQiAsaS7BhDPARIsAAX5cSDWCVrMA96Pg_fcRKKfij6b9OBjLDUAOruZwYfoAOECYniVQ6kkMZ4aAmWZEALw_wcB)などがあります。
私は継続的な料金が最も安いXserverでドメインを取得しました。
Xserverのドメイン取得手順は以下です。

https://www.xdomain.ne.jp/manual/man_order_domain.php

## Dockerの基本知識編
「そんなこと言われてもDockerの知識なんてねぇよ！」という方も多いと思います。
しかし、以下のサイトを読むだけでDockerの基本知識が身に付きます。
とても分かりやすかったです。

https://zenn.dev/suzuki_hoge/books/2022-03-docker-practice-8ae36c33424b59


# システム構成
前置きが長くなりました。ここから実装の詳細について説明します。

今回デプロイしたbackendサービスは以下の3つで構成されます
- **backendサーバー（fastapi）**: webサイトからのアクセスに対応するAPIサーバー
- **nginxサーバー（nginx）**: HTTPSからのアクセスに対応
- **cloudflared tunnelサーバー（cloudflare tunnel）**: backendサービスをトンネリングを使用して家からインターネット上に公開

これら3つのサーバーをDockerでコンテナにし、docker-composeで接続しました。
複数のコンテナからなるサービスをデプロイする場合、大規模なものの場合はkubenetes(k8s)などを使う必要があり結構大変なのですが、今回のような小中規模のものであればdocker-composeで充分可能です。

自宅サーバーからサービスを外部に公開するには、固定IPなどを取得する必要があるのですが、固定IPは利用料金が非常に高いのが難点です。そんな時に便利なのが、トンネリングサービスです。

トンネリングサービスは、ローカル環境で動作しているアプリケーションやサービス（例: Webサーバー、APIサーバー）をインターネット経由で外部公開する技術です。
開発段階ではngrokというサービスが非常に便利なのですが、このサービスは
- 一度起動したトンネリングの継続時間が24時間と短い
- デプロイ用で持続的に使うには課金が必要

という特徴があります。
そこで、デプロイのような継続的なトンネリングできるサービスとして、Cloudflare tunnelを使用しました。
Cloudflare tunnelは、自分で取得したドメインを使って、自宅PCのポートをそのドメインへ接続（外部からアクセスできるように）することができます。
なぜこれほど便利なサービスが無料なのか、と怪しくも思ったのですが、Cloudflare社の社会貢献的な方針だそうです。

https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927259/45b18187-ce8e-5022-b4d5-b09516e4d2e0.png)


# 設定手順
自宅サーバーからbackendを公開するための具体的な手順です
## 1. Cloudflareに登録し、自分で取得したドメインを登録
- 下記サイトの「手順」1~3にしたがって、取得したドメインとDNS情報を登録します
- 下記のサイトではトンネリングの設定もCloudflareページ内で行っていましたが、今回はPCのCLIからトンネリングを行うので「手順」4以降は行わなくて大丈夫です
- また、下の写真のように、DNS RecordsのAdd Recordにて今回使用するネームサーバー（アクセスされるアドレス）を設定します(ex. fuga.hoge.com)
- Cloudflareでは1階層下までのサブドメインは設定可能なので、1つのドメインで複数のRecordを登録できます（この写真の場合、2つのRecordを登録しています。Nameの箇所にサブドメイン名が入ります）

https://zenn.dev/takajun/articles/fbd783e459c722#%E6%89%8B%E9%A0%86

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927259/431f8289-e299-efc4-a406-5d2e34f03a59.png)


## 2．自宅サーバーにCloudflare CLIをインストール
Cloudflareのトンネル設定に必要なCLIツール`cloudflared`をインストールします
```
# Cloudflareの公式リポジトリを使ってインストール
$ curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
$ sudo mv cloudflared /usr/local/bin/
$ sudo chmod +x /usr/local/bin/cloudflared
```

## 3．Cloudflare Tunnelの作成
以下のコマンドでトンネルを作成し、ネームサーバー(ex. fuga.hoge.com)を指定します
```
$ cloudflared tunnel login
```
このコマンドを実行すると、ブラウザでCloudflareにログインするよう求められ、ログイン後、トンネルの作成が許可されます

その後、以下のコマンドでトンネルを作成します
```
$ cloudflared tunnel create <トンネル名>
```
これにより、UUID.jsonという設定ファイルが作成されます
UUIDがトンネリングIDとなっており、これを控えておきます（"tunnel_id"として、後に使用します）

## 4．docker-compose.yml、Dockerfile、nginx.conf、cloudflared/config.ymlを作成
backendサーバーとcloudflareサーバーは別にDockerfile（後述）を用意し、nginxサーバーはnginx imageを使用しました
PCのポート8000を使って外部に公開します

- docker-compose.yml
    ```
    services:
      backend:
        build:
          context: .
          dockerfile: Dockerfile.backend
        container_name: backend
        working_dir: /backend
        volumes:
          - ./app:/backend/app
          - ./data:/backend/data
        ports:
          - "8000:8000"
        command: poetry run uvicorn app.main:server --host 0.0.0.0 --port 8000 --forwarded-allow-ips="*"
        networks:
          - cloudflared_network
        depends_on:
          - cloudflared
    
    
      nginx:
        image: nginx:latest
        container_name: nginx
        ports:
          - "8080:8000"
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf:ro
        depends_on:
          - backend
        networks:
          - cloudflared_network
    
      cloudflared:
        build:
          context: .
          dockerfile: Dockerfile.cloudflared
        container_name: cloudflared
        networks:
          - cloudflared_network
    
    networks:
      cloudflared_network:
        driver: bridge
    ```
- Dockerfile.backend
    ```
    # ベースイメージ
    FROM python:3.10-slim
    
    # Poetryインストール
    RUN curl -sSL https://install.python-poetry.org | python3 -
    ENV PATH="/root/.local/bin:$PATH"
    
    # 作業ディレクトリ設定
    WORKDIR /backend
    
    # Poetry のインストール
    RUN pip install --no-cache-dir poetry
    
    # pyproject.tomlとpoetry.lockをコピーし、依存関係をインストール
    COPY pyproject.toml poetry.lock ./
    
    # 依存関係インストール（仮想環境を無効化）
    RUN poetry config virtualenvs.create true && poetry install --no-root --no-interaction --no-ansi
    
    COPY ./app /backend/app
    COPY ./data /backend/data
    ```
    - backendサーバーのためのDockerfileです
    - python仮想環境にはpoetryを使用しました。venvやcondaを使っている場合は、docker-compose.ymlのbackendサーバー起動コマンド、Dockerfileのpoetry installの箇所を適宜変更してください
- Dockerfile.cloudflared
    ```
    FROM cloudflare/cloudflared:latest
    
    COPY ./cloudflared/ /etc/cloudflared/
    
    CMD ["tunnel", "--config", "/etc/cloudflared/config.yml", "run"]
    ```
    - cloudflare tunnelingのためのDockerfileです
    - cloudflareはDockerコンテナにしないでPCに直接インストールすることでも使えますが、その場合はdockerコンテナのネットワークとの接続がややこしくなるのでdockerコンテナとして作成し他のコンテナと同一ネットワークで使用するのがおすすめです

- nginx.conf
    ```
    events {}
    
    http {
        server {
            listen 8000;  # 外部アクセス用のポート
    
            location / {
                proxy_pass http://backend:8000;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
            }
        }
    }
    ```
    - nginxサーバーを使わないと、HTTPS（VercelなどでデプロイしたWebサイト）からアクセスできません
    - nginxにより、外部からHTTPSでアクセスされたものをDocker内ネットワークのbackendサーバーのポート8000へのアクセスに切り替え、ということをしています
    - これにより、HTTPSアクセスされたものをHTTPでのアクセスに切り替える（backendサーバーではHTTPSアクセスを考慮しなくても済む）ようにできます
- cloudflared/config.yml
    ```
    tunnel: "tunnel_id"(上述)
    credentials-file: /etc/cloudflared/"tunnel_id".json(上述)
    
    
    ingress:
      - hostname: DNS Recordsにて設定したネームサーバー名(ex. fuga.hogehoge.com)
        service: http://nginx:8000
        originRequest:
          noTLSVerify: true
      - service: http_status:404
    ```
    - トンネリングのための設定ファイルです
    - 外部からhostnameのアドレスにアクセスされたとき、serviceで指定したアドレス（nginxのport 8000）にアクセスを切り替えます
    - ここで設定する`http://nginx:8000`のポート番号(8000)は、nginx.confでlistenするポート番号(8000)と同一にする必要があります
    - "tunnel_id"は、「設定手順3：Cloudflare Tunnelの作成」にてトンネリングを作成したときに生成されたUUIDです。これを"tunnel_id"の箇所に書き込みます

これらの設定により、cloudflared/config.ymlファイル内で指定したhostnameでのHTTPSアクセスは
1．cloudflared/config.ymlのservice(`http://nginx:8000`)にアクセスが切り替わる
2．nginx.confで設定したlocation / {}のproxy_pass(`http://backend:8000`)にアクセスが切り替わる
3．backendサーバーへアクセスされ、作成したAPIが実行される
という手順がなされ、処理が行われるようになります
    

これまでに説明したファイルを以下のディレクトリ構成で配置します。
```
.
├── Dockerfile.backend
├── Dockerfile.cloudflared
├── app
│   └── main.py
├── cloudflared
│   └── config.yml
├── data
│   └── backendサーバーで使うファイル
├── docker-compose.yml
├── nginx.conf
├── poetry.lock
└── pyproject.toml
```

【補足】main.pyはfastapiのAPIを定義したファイルです。もっとも簡単な構成は以下です
- main.py
    ```
    from fastapi import FastAPI
    from fastapi.middleware.cors import CORSMiddleware  # CORSミドルウェアのインポート
    
    server = FastAPI()
    
    server.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],  # 必要なら特定のオリジンのみ許可
        allow_credentials=True,
        allow_methods=["*"],  # すべてのHTTPメソッドを許可
        allow_headers=["*"],  # すべてのヘッダーを許可
    )
    
    
    @server.get("/")
    def read_root():
        return {"message": "Netrend FastAPI server is running!"}

    ```
    - middlewareを設定しないと、Webアクセス時にCORSエラーが起きてしまうので、必ずmiddlewareを設定する必要があります

## 5．cloudflare関連の細かな設定
生成された"tunnel_id".jsonをcloudflared/にコピー
```
$ cp ~/.cloudflared/"tunnel_id".json ./cloudflared/
```
これにより，cloudflared/は以下のようになります
```
$ ls cloudflared
config.json "tunnel_id".json
```

先ほど作成したcloudflared/config.ymlとコピーした"tunnel_id".jsonに読み取り権限を付ける
```
$ chmod 644 cloudflared/config.yml
$ chmod 644 cloudflared/"tunnel_id".json
```

作成したトンネルにドメインとDNSを設定（【重要】これをしないとアクセスできない）
"tunnel_id"にはトンネリングのUUIDを、"サブドメイン名"には使用したいサブドメイン名を付けてください（例. subdomain.mydomain.comの場合は「subdomain」）
このサブドメイン名がcloudflareのDNSのページにて「Name」のれｔに表示されます

```
# ↓ 「""」はいらない
$ cloudflared tunnel route dns "tunnel_id" "サブドメイン名"
```

## 6．デプロイ
```
# Dockerのビルド
$ docker-compose build --no-cache

# Docker起動
$ docker-compose up -d
```
これによってトンネリングがなされ、外部からネームサーバーにアクセスするとbackendサーバーで設定したAPIが呼び出されるはずです

# 終わりに
自宅サーバーでのデプロイもそこそこ大変なのですが、今後数十年間（cloudflare tunnelがサービスを提供してくれている間は…）使い続けられると思うとかなり便利だと思います
DNS Recordsに登録するネームサーバーを別のサブドメインに変更するだけで複数のサービスをデプロイできるのも嬉しいです