在 Railway 上使用 Node.js 和 PostgreSQL 搭配 Drizzle

This tutorial demonstrates how to use Drizzle ORM with Node.js and a PostgreSQL database, all deployed on Railway.

本指南假定您已熟悉:
  • You should have Node.js installed. You can install it by following the official guide.

  • You should have installed Drizzle ORM and Drizzle kit. You can do this by running the following command:

npm
yarn
pnpm
bun
npm i drizzle-orm
npm i -D drizzle-kit
  • You should have installed the pg package as the PostgreSQL driver and tsx to run TypeScript files directly.
npm
yarn
pnpm
bun
npm i pg
npm i -D @types/pg tsx
  • You should have a Railway account.

设置 Railway 和 Drizzle ORM

创建 Railway 项目

登录你的 Railway 仪表盘,然后点击 New Project 按钮。

创建 PostgreSQL 数据库

在项目画布上,点击右上角的 New 按钮,然后选择 DatabasePostgreSQL。你也可以使用命令面板(Cmd/Ctrl + K)并搜索 PostgreSQL。Railway 会为你创建一个新的 PostgreSQL 实例。

获取连接字符串

点击项目中的 PostgreSQL 服务,进入 Variables 选项卡,找到 DATABASE_PUBLIC_URL 变量。复制该值——它应该类似于这样:

postgresql://postgres:password@region.railway.app:port/railway

IMPORTANT

Railway 提供两种类型的数据库连接 URL:

  • Public URL — 可从任何地方访问(你的本地机器、外部服务)。使用 TCP 代理,看起来像 postgresql://postgres:password@region.railway.app:port/railway
  • Private URL — 只能通过内部网络从同一 Railway 项目中的服务访问。使用 *.railway.internal 主机名。

对于本地开发(例如运行 drizzle-kit pushdrizzle-kit studio),你必须使用公共 URL。私有的 *.railway.internal 主机名无法从你的本地机器解析。

设置连接字符串变量

在项目根目录创建一个 .env 文件,并添加 DATABASE_URL 环境变量。使用 Railway 提供的公共连接字符串:

DATABASE_URL=postgresql://postgres:password@region.railway.app:port/railway

这个 .env 文件仅用于本地开发。部署到 Railway 时,你 将在 Railway 仪表盘中通过服务引用变量单独配置 DATABASE_URL 环境变量。

将 Drizzle ORM 连接到数据库

创建一个 db.ts 文件并设置数据库配置:

src/db.ts
import { drizzle } from "drizzle-orm/node-postgres";

export const db = drizzle(process.env.DATABASE_URL!);

创建表

创建一个 schema.ts 文件并声明你的表:

src/schema.ts
import * as p from "drizzle-orm/pg-core";

export const usersTable = p.pgTable("users", {
  id: p.serial().primaryKey(),
  name: p.text().notNull(),
  age: p.integer().notNull(),
  email: p.text().notNull().unique(),
});

export const postsTable = p.pgTable("posts", {
  id: p.serial().primaryKey(),
  title: p.text().notNull(),
  content: p.text().notNull(),
  userId: p
    .integer()
    .notNull()
    .references(() => usersTable.id, { onDelete: "cascade" }),
  createdAt: p.timestamp().notNull().defaultNow(),
  updatedAt: p
    .timestamp()
    .notNull()
    .$onUpdate(() => new Date()),
});

export type InsertUser = typeof usersTable.$inferInsert;
export type SelectUser = typeof usersTable.$inferSelect;

export type InsertPost = typeof postsTable.$inferInsert;
export type SelectPost = typeof postsTable.$inferSelect;

设置 Drizzle 配置文件

Drizzle config - 一个配置文件,由 Drizzle Kit 使用,包含关于你的数据库连接、迁移文件夹和 schema 文件的所有信息。

在项目根目录创建一个 drizzle.config.ts 文件,并添加以下内容:

drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/schema.ts",
  out: "./migrations",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

将更改应用到数据库

你可以使用 drizzle-kit generate 命令生成迁移,然后使用 drizzle-kit migrate 命令运行它们。

生成迁移:

npx drizzle-kit generate

generate 命令只会根据你的 schema 创建迁移 SQL 文件——它不会对数据库应用任何更改。这些迁移会按照 drizzle.config.ts 中的配置存储在 migrations 目录下。这个目录将包含更新数据库 schema 所需的 SQL 文件,以及一个用于存储不同迁移阶段 schema 快照的 meta 文件夹。

生成的迁移示例:

CREATE TABLE "posts" (
	"id" serial PRIMARY KEY NOT NULL,
	"title" text NOT NULL,
	"content" text NOT NULL,
	"userId" integer NOT NULL,
	"createdAt" timestamp DEFAULT now() NOT NULL,
	"updatedAt" timestamp NOT NULL
);
--> 语句分隔点
CREATE TABLE "users" (
	"id" serial PRIMARY KEY NOT NULL,
	"name" text NOT NULL,
	"age" integer NOT NULL,
	"email" text NOT NULL,
	"CONSTRAINT" "users_email_unique" UNIQUE("email")
);
--> 语句分隔点
ALTER TABLE "posts" ADD CONSTRAINT "posts_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;

将生成的迁移应用到数据库:

npx drizzle-kit migrate

在本教程中,迁移会在应用启动时使用 drizzle-orm/node-postgres/migrator 中的 migrate() 自动应用。你也可以在本地测试时使用 drizzle-kit migrate 手动应用迁移。

或者,你也可以使用 Drizzle kit push command 直接将更改推送到数据库:

npx drizzle-kit push
IMPORTANT

Push 命令非常适合本地开发中的快速原型设计,无需管理迁移文件即可快速迭代。对于生产部署,建议使用 generate + migrate 工作流,以保留 schema 更改的版本历史。

