🐳

Docker環境でNext.jsのSSR実装するとき気をつけること

2023/11/15に公開

Docker環境でNext.jsのSSRの実装をしていたら途中ハマったので、解決策を備忘録として残しておきます。
こんな人には役に立つかもしれません。

  • Dockerを使っており、フロントエンドとバックエンドでコンテナを分けている
  • Next.jsでSSRまたはSSGを実装する

私の開発環境は以下の通り。

  • フロントエンド:Next.js (v13.1.6)
  • バックエンド:Python, Django REST framework(v3.13.1), PostgreSQL(v14)

フロントエンドとバックエンドでそれぞれDockerコンテナを設けて開発しており、バックエンドではNginxを導入しています。

事の経緯

途中までSSR等使うことなく開発を進めていましたが、追加機能の実装に伴い必要に迫られたのでトップページでSSRを実行しました。
フロントエンドはローカルホスト3000番, バックエンドはlocalhost8000番で開発を進めており、下記のようなコードを加えたところ...

export async function getServerSideProps(context:any) {
  const { data } = await axios.get("http://localhost:8000/api/foo/");
  return {
    props: {
      data: data
    },
  };
}

localhostにアクセスできなくなり、500エラーを返すようになってしまった。
そのときのエラーメッセージとしては下記のようなものです。

Connect ECONNREFUSED 127.0.0.1:8000

色々調べた感じ原因は2つあり、二段階で解決する必要がありそう。

原因と解決策1

ということでまず1段階目。
Docker環境下でSSRする場合はホスト側ではなくコンテナ側のポートを指定する必要がありそう...というわけでそのようにAPIのコール先を書き換える。
例えばバックエンドでnginxを使っているなら docker-compose.yml には下記のように記述されているんじゃないかと思います。

version: "3.9"

services:
  app:
    ...(省略)
  
  db:
    ...(省略)
  
  nginx:
    ports:
      - 8000:80
    ...(省略)

このときnginxのservice名はnginx、ポート80でリッスンしているので、下記のようにします。

// Before 
const { data } = await axios.get("http://localhost:8000/api/foo/");
  
// After
const { data } = await axios.get("http://nginx:80/api/foo/");

これで1つ目のエラーが解決できました。
ローカルホストにアクセスすると先のエラーメッセージは出なくなります。
が、今度は次のようなエラーが出ます。

Error: getaddrinfo ENOTFOUND nginx

原因と解決策2

Error: getaddrinfo ENOTFOUND nginx

これはコンテナを複数使っていてかつ、ネットワークがそれぞれ異なっていると起きるようです。
というわけで、フロントエンドとバックエンドともに同じネットワークを使うようコンテナの設定を修正します。

まずバックエンド側の docker-compose.ymlnetworks に関する設定を追記します。

version: "3.9"

services:
  app:
    ...(省略)
    networks:
      - front
      - back

  db:
    ...(省略)
    networks:
      - back
  
  nginx:
    ...(省略)
    networks:
      - front

networks:
  front:
    external: false
  back:
    external: false

ここでは frontback という2つのネットワークをそれぞれ定義し、nginxは front 、dbは back 、appは frontback 両方のネットワークに接続することを意味します。
これを定義していおくことでコンテナ間の通信が可能になります。
最後の方に記述している external: false はこれらのネットワーク front , back が新規にここで定義されたものであることを明示しています。

もしすでに外部のコンテナで定義されたネットワークを使う場合はそのネットワーク名を記述し、 external: true とします。(これについては詳細を後述)

続いてフロントエンド側の docker-compose.yml にもnetworksに関する設定を追記します。

services:
  app:
    ...(省略)
    networks:
      - foo_front

networks:
  foo_front:
    external: true

まずこの foo_front を説明します(少々ややこしい)。
先程は front , back というネットワークを新規に定義しましたが、同一のネットワークを使う必要があるため今回は既存のネットワークを使うよう設定します。

というわけで、先程バックエンドのコンテナでnginxに設定したネットワーク名をこちらのnetworksにも設定します。
ではネットワーク名は front か?...と思いきや、ちがいます。
デフォルトではプロジェクト名(ディレクトリ名)がネットワーク名のプレフィックスとして使用されプロジェクト名_frontとなります。
というわけで、このバックエンドのディレクトリ名が例えば foo だった場合はfooがプレフィックスとなり、 foo_front となります。

なお、ネットワーク名はコマンド docker network ls で確認することもできます。

さらに、先程は新規でネットワークを定義したため docker-compose.yml の下部にnetworksの設定で external: false を定義しましたが、今回は先程もうすでに定義したネットワークを使うため external: true とします。

これにて解決。

※2023/11/17追記
一点付け加え忘れました!
Djangoとnginxを使っている人は今回の変更により、 setting.pyALLOWED_HOSTSnginx の追記が必要です。お忘れなきよう

Discussion