Next.js 页面能打开以后,连接数据库常常是下一步。应用和数据库放进不同容器时,localhost 的含义变了:应用容器里的 localhost 指向应用自己,不是数据库容器。
Compose 可以把两个服务的启动参数和网络关系放到一份配置里。下面用一个访问计数接口说明,不涉及云平台部署。
创建一个最小 Next.js 应用
新建目录,创建 package.json:
{
"name": "next-db-demo",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "15.5.27",
"react": "19.2.0",
"react-dom": "19.2.0",
"pg": "8.13.3"
}
}
运行 npm install 生成 package-lock.json,Docker 构建会用 npm ci 读取它。创建 next.config.mjs:
export default { output: "standalone", poweredByHeader: false };
创建 app/layout.jsx:
export default function Layout({ children }) {
return (
<html lang="zh-CN">
<body>{children}</body>
</html>
);
}
创建 app/page.jsx:
export default function Page() {
return (
<main>
<h1>数据库计数</h1>
<p>用 POST /api/db 增加记录,GET /api/db 读取数量。</p>
</main>
);
}
创建 app/api/db/route.js:
import pg from "pg";
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
connectionTimeoutMillis: 3000,
max: 5,
});
export const dynamic = "force-dynamic";
export async function GET() {
try {
const { rows } = await pool.query("SELECT count(*)::int AS visits FROM visits");
return Response.json(rows[0], { headers: { "Cache-Control": "no-store" } });
} catch {
return Response.json({ error: "数据库暂时不可用" }, { status: 503 });
}
}
export async function POST() {
try {
await pool.query("INSERT INTO visits DEFAULT VALUES");
return Response.json({ ok: true }, { status: 201 });
} catch {
return Response.json({ error: "数据库暂时不可用" }, { status: 503 });
}
}
读取和写入分开,避免打开一个 GET 地址就增加记录。连接池放在模块里复用,不在每个请求里重新创建。这里没有鉴权,接口只用于本机演示。
镜像里只带运行需要的内容
创建 Dockerfile:
FROM node:22.14.0-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY app ./app
COPY next.config.mjs ./
RUN npm run build
FROM node:22.14.0-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]
构建阶段安装依赖并生成 standalone 产物,运行阶段复制服务端入口和静态文件。若项目有 public 目录,还要把它复制到运行镜像;这个最小应用没有 public。
创建 .dockerignore,避免把本机依赖和构建缓存带进去:
node_modules
.next
.git
数据库和应用怎样连接
创建 init.sql:
CREATE TABLE IF NOT EXISTS visits (id serial PRIMARY KEY, created_at timestamptz DEFAULT now());
同目录创建 compose.yaml:
services:
db:
image: postgres:16.8-bookworm
environment:
POSTGRES_USER: demo
POSTGRES_PASSWORD: local-demo-only
POSTGRES_DB: demo
volumes:
- demo-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: [CMD-SHELL, "pg_isready -U demo -d demo"]
interval: 3s
timeout: 3s
retries: 20
app:
build: .
environment:
DATABASE_URL: postgres://demo:local-demo-only@db:5432/demo
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:4312:3000"
volumes:
demo-data:
连接地址里的 db 是 Compose 服务名,端口 5432 是容器内部端口。数据库没有映射到宿主机,应用对外只绑定本机 4312。
depends_on 配合 service_healthy 等待数据库通过健康检查,不能只靠“先启动了 db 容器”就认为数据库已经能接收连接。具体行为参考 Docker 启动顺序文档。
检查配置,再检查运行结果
docker compose config --quiet
docker compose up --build -d
docker compose ps
curl -X POST http://127.0.0.1:4312/api/db
curl http://127.0.0.1:4312/api/db
docker compose logs app db
config 通过只能说明配置可解析。还需要看到数据库 healthy,POST 返回 201,再由 GET 读到 visits。重启应用后,数据应保留在数据库卷里。
当前本机的 Docker / WSL 后端无法启动,容器运行、数据库持久化还没实际验收。应用构建和配置检查不能替代这些结果;上面的响应是运行时需要检查的目标。
init.sql 只在新数据库目录初始化时执行,改动 SQL 不会自动改已有卷里的表。正式项目要有迁移步骤。健康检查也不能保证数据库永远可用,应用仍然需要超时和失败处理。密码只是本地示例值,部署时应由运行环境注入。