🐾

Docker で Remix チュートリアル環境を構築する

2023/11/13に公開

はじめに

フロントエンドは全然ド素人[1]だけどちょっとずつやっていこうと思う今日この頃。

試すにしても環境は汚したくない[2]ので、すべて Docker でやることにしているのだが、それはそれで面倒くさいので記事にしておく。

フロントエンドは React で作ろうと思ってフレームワークには Remix を選定[3]した。選定理由の参考にしたのは以下の記事である。

簡単に言えば、Next.js は特有の API を使用していて可搬性がない(他のフレームワークへの乗り換えがしにくい)らしく、Remix のほうが Web 標準やピュアな React に則っているらしいからである。勉強を進めてあとから Next.js を使いたくなったとき Remix → Next.js の乗り換えはおそらく知識を広げる方向で学べばよいが、その逆は学び直しになるのだろうと私の勘が告げているので Remix にした。

Quick Start

まずは Remix の公式クイックスタートガイドの部分を Docker でできるようにする。

https://remix.run/docs/en/main/start/quickstart

後々のことを考えて悩んだ結果、最初に取るディレクトリ構成は以下のようにした。

ディレクトリ構成
remix_devcon/
└ Makefile

つまりプロジェクトを作成するような Makefile を書く。

Makefile の中身解説

Makefile の変数の定義方法=, :=, ?= の3種類がある。

= は挙動がややこしいので以下の Makefile では :=?= しか使っていない。

:= は即時展開が行われ、右辺の値で左辺の変数を定義する。ここでは $(shell) を使ってシェルコマンドの実行結果を変数に代入している。id -un などはそのコマンドを実行しているユーザーの情報を取得するコマンドで、Docker コンテナの中でホストにおけるユーザーとまったく同じ Linux ユーザーを作成するために取得している。

?= は条件付き代入で、左辺の変数が未定義の場合に右辺の値でその変数を定義する。NODE_IMAGE, PROJECT_NAME は条件付き代入で定義することで外部から与えることが可能になっている。たとえば

$ PROJECT_NAME=myapp make create_env

のように実行すれば、変数 PROJECT_NAME の中身は myapp になり、単に

$ make create_env

と実行すれば変数 PROJECT_NAME の中身はデフォルト値の project になる。

その後、docker コマンドで -it は簡単に言えばコンテナのシェルとホストのシェルを繋ぐ指定であり、これを指定しないとシェルは一瞬で終了してしまう。--rm はコマンド実行終了後にコンテナを破棄するオプションである。

-p 3000:3000 はホスト側 3000 番ポートをコンテナ側 3000 番ポートにバインドしており、サーバーを起動してアクセスするにはこの指定が必要である。念のため、: の左がホスト側、右がコンテナ側である。

-v ${PWD}/${PROJECT_NAME}:/project は指定したプロジェクト名のディレクトリをコンテナ内の /project にマウントする指定である。${PWD} はしばしば sudo のときにも引き継がれるように別途設定する必要がある。

-v /project/node_modules はコンテナ内にある /project/node_modules ディレクトリをコンテナ内のディレクトリとして再マウントすることで、ホスト側ディレクトリのマウント対象から外す指定である。これをやっておくと node_modules にインストールされる各種モジュールがホスト側のディレクトリを汚染することがなくなる。

-w /project はコマンド実行時のカレントディレクトリ指定。-u root はコマンド実行ユーザーの指定。

groupadd -g ${DEVEL_GID} ${DEVEL_GROUP} \
&& useradd -g ${DEVEL_GID} -u ${DEVEL_UID} -m -s /bin/bash ${DEVEL_USER} \
&& chown -R ${DEVEL_USER}:${DEVEL_GROUP} /project/node_modules \
&& su ${DEVEL_USER}

のあたりはホスト側の Linux ユーザーと同じユーザーを作成し、root の所有でマウントされている /project/node_modules の所有をコマンド実行ユーザーに与え、最後に新たに作成した Linux ユーザーとしてシェルにログインする。

また、ベースイメージには最低限のエディタもインストールされていないので apt-get でインストールしている。

${PWD}sudo 時も引き継がれるようにするか、PWD=`pwd` make create_env のように明示的に与えてください。

Makefile
NODE_IMAGE ?= node:21-bookworm
PROJECT_NAME ?= project

