> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 用 Docker 部署

> 把 Vibestrap 打包成 Docker 镜像跑起来——本地用 compose 一键起，生产环境随便挑主机部署。

Vibestrap 自带一套生产级容器配置：3 阶段 Alpine `Dockerfile`（deps → builder →
runner）；`docker-compose.yml` 只有 Postgres + app 两个服务；每次发布产出**一个**
镜像（约 349MB，Next.js standalone server，`dumb-init` 当 PID 1）。运行时镜像
故意**不**打包 `drizzle-kit`——迁移由开发者从本机针对生产 `DATABASE_URL` 触发。
这样镜像保持精简，滚动发布也不会有竞态风险。

## 前置条件

* **Docker 24+**。BuildKit 默认开启，deps 阶段的 pnpm store
  `--mount=type=cache` 必须靠它。
* Compose 插件（`docker compose version` 能打印版本就行）。
* 本地开发就这些——compose 自带 Postgres 17。
* 生产建议上托管 Postgres（Neon、Supabase、RDS、Crunchy、Railway 任选）。
  内置的 compose Postgres 只适合开发，没备份没高可用。

## 快速上手:docker compose

本地栈就两个服务——`postgres` 和 `app`。compose 不跑迁移，迁移由你在宿主机用
`pnpm db:push`（开发）或 `pnpm db:migrate`（提交了正式迁移文件之后）触发。

```bash theme={null}
# 1. 先单独把 Postgres 起来
docker compose up -d postgres

# 2. 在宿主机把 schema 推上去（开发捷径，不生成迁移文件）
pnpm db:push

# 3. 把 app 起来
docker compose up app
```

