Incident timeline

    ← All articles

    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:

    • web waits for api, and api waits for database.
    • {{ 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.

    Browser webNext.js apiNode / Go databasepostgres internal address: {{ api.url }}
    Let the browser talk to Next.js and Next.js talk to the API. No CORS, no public API address.

    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.