跳转到主要内容
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

本地栈就两个服务——postgresapp。compose 不跑迁移,迁移由你在宿主机用 pnpm db:push(开发)或 pnpm db:migrate(提交了正式迁移文件之后)触发。
# 1. 先单独把 Postgres 起来
docker compose up -d postgres

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

# 3. 把 app 起来
docker compose up app
打开 localhost:3000,去 /register 注册,搞定。 停服保留数据:docker compose down。彻底清掉 Postgres:docker compose down -v
开发阶段 pnpm db:push 直接把 src/db/*.schema.ts 同步到本地 DB——不用生成 迁移文件。生产环境永远用 pnpm db:migrate 跑提交进 git 的迁移。

加可选 API key

docker-compose.yml 内置默认值只覆盖最低需求(auth + DB)。要启用 Stripe、Resend、 OAuth provider、AI key 等,建一个 .env.docker
cp .env.example .env.docker
# 编辑 .env.docker——只填你真正想启用的那几个
Compose 通过 env_file 指令读它(声明了 required: false,文件不存在也不报错)。 .env.docker 里的值会覆盖 environment: 块里的默认。改完用 docker compose up --build 重启生效。.env.docker 既在 .gitignore 也在 .dockerignore 里——不会进镜像,不会进 git。

构建生产镜像

每次发布构建一个镜像:
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

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_SECRET32 位以上随机字符。openssl rand -base64 32
BETTER_AUTH_URL公网域名。没填 OAuth 回调直接挂。
ADMIN_EMAILS逗号分隔的 email,注册后自动拿 role=admin
按需补 provider key(Stripe、Resend、OAuth、AI),完整列表看 docs/env-reference

数据库迁移

迁移故意不放在运行时镜像里。runner 只装了服务流量必需的东西—— drizzle-kitdevDependencies 里,由你从本机调用:
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,认知成本更低。
这一步跑的是 drizzle-kit migrate,针对的是提交进 src/db/migrations/ 的 迁移文件。先用 pnpm db:generate 生成、commit SQL,再 pnpm db:migrate 打到生产。
如果生产 DB 在私有 VPC 里、本机连不上,看 kubernetes 部署文档里的”一次性 pod” 兜底方案——在集群内拉一个临时容器,借集群网络跑同一条命令。

PaaS 一键部署

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

多平台 / Apple Silicon

M 系列 Mac 上构建给 x86 Linux 服务器用,要用 buildx
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-compatpython3makeg++)放在 deps 阶段。 避免 Alpine 上某些 npm 包源码编译时 npm install 失败 (better-sqlite3、sharp 各种变体之类)。
  • 构建时占位符 DATABASE_URLBETTER_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 主机上起不来。 用上面的多平台方案。

官方文档