打开 [localhost:3000](http://localhost:3000)，去 `/register` 注册，搞定。

停服保留数据：`docker compose down`。彻底清掉 Postgres：`docker compose down -v`。

<Note>
  开发阶段 `pnpm db:push` 直接把 `src/db/*.schema.ts` 同步到本地 DB——不用生成
  迁移文件。生产环境永远用 `pnpm db:migrate` 跑提交进 git 的迁移。
</Note>

## 加可选 API key

`docker-compose.yml` 内置默认值只覆盖最低需求（auth + DB）。要启用 Stripe、Resend、
OAuth provider、AI key 等，建一个 `.env.docker`：

```bash theme={null}
cp .env.example .env.docker
# 编辑 .env.docker——只填你真正想启用的那几个
```

Compose 通过 `env_file` 指令读它（声明了 `required: false`，文件不存在也不报错）。
`.env.docker` 里的值会覆盖 `environment:` 块里的默认。改完用
`docker compose up --build` 重启生效。`.env.docker` 既在 `.gitignore` 也在
`.dockerignore` 里——不会进镜像，不会进 git。

## 构建生产镜像

每次发布构建**一个**镜像：

```bash theme={null}
docker build \
  --build-arg NEXT_PUBLIC_APP_URL=https://your-domain.com \
  --build-arg NEXT_PUBLIC_APP_NAME=your-product \
  --target runner \
  -t ghcr.io/your-org/vibestrap:v1.0.0 .
```

`runner` target 跑业务流量。`NEXT_PUBLIC_*` 必须在构建时传入——Next.js 会把这些值
内联到客户端 bundle 里。镜像推到 registry，编排层（K8s manifest、Nomad job、
ECS task 之类）按 tag 引用。

## 为什么 NEXT\_PUBLIC\_APP\_URL 必须是 build-arg

这个坑第一次踩谁都中招。Next.js 把所有 `NEXT_PUBLIC_*` 值在**构建时**内联进
客户端 JavaScript bundle——浏览器里它们不是从 `process.env` 读出来的，而是在打包前
作为字符串字面量替换掉。

如果构建生产镜像时不传 `--build-arg NEXT_PUBLIC_APP_URL=https://...`，
Dockerfile 里的默认值（`http://localhost:3000`）就会被烧进发给用户的 JS 里。表现：

* 客户端的 OAuth 重定向 URL 指向 `http://localhost:3000/...`
* 分享 / SEO 元数据里的绝对 URL 都是 localhost
* 任何在客户端组件里读 `process.env.NEXT_PUBLIC_APP_URL` 的地方，
  生产环境拿到的都是 `http://localhost:3000`

运行时改不了——只能换正确 build-arg 重新构建。

## 运行生产 runner

```bash theme={null}
docker run --env-file .env.production -p 3000:3000 \
  ghcr.io/your-org/vibestrap:v1.0.0
```

runner 的最小化运行时 env：

| 变量                   | 说明                                          |
| -------------------- | ------------------------------------------- |
| `DATABASE_URL`       | 带连接池的 Postgres URL。托管库加 `?sslmode=require`。 |
| `BETTER_AUTH_SECRET` | 32 位以上随机字符。`openssl rand -base64 32`。       |
| `BETTER_AUTH_URL`    | 公网域名。没填 OAuth 回调直接挂。                        |
| `ADMIN_EMAILS`       | 逗号分隔的 email，注册后自动拿 `role=admin`。            |

按需补 provider key（Stripe、Resend、OAuth、AI），完整列表看
[`env-reference`](/zh/env-reference)。

## 数据库迁移

迁移**故意不放在运行时镜像里**。runner 只装了服务流量必需的东西——
`drizzle-kit` 在 `devDependencies` 里，由你从本机调用：

```bash theme={null}
DATABASE_URL='postgres://user:pass@prod-host:5432/db?sslmode=require' \
  pnpm db:migrate
```

`docker run` / `kubectl apply` 滚出依赖新 schema 的镜像**之前**先跑这一步。
为什么这么搞：

* 运行时镜像更精简——不带 `drizzle-kit`，不带迁移文件。
* 单副本部署本身就没竞态，schema 什么时候动完全你说了算。
* 比起为这种规模的 scaffold 接 init container 或 one-shot job，认知成本更低。

<Warning>
  这一步跑的是 `drizzle-kit migrate`，针对的是提交进 `src/db/migrations/` 的
  迁移文件。先用 `pnpm db:generate` 生成、commit SQL，再 `pnpm db:migrate`
  打到生产。
</Warning>

如果生产 DB 在私有 VPC 里、本机连不上，看 kubernetes 部署文档里的"一次性 pod"
兜底方案——在集群内拉一个临时容器，借集群网络跑同一条命令。

## PaaS 一键部署

Railway、Render、Fly.io、Coolify、Dokploy——都自动识别根目录 `Dockerfile`。
`runner` target 已经是最后一个阶段，构建直接落到正确镜像上，不需要额外配置。
触动 schema 的发布之前，从本机（或带源码的一次性容器）跑 `pnpm db:migrate`。

## 多平台 / Apple Silicon

M 系列 Mac 上构建给 x86 Linux 服务器用，要用 `buildx`：

```bash theme={null}
docker buildx create --use --name vibestrap-builder
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --target runner \
  --build-arg NEXT_PUBLIC_APP_URL=https://your-domain.com \
  -t ghcr.io/your-org/vibestrap:v1.0.0 \
  --push .
```

会产出 manifest list——每台主机上的 Docker 自动拉对应架构的变体。跳过这一步，
arm64 构建出来的镜像在 amd64 上根本起不来。

## 引擎盖下都改了什么

当前配置里值得知道的几个生产级升级：

* **`dumb-init` 当 PID 1。** 正确把 SIGTERM 转发给 Node 进程，`docker stop`
  能在约 0.3 秒优雅退出，不用等满 10 秒宽限期再被 SIGKILL。滚动发布的关键。
* **BuildKit pnpm store 缓存挂载**（`--mount=type=cache,id=pnpm-store`）。
  CI 构建从冷启动 3 分钟降到热缓存 90 秒——pnpm 的内容寻址 store 跨构建复用。
* **原生依赖工具链**（`libc6-compat`、`python3`、`make`、`g++`）放在 deps 阶段。
  避免 Alpine 上某些 npm 包源码编译时 `npm install` 失败
  （better-sqlite3、sharp 各种变体之类）。
* **构建时占位符** `DATABASE_URL` 和 `BETTER_AUTH_SECRET`。
  作用域限制在 `pnpm build` 这一行 RUN 里——不会留在镜像层里，避开了
  Docker linter 的 `SecretsUsedInArgOrEnv` 警告，同时让 `next build` 能过
  模块级的 Zod（`@t3-oss/env-nextjs`）校验。
* **runner 阶段的 `mkdir .next && chown nextjs:nodejs`。** 提前给非 root 用户
  建好缓存目录，prerender / image-optimization 在运行时写入才能成功，
  连受限 PSP 的平台也能跑。

## 常见坑

* **构建时忘传 `--build-arg NEXT_PUBLIC_APP_URL`。** 客户端所有绝对 URL 在生产
  全部指向 `http://localhost:3000`。运行时改不了——重新构建。
* **新镜像先于 `pnpm db:migrate` 上线。** 新代码可能引用还不存在的字段，
  启动时 Zod 或 Drizzle 直接报错。永远先迁移、再发布。
* **把 `.env` 挂载到运行容器的 `/app/.env`。** Next.js 的 standalone server
  运行时不读 `.env` 文件。env 必须通过 `docker run -e` / `--env-file` 注入，
  或者用平台的 secret store。
* **Apple Silicon 上不用 `buildx` 直接构建给 amd64 生产用。** M 系列上的普通
  `docker build` 出来的是 arm64 镜像，在 x86 Linux 主机上起不来。
  用上面的多平台方案。

## 官方文档

* [Docker docs](https://docs.docker.com/)
* [Docker Compose](https://docs.docker.com/compose/)
* [Next.js Docker example (官方)](https://github.com/vercel/next.js/tree/canary/examples/with-docker)
* [Drizzle migrations](https://orm.drizzle.team/docs/migrations)
