Compose Deployment

Running Next.js, Express, and MongoDB with Compose

Docker and Compose already brings up MongoDB 1,815 and the Express 24,430 API together; adding the Next.js 10,514 container completes the stack.

compose.yaml — the full MERN stack on one hostYAML
services:
  mongo:                              # health check as in Section 4.1.4
    image: mongodb/mongodb-community-server:8.3-ubi8
    volumes: [mern-mongo-data:/data/db]
  api:
    build: ./api
    environment: { MONGODB_URI: "mongodb://mongo:27017/bookshelf" }
    depends_on: { mongo: { condition: service_healthy } }
  web:
    build:
      context: ./web
      args: { NEXT_PUBLIC_SITE_NAME: "Widget Shop" }
    ports: ["3000:3000"]
    environment: { API_BASE_URL: "http://api:4000" }
    volumes: [next-cache:/app/.next/server/app]
    depends_on: [api]
    restart: unless-stopped
    healthcheck:
      test:
        - CMD-SHELL
        - node -e "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1))"
      interval: 15s
volumes: { mern-mongo-data: , next-cache: }

API_BASE_URL points at http://api:4000, a hostname Compose resolves on the project network and that exists nowhere else. That is the shape you want: the browser never talks to Express directly, so only web publishes a port and the server-side fetches of Consuming Express cross a private network. Anything the browser must know is still a NEXT_PUBLIC_* build argument, which is why it sits under build.args rather than environment. The named volume on /app/.next/server/app keeps regenerated ISR pages across docker compose up --build, and the health check reuses the endpoint from Self-Hosting Behind nginx through Node's built-in fetch, so the image needs no curl 3,008 .

When one container becomes three

Scaling web horizontally breaks three assumptions, and Next.js documents an answer for each. Every instance keeps its own filesystem cache, so revalidateTag() on one leaves the others stale: point cacheHandler (and cacheHandlers for use cache) at shared storage such as Redis 2,763 , set cacheMaxMemorySize: 0, and implement refreshTags(), which Next.js calls before each request so an instance learns about invalidations elsewhere. Server Function payloads are encrypted per build, so every instance needs the same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY. And rolling deployments need the deploymentId of Production Builds.