基本文件结构

这是项目的基本文件结构。在 src 目录中,我们有与数据库相关的文件,包括 db.ts 中的连接和 schema.ts 中的 schema 定义。

📦 <project root>
 ├ 📂 src
 │  ├ 📜 db.ts
 │  ├ 📜 schema.ts
 │  └ 📜 index.ts
 ├ 📂 migrations
 │  ├ 📂 meta
 │  │  ├ 📜 _journal.json
 │  │  └ 📜 0000_snapshot.json
 │  └ 📜 0000_optimal_the_fury.sql
 ├ 📜 .env
 ├ 📜 drizzle.config.ts
 ├ 📜 package.json
 └ 📜 tsconfig.json

将应用部署到 Railway

为部署准备项目

一旦你生成了迁移,就将 migrations 目录提交到你的仓库中——这些文件会在应用启动时自动应用。

创建一个 index.ts 文件作为应用入口点。这个示例会在启动时运行迁移,然后使用 Node.js 启动一个简单的 HTTP 服务器:

src/index.ts
import { createServer } from "node:http";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { usersTable } from "./schema";

const db = drizzle(process.env.DATABASE_URL!);

await migrate(db, { migrationsFolder: "./migrations" });

const server = createServer(async (req, res) => {
  const url = new URL(req.url!, `http://${req.headers.host}`);

  if (url.pathname === "/users") {
    const users = await db.select().from(usersTable);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(users));
    return;
  }

  res.writeHead(200);
  res.end("OK");
});

const port = process.env.PORT || 3000;
server.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

migrate() 函数会读取你 migrations 目录中的 SQL 文件,并将所有尚未应用的迁移应用到数据库。它可以安全地在每次启动时运行——已经应用过的迁移会被跳过。

顶层 await 要求 Node.js 将该文件视为 ES 模块。请确保你的 package.json 包含 "type": "module"

确保你的 package.json 包含模块类型和一个 start 脚本:

package.json
{
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts"
  }
}

将项目推送到 GitHub

Railway 从 GitHub 仓库部署。初始化一个 git 仓库并推送你的代码:

git init
git add .
git commit -m "initial commit"

在 GitHub 上创建一个新仓库并将你的代码推送到其中。

将 GitHub 仓库连接到 Railway

在你的 Railway 项目中,点击画布右上角的 New 按钮,然后选择 GitHub Repo。如果提示,请连接你的 GitHub 账户,然后选择你刚刚推送的仓库。

配置环境变量

在已部署服务的设置中,进入 Variables 选项卡。通过引用 PostgreSQL 服务变量添加 DATABASE_URL 变量:

点击 Add Variable,将名称设置为 DATABASE_URL,并使用 Railway 变量引用 ${{Postgres.DATABASE_URL}} 作为值。

${{Postgres.DATABASE_URL}} 引用变量会在运行时自动解析为私有内部连接字符串,这非常适合 Railway 内部服务之间的通信。这与您在本地 .env 文件中使用的公共 URL 不同。

部署

当你将代码推送到已连接的分支时,Railway 会自动部署你的应用。你可以在 Deployments 选项卡中监控部署过程。

验证你的设置

部署完成后,前往 Railway 项目的 Architecture 选项卡。你应该能看到你的应用服务已连接到 PostgreSQL 数据库。

替代方案:零停机迁移

本教程在应用启动时使用 migrate() 来应用迁移。这是最简单的方法,并且适用于大多数应用。

如果你需要零停机部署,你可能希望在新版本应用开始接收流量之前,将迁移作为一个单独步骤来运行。这样你就可以独立于数据库更改来回滚应用。

在 Railway 上,你可以使用 pre-deploy command 来实现这一点——它会在构建和部署阶段之间运行,可以访问你的私有网络和环境变量,如果它失败了,部署将不会继续。

在你的服务设置中,向下滚动到 Deploy 部分并点击 Add pre-deploy step

输入以下命令:

npx drizzle-kit migrate

采用这种方式后,请从你的 index.ts 中移除 await migrate(db, { migrationsFolder: "./migrations" }) 调用——迁移将由 pre-deploy 命令来处理。

有关更多详细信息,请参阅 Drizzle migrations fundamentals 页面。

将 Drizzle Studio 部署到 Railway

你可以将 Drizzle Studio 与你的应用一起部署到 Railway 上,直接从浏览器中浏览和管理你的数据库。你可以使用 Drizzle Studio Railway 模板 或按照以下步骤操作。

从模板添加新服务

在你的 Railway 项目中,点击画布右上角的 New 按钮,并选择 Template

选择 Drizzle Studio 模板

搜索 drizzle studio,并选择由 Drizzle 提供的 Drizzle Studio 模板。

配置环境变量并部署

该模板预先配置了两个环境变量:

  • PASSCODE - 用于安全访问你的 Studio 实例的密码。默认值为 ${{secret()}},它会生成一个随机密钥。
  • DATABASE_URL - 数据库连接字符串。将其设置为 ${{Postgres.DATABASE_URL}},以引用你现有的 PostgreSQL 服务。

点击 Deploy Template 进行部署。

找到你的 Drizzle Studio URL

部署完成后,点击项目中的 Drizzle Studio 服务,进入 Settings 选项卡,然后向下滚动到 Networking 部分。你会在 Public Networking 下找到你的公共域名。

使用 Drizzle Studio 浏览你的数据库

在浏览器中打开 Drizzle Studio 的 URL。你应该能看到数据库表,并可以直接浏览、筛选和编辑数据。

验证你的设置

进入 Railway 项目的 Architecture 选项卡。此时你应该能看到三个服务:你的应用、PostgreSQL 数据库以及 Drizzle Studio。