DEVEL_USER  := $(shell id -un)
DEVEL_UID   := $(shell id -u)
DEVEL_GROUP := $(shell id -gn)
DEVEL_GID   := $(shell id -g)

create_env:
	mkdir -p ${PROJECT_NAME}
	sudo docker container run -it --rm \
		-p 3000:3000 \
		-v ${PWD}/${PROJECT_NAME}:/project \
		-v /project/node_modules \
		-w /project \
		-u root \
		${NODE_IMAGE} /bin/bash -c "\
		    groupadd -g ${DEVEL_GID} ${DEVEL_GROUP} \
		    && useradd -g ${DEVEL_GID} -u ${DEVEL_UID} -m -s /bin/bash ${DEVEL_USER} \
		    && chown -R ${DEVEL_USER}:${DEVEL_GROUP} /project/node_modules \
		    && apt-get update \
		    && apt-get install -y nano vim \
		    && su ${DEVEL_USER} \
		"

以下のコマンドを実行してみてほしい。

$ PROJECT_NAME=my-remix-app make create_env

すると Quick Start ガイドで言うところの

$ mkdir my-remix-app
$ cd my-remix-app

までが実行された状態でコンテナが立ち上がる。続きのコマンドを打ってみるとよい。注意点として、途中でコンテナ環境から exit してしまうと node_modules 内のデータが消えるような設定になっているので、チュートリアルは一気にやること。

$ npm init -y

# install runtime dependencies
$ npm i @remix-run/node @remix-run/react @remix-run/serve isbot react react-dom

# install dev dependencies
$ npm i -D @remix-run/dev
$ mkdir app
$ touch app/root.jsx
$ vim app/root.jsx
app/root.jsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
} from "@remix-run/react";

export default function App() {
  return (
    <html>
      <head>
        <link
          rel="icon"
          href="data:image/x-icon;base64,AA"
        />
        <Meta />
        <Links />
      </head>
      <body>
        <h1>Hello world!</h1>
        <Outlet />

        <Scripts />
      </body>
    </html>
  );
}

また、package.json の2行目に "type": "module", を追記する。

$ vim package.json
package.json
{
  "type": "module"
  // ...
}

ビルドして serve する。

$ npx remix build
 info  building... (NODE_ENV=production)
 info  built (640ms)
$ npx remix-serve build/index.js
[remix-serve] http://localhost:3000 (http://172.17.0.2:3000)

この時点で同じコンピュータからであれば localhost:3000 または外部のコンピュータからであれば [IPアドレス]:3000 をブラウザのアドレス欄に入力すると以下のように Hello world! が表示される。

ここまでの作業をしたあとでコンテナ環境を exit して remix_devcontree コマンドで見てみると以下のようになっている。

remix_devcon/
├── Makefile
└── my-remix-app
    ├── app
    │   └── root.jsx
    ├── build
    │   ├── index.js
    │   ├── metafile.js.json
    │   ├── metafile.server.json
    │   └── version.txt
    ├── node_modules
    ├── package.json
    ├── package-lock.json
    └── public
        └── build
            └── ...

クイックスタートにはまだ他の作業もあるのでお好みで続けるとよい。

Tutorial

Remix 公式にはクイックスタートのあとにチュートリアルがある。

https://remix.run/docs/en/main/start/tutorial

クイックスタートのほうで用意したディレクトリ構成や Makefile はチュートリアルでもそのまま使えるように工夫してある。

まだよく知らないが、おそらく Remix のプロジェクトは最初 npx create-remix@latest --template ... から始まるので、これを使えるようにしておけば今後の開発でも使えるだろうという推測のもと、そのコマンドが機能するように Makefile を作ってある。

$ PROJECT_NAME=remix_tutorial make create_env

あとはチュートリアル通りに進めるだけ。

$ npx create-remix@latest --template ryanflorence/remix-tutorial-template

既にプロジェクトのディレクトリの中にいるので、プロジェクト作成場所はカレントディレクトリ . を指定するとよい。git はインストールしていないので、git による初期化は No を選択しないとエラーになる[4]。Dependencies のインストールはどうせあとでやらざるを得ないので Yes でよい。

インストールが終わったら以下のコマンドでアプリを起動できる。

$ npm run dev

