Guide
How to deploy a Next.js frontend and a Node or Go API from one repo
A Next.js app calling an API with a database, deployed from a single repository: Dockerfiles, one config file, generated URLs and the right order.
· 3 min read
In short
- One folder and Dockerfile per app, one prongo.yaml for the stack
- Use the generated {{ api.url }} on the server, never localhost
- Only NEXT_PUBLIC_ values reach the browser, and they are baked in at build time
A very common shape for a modern project: a Next.js frontend, a separate API (Node, Go or PHP) and a Postgres database, all in one repository. Locally it is three terminals. In production it should be one deploy. Here is how to set that up properly.
The layout
my-app/
├── apps/
│ ├── web/ # Next.js, Dockerfile, port 3000
│ └── api/ # Node or Go, Dockerfile, port 8080
└── prongo.yaml
A Dockerfile for Next.js
Next.js can emit a minimal standalone server, which keeps the image small. Enable it in next.config.js:
module.exports = { output: 'standalone' }
Then a multi-stage Dockerfile in apps/web:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Your API needs its own Dockerfile that listens on the port you declare.
One config file for the whole stack
project: my-app
services:
database:
image: postgres:16
port: 5432
env:
POSTGRES_DB: app
api:
build: ./apps/api
port: 8080
depends_on: [database]
pre_deploy: [npm run migrate]
web:
build: ./apps/web
port: 3000
depends_on: [api]
env:
API_URL: "{{ api.url }}"
What this gives you:
webwaits forapi, andapiwaits fordatabase.{{ api.url }}is replaced with the real address, so there is no hardcoded host.- Migrations run before the new API version goes live, and a failure keeps the old version running. See migrations on every deploy.
- The database password lives in the dashboard as a secret, not in git.
Field details are in the config reference.
Server-side vs browser calls
This trips up many people. API_URL above is read on the server (server components, route handlers), where the internal address works.
If the browser also calls the API directly, it needs a public address, and it must be exposed to the client with a NEXT_PUBLIC_ prefix. Those values are inlined at build time, so never put a secret there. A good pattern is to let the browser talk to Next.js, and let Next.js talk to the API over the internal address. That also avoids CORS.
Common problems
- “fetch failed” in production only. You are still pointing at
localhost. Use the generated service URL. - Works after a second deploy. The frontend started before the API. Add
depends_on: [api]. - Environment variable is undefined in the browser. It needs the
NEXT_PUBLIC_prefix and a rebuild, since it is baked in at build time. - Image is huge. Use the standalone output and a multi-stage build as above.
Where to go next
The general principles are in how to deploy a monorepo with multiple services. If you are weighing hosts, read Vercel vs a monorepo platform.
Frequently asked questions
Can I deploy Next.js and an API from the same repository?
Yes. Give each app its own folder and Dockerfile, then deploy them as separate services that are linked together. A monorepo-aware platform lets you do this from one config file.
How does the Next.js app know the API address in production?
Pass it as an environment variable that the platform fills in at deploy time, for example API_URL: "{{ api.url }}", instead of hardcoding localhost.
Should the API be a Next.js route handler or a separate service?
A separate service makes sense when the API is shared by several clients, needs long-running work, WebSockets, or a different language. Route handlers are fine for small, frontend-only needs.
What about NEXT_PUBLIC variables?
Variables prefixed with NEXT_PUBLIC_ are inlined into the browser bundle at build time, so they must be public values. Server-side URLs and secrets should stay in non-prefixed variables.