Docker上にNext.js+nginxサーバーを構築し、httpsでアクセスできるようにする

# はじめに
## 注意事項
- Docker, Next.js, nginxは1ヶ月触った程度の初学者
- あくまで開発中のテスト用の構築
## 環境
- OS:Ubuntu 20.04.6
- Docker: 26.1.3
- Docker Compose: 2.27.0
- node: 21.7.1
# 経緯
ブラウザ上でマイクやカメラといった機能を使用したい場合、**https通信でアクセス**しなければいけません。

`localhost`で試している分にはブラウザがよしなに対応してくれたり、Next.jsの`--experimental-https`オプションがあるためそこまで困りませんが、スマートフォン上のブラウザから試してみたい場合や、実ネットワークを介して試してみたい場合などはhttps通信でのアクセスが必須です。

そこで、nginxをリバースプロキシとしてNext.jsアプリケーションを配信しつつ、nginxへのアクセスを**自己署名証明書**を用いたhttps通信に対応させることで、なんちゃってhttps通信を実現します。

実際のWebサイトとして公開する際は当然**Let’s Encryptのような認証局を通した証明書を用いる**必要がありますが、開発環境として試す際は上記の方がお手軽に試せると思います。
# 実践編
## 準備
### ディレクトリ構造
今回は以下のようなディレクトリ構造で試します。
```
- root
  - app
    - /* create-next-appで作成した各種ファイルとフォルダ */
    - Dockerfile
  - nginx
    - Dockerfile
    - nginx.conf
  - compose.yaml
```
appフォルダは`create-next-app`コマンドで自動的に作成されるため、nginxフォルダだけ作成しておきます。
### 各種インストール
詳細なインストール方法は割愛しますが、以下をインストールしておきます。
nodeは`create-next-app`を実行するためにnpxを使用します。
(もしnpxが入っていない場合は` npm install -g npx
`でインストールできると思います。)
- node
- dockerとdocker compose
## 実装
### Next.js
今回はとりあえず`create-next-app`で作成されるサンプルアプリを使用します。
```shell
$ npx create-next-app
✔ What is your project named? … app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No
```
Next.jsのプロジェクト名をapp、その他のオプションはとりあえずそのままに作成しました。

### Docker
上記の`create-next-app`で作成したappフォルダ、およびnginxフォルダそれぞれに`Dockerfile`を、プロジェクトのルートディレクトリに`compose.yaml`ファイルを作成していきます。
```Dockerfile:Dockerfile(Next.js)
FROM node:latest

WORKDIR /app
```
Next.js側の`Dockerfile`はシンプルにnodeイメージをベースに作成しておきます。WORKDIRで後ほどバインドマウントする`/app`フォルダを指定しておきます。

```Dockerfile:Dockerfile(nginx)
FROM nginx:latest

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y openssl sudo

RUN mkdir -p /etc/nginx/ssl
RUN openssl genrsa 2048 > /etc/nginx/ssl/nginx.key && \
    openssl req -subj '/CN=/O=/C=' -new -key /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.csr && \
    openssl x509 -req -days 365 -in /etc/nginx/ssl/nginx.csr -signkey /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt 

COPY ./nginx.conf /etc/nginx/nginx.conf

CMD ["nginx", "-g", "daemon off;"]
```
nginx側の`Dockerfile`ではnginxイメージをベースに、httpsのために必要となる証明書を`openssl`で作成しています。
また、同じフォルダに作成する`nginx.conf`(後述)をDocker上の`/etc/nginx/nginx.conf`にコピーします。

```yaml:compose.yaml
services:
  app:
    container_name: sample_next
    build:
      context: ./app
      dockerfile: Dockerfile
    tty: true
    volumes:
      - ./app:/app
    environment:
      - WATCHPACK_POLLING=true
    ports:
      - "3000:3000"
    command: sh -c "npm install && npm run dev"

  nginx:
    container_name: sample_nginx
    build:
      context: ./nginx
      dockerfile: Dockerfile
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - app
```
最後にこれら二つのDockerを起動するための`compose.yaml`ファイルを作成します。