> dev
> remix dev


 💿  remix dev

 info  building...
 info  built (1.1s)
[remix-serve] http://localhost:3000 (http://172.17.0.2:3000)

ここまでの作業を行なってコンテナ環境から exit し、ディレクトリ構造を見てみると以下のようになっている。

remix_devcon/
├── Makefile
└── remix_tutorial
    ├── app
    │   ├── app.css
    │   ├── data.ts
    │   └── root.tsx
    ├── build
    │   ├── index.js
    │   ├── index.js.map
    │   ├── metafile.js.json
    │   ├── metafile.server.json
    │   └── version.txt
    ├── node_modules
    ├── package.json
    ├── package-lock.json
    ├── public
    │   ├── build
    │   │   └── ...
    │   └── favicon.ico
    ├── README.md
    ├── remix.config.js
    ├── remix.env.d.ts
    └── tsconfig.json

あとのことをダラダラとこの記事に書いても仕方がないので、あとは公式のチュートリアルを続けてください。

オマケ: 作ったアプリをデプロイするとき

公式の Deployment マニュアルには、自前のサーバーにデプロイするならそっちのドキュメントを読めと書いてある。

https://remix.run/docs/en/main/guides/deployment

いやそうは言うたかて、どのファイルをデプロイしたらいいかを聞きたいんじゃが。あとはこれしか書いていない。

After initializing an app, make sure to read the README.md

チュートリアルで生成された README.md を読むと以下のようなことが書いてある。

### DIY

If you're familiar with deploying node applications, the built-in Remix app server is production-ready.

Make sure to deploy the output of `remix build`

- `build/`
- `public/build/`

オッケー。状況によっては変わる可能性もあるかもしれないが、とりあえず build/public/build/ さえあればよいらしい。

あとは create したときの環境を再現するために package.jsonpackage-lock.json があればよい。

ディレクトリ構成
remix_devcon/
├ docker-compose.yml
├ Dockerfile
├ Makefile
└ project/
  ├ package.json
  ├ package-lock.json
  ├ build/
  │ └ ...
  ├ public/
  │ └ build/
  │   └ ...
  └ ...
Dockerfile
ARG NODE_IMAGE
FROM ${NODE_IMAGE}

COPY project/package.json project/package-lock.json /project/
COPY project/build/ /project/build/
COPY project/public/build/ /project/public/build/

WORKDIR /project
RUN npm ci
docker-compose.yml
version: '3.9'
services:
  remix:
    container_name: remix-server
    build:
      context: .
      args:
        NODE_IMAGE: "node:21-bookworm"
    ports:
      - "3000:3000"
    command: ["npx", "remix-serve", "build/index.js"]

NODE_IMAGEdocker-compose.yml で与えておけば、以下のコマンドがそのまま使えるので楽である。デプロイ用のときだけイメージサイズの小さい node:21-alpine などに変えることもできる。

$ sudo docker-compose build
$ sudo docker-compose up

一方で開発用とデプロイ用でイメージを揃えたい場合は Makefile と2ヶ所に書いてあるのは管理する上で気づかないうちに齟齬が発生する可能性があるので、.env などを活用して書いてある場所を1ヶ所に絞るか、あるいは Makefile

Makefile
.PHONY: build_image
build_image:
	sudo docker-compose build --build-arg NODE_IMAGE=${NODE_IMAGE}

のように --build-arg で明示的に与えてもよい(この方法は docker-compose を使用する際に make を経由しなければならないという制約を課してしまうのであまりよくない)。

まぁその辺りはお好みで。

おしまい

Enjoy your Hacking Life!

脚注
  1. 本業はデータサイエンスだがなまじっかプログラミング歴が長いせいで分析インフラの構築などをやらされることが多い。そしていよいよフロントエンドからも逃げられなさそう。 ↩︎

  2. 私の遊び場は ConoHa VPS基本的な環境構築は自動化されているとはいえ、汚すたびに立て直したりバックアップから復元するのは面倒くさい。 ↩︎

  3. 対抗は Next.js だった。 ↩︎

  4. Makefile の中の apt-getgit をインストールしてもよいが、config をホスト側と共有するにはひと手間かかるし、この記事の趣旨から外れてしまうのでこの記事ではやらない。 ↩︎

Discussion