概要
CloudFlareにデプロイするためのRemixプロジェクトを作成する方法をまとめる。
なお、ローカル開発環境ではPrismaを用いてSQLiteを、CloudFlareでのデプロイ環境ではD1を用いる。
やること
- CloudFlareにデプロイするRemixプロジェクトを作成
- CloudFlareへデプロイできるようにする
- DBはCloudFlareのD1を使う
前提
- 筆者はMacのローカルのNode.jsのバージョン管理にvoltaを用いている。
- Macのローカルで
npm
コマンドが実行できること。 - CloudFlareのアカウントはすでに持っておりブラウザからログインする事ができること。
- CloudFlare上でD1のDBが作成されていること。
方法
Remixプロジェクトの作成
CloudFrontにデプロイするRemixのプロジェクトを作成する場合、作成時にCloudFlareのテンプレートを指定して作成したほうが良いです。
「一旦何もテンプレートは指定せずプロジェクト作ってあとからCloudFlare用の設定をすればいいや〜」はかなり大変です。
-
下記を実行して「todo-cloudflare」というRemixのプロジェクトを作成する。(作成中にCloudFlareへのログインを求められる事がある。)
npm create cloudflare@latest todo-cloudflare-d1 -- --framework=remix
-
プロジェクト作成中の分岐は下記の様に選択する。
- Initialize a new git repository?(ローカルGitリポジトリの初期化、および初期ファイルのコミット) → Yes
- Install dependencies with npm?(npmの初期化) → Yes
- Do you want to deploy your application?(今すぐデプロイするかどうか) → Yes (この前後でログインを求められる事がある。)
-
実行が完了するとデプロイされた画面がブラウザで開く。(おそらくURLのパスは同じプロジェクト名に設定しても異なる。)
-
下記を実行してプロジェクトのディレクトリ直下のNode.jsのバージョンを20.11.1に固定しておく。
cd todo-cloudflare-d1 volta install node@20.11.1 volta pin node@20.11.1
ローカルリポジトリのGitの設定
-
下記をそれぞれ実行してGitのコミッターの設定をしておく。
git config --local user.name '任意のコミッター名' git config --local user.email '任意のコミッターのメールアドレス'
-
もしデフォルトブランチがmasterのままならmainに変えておくことをおすすめする。参考記事を下記に記載する。
DBの設定
-
下記を実行してPrismaを用意する。
npm install prisma npm install @prisma/client npm install @prisma/adapter-d1 npx prisma init
-
prisma/schema.prismaを下記のように編集する。
prisma/schema.prisma// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client-js" previewFeatures = ["driverAdapters"] } datasource db { provider = "sqlite" url = env("DATABASE_URL") } // userテーブルの追加のために下記を追記 model users { id Int @id @default(autoincrement()) email String @unique name String }
-
プロジェクトルートに.envを用意し、下記のように記載する。(ローカル開発環境ではD1のローカル用のDBを使うため不要かも知れないが、設定されていない状態で
env("DATABASE_URL")
がどのような挙動になるか不安なので設定しておく。).envDATABASE_URL="file:./dev.db"
-
.envの例を作っておく。
cp .env .env.example
-
プロジェクトルート直下のwrangler.tomlを開きまずは下記の記載をコメントアウトする。
wrangler.toml# [[d1_databases]] # binding = "MY_DB" # database_name = "my-database" # database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
-
更に下記の様に記載する。
wrangler.toml[[d1_databases]] binding = "RemixのコードからDBにアクセスするときに使う任意の文字列" database_name = "CloudFlare上のD1のデータベース名" database_id = "CloudFlare上のD1のデータベースID"
-
下記を実行してマイグレーションのクエリを記載するファイルを作成する。
npx wrangler d1 migrations create wrangler.tomlに記載したdatabase_name create_users_table
-
下記を実行してマイグレーションのクエリを追記する。
npx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prisma --script --output migrations/0001_create_users_table.sql
-
下記を実行してローカル開発環境用D1にマイグレーションを実行する。
npx wrangler d1 migrations apply wrangler.tomlに記載したdatabase_name --local
-
実行すると下記のようになる。
$ npx wrangler d1 migrations apply todo-cloudflare-d1 --local ⛅️ wrangler 3.57.1 (update available 3.60.2) ------------------------------------------------------- Migrations to be applied: ┌─────────────────────────────┐ │ name │ ├─────────────────────────────┤ │ 0001_create_users_table.sql │ └─────────────────────────────┘ ✔ About to apply 1 migration(s) Your database may not be available to serve requests during the migration, continue? … yes 🌀 Executing on local database todo-cloudflare-d1 (4b6eada0-7ef7-4717-87ad-074b0fd04ccc) from .wrangler/state/v3/d1: 🌀 To execute on your remote database, add a --remote flag to your wrangler command. ┌─────────────────────────────┬────────┐ │ name │ status │ ├─────────────────────────────┼────────┤ │ 0001_create_users_table.sql │ ✅ │ └─────────────────────────────┴────────┘
-
下記を実行してCloudFlare上のD1にマイグレーションを実行する。
npx wrangler d1 migrations apply wrangler.tomlに記載したdatabase_name --remote
-
下記を実行してローカル開発環境用D1にテストデータを格納する。
npx wrangler d1 execute wrangler.tomlに記載したdatabase_name --command "INSERT INTO \"users\" (\"email\", \"name\") VALUES('hoge@example.com', 'Foo Bar (Local)');" --local
-
下記を実行してCloudFlare上のD1にテストデータを格納する。
npx wrangler d1 execute wrangler.tomlに記載したdatabase_name --command "INSERT INTO \"users\" (\"email\", \"name\") VALUES('hoge@example.com', 'Foo Bar (Remote)');" --remote
-
ローカル開発環境用D1にTablePlusなどでアクセスする場合、SQLiteの接続設定で
.wrangler/state/v3/d1/miniflare-D1DatabaseObject
ディレクトリ直下の.sqliteファイルを指定すれば開くことができる。 -
下記を実行してPrismaのクライアントを作成する。
npx prisma generate
-
下記を実行してwarangler.tomlから型ファイルを生成する。(実行するとプロジェクトルートにworker-configration.d.tsというファイルができる。)
npx wrangler types
-
下記を実行してディレクトリとファイルの作成を行う。
mkdir app/database touch app/database/client.ts
-
app/database/client.ts
に下記の内容を記載する。app/database/client.tsimport { PrismaClient } from '@prisma/client' import { PrismaD1 } from '@prisma/adapter-d1' export const connection = async (db: D1Database) => { const adapter = new PrismaD1(db) return new PrismaClient({ adapter }) }
-
プロジェクトルートのload-context.tsを下記のように編集する。
load-context.tsimport { type AppLoadContext } from '@remix-run/cloudflare' import { type PlatformProxy } from "wrangler"; import { connection } from './app/database/client' type Cloudflare = Omit<PlatformProxy<Env>, "dispose">; declare module "@remix-run/cloudflare" { interface AppLoadContext { cloudflare: Cloudflare; db: Awaited<ReturnType<typeof connection>>; } } type args = { request: Request, context: { cloudflare: Cloudflare } } type GetLoadContext = (args: args) => Promise<AppLoadContext> export const getLoadContext: GetLoadContext = async ({ context }) => { return { ...context, db: await connection( context.cloudflare.env.DB, // 末尾のDBはwrangler.tomlのbindingの値 ), } }
-
プロジェクトルートのvite.config.tsを下記のように編集する。
vite.config.tsimport { vitePlugin as remix, cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; import { getLoadContext } from "./load-context"; export default defineConfig({ plugins: [ remixCloudflareDevProxy({ getLoadContext }), remix({ future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, }, }), tsconfigPaths(), ], });
-
functions/[[path]].ts
を開き下記のように編集する。functions/[[path]].tsimport { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - the server build file is generated by `remix vite:build` // eslint-disable-next-line import/no-unresolved import * as build from "../build/server"; import { getLoadContext } from "../load-context"; export const onRequest = createPagesFunctionHandler({ build, getLoadContext });
-
npm run dev
を実行して下記のような画面が現段階で問題なく開くことを確認する。 -
app/routes
直下にloader.tsを作成し、下記のように記載する。app/routes/loader.tsimport type { LoaderFunctionArgs} from "@remix-run/cloudflare"; export const loader = async ({ context }: LoaderFunctionArgs) => { try { return await context.db.users.findMany(); } catch (error) { console.error("Failed to load users:", error); throw new Response("Internal Server Error", { status: 500 }); } }
-
app/routes/_index.tsx
を開き下記のように修正する。app/routes/_index.tsximport type { MetaFunction } from "@remix-run/cloudflare"; import { loader } from "./loader"; export { loader }; import { useLoaderData } from "@remix-run/react"; export const meta: MetaFunction = () => { return [ { title: "New Remix App" }, { name: "description", content: "Welcome to Remix on Cloudflare!", }, ]; }; interface User { id: string; name: string; email: string; } export default function Index() { const users = useLoaderData<User[]>(); return ( <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}> <h1>Welcome to Remix on Cloudflare</h1> <ul> <li> <a target="_blank" href="https://remix.run/docs" rel="noreferrer"> Remix Docs </a> </li> <li> <a target="_blank" href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-remix-site/" rel="noreferrer" > Cloudflare Pages Docs - Remix guide </a> </li> </ul> <div> <h2>Users</h2> <ul> {users.map((user: User) => ( <li key={user.id}> <div>ID: {user.id}</div> <div>NAME: {user.name}</div> <div>EMAIL: {user.email}</div> </li> ))} </ul> </div> </div> ); }
-
下記のようにローカルの情報が表示されれば問題ない。
-
また、下記のようにすることでjsonを画面に表示する事ができる。
app/routes/_index.tsximport type { MetaFunction } from "@remix-run/cloudflare"; import { loader } from "./loader"; export { loader }; import { useLoaderData } from "@remix-run/react"; export const meta: MetaFunction = () => { return [ { title: "New Remix App" }, { name: "description", content: "Welcome to Remix on Cloudflare!", }, ]; }; interface User { id: string; name: string; email: string; } export default function Index() { const users = useLoaderData<User[]>(); return ( <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}> <h1>Welcome to Remix on Cloudflare</h1> <ul> <li> <a target="_blank" href="https://remix.run/docs" rel="noreferrer"> Remix Docs </a> </li> <li> <a target="_blank" href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-remix-site/" rel="noreferrer" > Cloudflare Pages Docs - Remix guide </a> </li> </ul> <div> <pre>{JSON.stringify(users, null, 2)}</pre> </div> <div> <h2>Users</h2> <ul> {users.map((user: User) => ( <li key={user.id}> <div>ID: {user.id}</div> <div>NAME: {user.name}</div> <div>EMAIL: {user.email}</div> </li> ))} </ul> </div> </div> ); }
-
一旦コミットしておく。(この次
npm run deploy
を実行するがこれ自体はコミットされていようがいまいが関係なく今のローカル環境のコードをビルドしてデプロイする。) -
下記を実行してデプロイを行う。(フリープランだと容量が大きいのでデプロイできない可能性がある。CloudFlare上で確認しなくて良いならこれはやらなくていい。)
npm run deploy
-
上記コマンドをCloudFlareに5ドル課金してでもデプロイを確認したい方はこちら → Remix CloudFlareにデプロイを行ったら容量系のエラーが出た
-
CloudFlareでデプロイされているURLにアクセスし、トップ画面を確認し「下記を実行してCloudFlare上のD1にテストデータを格納する。」の手順で格納した(Remote)がついているユーザーが表示されていることを確認する。
自分用メモ
-
CloudFlare CLIツールログイン
npx wrangler login
-
CloudFlare CLIツールログアウト
npx wrangler logout
参考文献