Next.jsのDockerイメージでは`create-next-app`で作成した`app`フォルダをそのままDocker上の`/app`フォルダとして使用できるようにバインドマウントしています。
Next.jsで使用するポート番号を`package.json`などから変更している場合は適宜書き換えてください。

nginxのDockerイメージではhttp通信で使用される80番ポートとhttps通信で使用される443番ポートを記載しておきます。今回、http通信でアクセスされた際はhttps通信を使用するようにリダイレクトさせたいと思います。

### nginx
```conf:nginx.conf
events {
    worker_connections 1024;
}

http {
    server {
        # http(80番ポート)はhttpsへリダイレクト
        listen 80;
        listen [::]:80;

        return 301 https://$host$request_uri;
    }

    server {
        # httpsで使用される443番ポートで待つ
        listen       443 ssl;
        listen       [::]:443 ssl;
        http2 on;

        # 作成した証明書のファイルを指定
        ssl_certificate /etc/nginx/ssl/nginx.crt;
        ssl_certificate_key /etc/nginx/ssl/nginx.key;

        # Next.jsへ接続
        location / {
            proxy_pass http://sample_next:3000;
        }
    }
}
```
nginxフォルダの`nginx.conf`では、以下の二点を記載しています。
1. http(80番ポート)でのアクセスは`https://$host$request_uri`へリダイレクト(301)
1. https(443番ポート)でのアクセスは`proxy_pass`を使用して`http://sample_next:3000`へ転送
    - **sample_next**は`compose.yaml`で指定したNext.jsのコンテナ名
    - `ssl_certificate`および`ssl_certificate_key`にはnginx側のDockerfileで作成した証明書のファイルの場所を指定

<details><summary>余談</summary>
nginxが自動で作成したnginx.confに443番ポート周りの設定をしたserverを追記すればいいだろ～と思っていたら、以下の行があるせいでリダイレクトしてくれない、という状況にハマってかなりの時間を浪費してしまいました。

```
include /etc/nginx/conf.d/*.conf;
```
お気を付けください。
</details>


## 動作確認
Dockerfileをビルドし、起動します。
```sh
$ docker compose build
$ docker compose up -d
```
Dockerを起動したマシンのIPアドレスをブラウザのURL欄に直接打ち込んでみます。

:::note warn
以下ではマシンのIPアドレスを`127.0.0.1`に置き換えてあります。
適宜自分の環境と読み替えてください。
:::

すると自動的に`https://127.0.0.1`にリダイレクトされるはずです。


リダイレクトされた後、初回は以下のような警告がブラウザによって表示されます。(IPアドレス部分は加工しています)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/fb9f46d4-c15f-a2f6-3014-df92900568f1.png)

このページの作者である自分が悪意のあるユーザーではないという方は(もちろん全員がそうだと思いますが)、詳細設定から **127.0.0.1にアクセスする**をクリックしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/f8192cc8-fa49-0f86-2d0b-4e88f105bb17.png)
上記のようなNext.jsのサンプルページが表示されればOKです。これでなんちゃってhttps通信でNext.jsアプリケーションへアクセスできるようになりました。

ブラウザ上では**セキュリティ保護なし**と表示されてしまいますが、マイクやカメラなどの機能へはアクセスすることができます。


# おわりに
nginxを使用することで簡単に自己署名証明書を使用したhttps通信の実現と、Next.jsアプリケーションへの転送を実現することができました。

Let's Encryptなどの認証局の登場によってhttps通信が簡単に実現しやすくなったことは良いことだと思いますが、ブラウザ側もhttp通信を排除しようという動きが進んで、サクッと開発したい時に対応することが増えていくのは難しい点ですね。

# 参考
- [Next.js + NginxサーバーをDockerで構築した話](https://qiita.com/piny940/items/602316d913938db01c81)
- [Docker + ec2 + nginx + Next.jsでの環境構築してWebアプリを公開する](https://qiita.com/Kento210/items/cbf18fd1fa810a4379f2)
- [【Nginx】nginxのproxy設定でNext.jsを配信する](https://zenn.dev/soramarjr/articles/601d15db1b75bf)
- [NGINXでのSSL(HTTPS)設定について](https://cn.teldevice.co.jp/blog/p39750/)