Skip to main content
Production Next.js · Blog 21

Next.js 16 Docker Compose Production Stack

Run a self-hosted Next.js app with PostgreSQL and Redis using private service discovery, persistent volumes, readiness checks, safe migrations, and optional Traefik routing.

Next.js 16 application connected by Docker Compose to PostgreSQL and Redis

In Blog #20, we deployed a Next.js container behind Traefik. Now we will expand that educational architecture into a complete application stack by adding PostgreSQL and Redis through Docker Compose. The database layer reuses the server-only Drizzle approach from Blog #19; moving PostgreSQL into a container changes the hostname, not the application data architecture.

Infrastructure audit

This repository publishes a PHP/Apache website. It has no root Next.js package, Dockerfile, Compose deployment, Traefik definition, PostgreSQL/Redis service, ORM, migrations, or health endpoint. The downloadable starter uses npm and floating latest packages, so no exact Next.js patch can be confirmed here. Everything below is an educational, pinned example for a separate Next.js 16 application; no production infrastructure is changed and Redis is not presented as mandatory.

  1. Blog #20Docker + TraefikPublished
  2. Blog #21Compose + Data ServicesCurrent
  3. Blog #22Redis Caching + Rate LimitingPublished

Production Stack at a Glance

Traefik is the optional public edge. The app joins both the proxy network and a private data network; PostgreSQL and Redis join only the data network. PostgreSQL holds relational records, while Redis handles only clearly chosen fast or shared state.

Diagram 1: Complete stackInternet traffic crosses Traefik to Next.js, while Next.js alone reaches PostgreSQL and Redis on a private data network backed by named volumes.
Internet
HTTPS
Traefik
Next.js appPostgreSQLPG volumeRedisRedis volume

Why Docker Compose?

Compose defines services, networks, volumes, health checks, environment inputs, and restart behavior in one reviewable model. Service names provide DNS discovery, named volumes outlive containers, and consistent commands make a local or one-host self-managed stack repeatable.

Where it fits

Small self-hosted systems, staging environments, integration tests, and teams that accept one-host operations.

What it does not solve

High availability, cross-host failover, managed backups, zero-downtime database upgrades, or automatic capacity planning. Compose is not Kubernetes.

Project Architecture

Educational Next.js project
project/
|-- app/
|-- db/
|-- Dockerfile
|-- compose.yaml
|-- .dockerignore
|-- .env.example
`-- package.json

If Traefik already runs in shared infrastructure, keep it there. Attach the app to its existing external proxy network instead of creating a second proxy or certificate resolver.

Next.js, PostgreSQL, and Redis Services

Reuse the standalone production image from Blog #20. Pin and test image patches or digests in the real deployment. This readable example uses PostgreSQL 18 and Redis 8 major lines. PostgreSQL 18 changed the official image volume target to /var/lib/postgresql; version 17 and older use /var/lib/postgresql/data.

compose.yaml
services:
  app:
    image: ghcr.io/OWNER/nextjs-app:${IMAGE_TAG}
    restart: unless-stopped
    env_file: [.env.production]
    expose: ["3000"]
    networks: [proxy, data]
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

  postgres:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets: [postgres_password]
    volumes:
      - postgres_data:/var/lib/postgresql
    networks: [data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:8-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    networks: [data]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s

networks:
  proxy:
    external: true
  data:
    internal: true

volumes:
  postgres_data:
  redis_data:

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt

This publishes no 5432 or 6379 host ports. expose documents the app's internal port but is not a firewall; network membership and host rules define reachability. The Redis probe fits this unauthenticated educational configuration. If authentication is enabled, adapt both client and probe without leaking the credential.

Private Docker network connecting a Next.js container with PostgreSQL and Redis while a protected gateway handles public traffic
Separate the edge from the data plane. Only the app needs both networks; databases should not share the public-facing proxy network.

Docker Networking and Service Discovery

Containers do not use localhost to reach each other. Inside the app container, localhost means the app itself. Compose DNS resolves postgres and redis because those are service names on the shared data network.

Diagram 2: Service discoveryThe Next.js container resolves postgres and redis on the private data network; localhost loops back to Next.js.
Next.js containerlocalhost → itself
postgres:5432redis:6379
Diagram 3: Network separationTraefik and the app share the proxy lane, while the app, PostgreSQL, and Redis share a separate private data lane.
Public edgeInternetTraefik :443
Proxy networkTraefikNext.js :3000
Internal dataNext.jsPostgreSQL + Redis

Environment Variables and Secret Flow

Keep DATABASE_URL, REDIS_URL, and AUTH_SECRET server-side, as explained in Blog #16. Never create NEXT_PUBLIC_DATABASE_URL or NEXT_PUBLIC_REDIS_URL. Commit an empty .env.example, not production values.

.env.example
DATABASE_URL=postgresql://APP_USER:APP_PASSWORD@postgres:5432/APP_DB
REDIS_URL=redis://redis:6379
AUTH_SECRET=
POSTGRES_DB=APP_DB
POSTGRES_USER=APP_USER
IMAGE_TAG=replace-with-release-tag
Diagram 4: Server-only connection flowThe browser calls Next.js; only the server reads database and Redis connection URLs and returns authorized safe data.
Browser
Next.js serverDATABASE_URLREDIS_URL
PostgreSQLRedis

PostgreSQL and Redis Persistence

A named volume separates data from a container lifecycle. Recreating PostgreSQL can leave its volume intact, but deleting the volume, losing the disk, corruption, operator error, or host failure can still destroy data. Schedule backups, encrypt and retain copies away from the host, and practice restoration.

Diagram 5: PostgreSQL persistenceDatabase files move into a named volume so data can survive container replacement, while a separate backup remains required.
PostgreSQLDatabase filesNamed volumeRecreate containerData remains

Redis supports RDB snapshots, AOF logging, both, or no persistence. Cache-only data may be safely rebuildable. Sessions, queues, rate-limit state, or coordination may have stricter recovery requirements. A Redis volume does not decide this policy by itself.

Diagram 6: Redis use casesCache entries, sessions, rate-limit counters, and queues have different acceptable loss windows.
CacheOften rebuildable
SessionsUser continuity
Rate limitsSecurity policy
QueuesDelivery semantics
Redis serving cache, rate limiting, secure session and coordination patterns next to PostgreSQL
Redis is a deliberate capability, not a default requirement. Define ownership, expiration, invalidation, memory limits, and failure behavior first.

Health Checks, Startup Order, and Migrations

Short-form depends_on orders startup but does not mean a dependency accepts queries. Long-form condition: service_healthy waits for the declared probe. Applications should still retry transient connections with bounded backoff because dependencies can restart later. Implement a small public liveness response with the safe server boundary from Blog #10: Route Handlers; do not return credentials or detailed dependency errors.

Diagram 7: Service healthCompose observes independent application, PostgreSQL, and Redis probes before treating dependencies as healthy.
App/api/health
PostgreSQLpg_isready
Redisredis-cli ping
Composeservice_healthy
Diagram 8: Controlled startupCreate data services, wait for health, run one reviewed migration job, start the app, then verify the release.
Create servicesWait for healthMigrate onceStart appVerify release

Do not run migrations during next build or in every replica. Reuse the Drizzle configuration from Blog #19, review SQL, confirm a restorable backup, run one controlled release task, then deploy compatible code.

Redis Caching Patterns

Redis application caching is separate from the Next.js framework cache in Blog #8. A cache-aside path checks Redis, loads the database or API on a miss, stores a scoped result with an appropriate TTL, and returns it. Choose expiration from business freshness needs; there is no universal TTL.

Diagram 9: Cache-aside requestA request checks Redis; a hit returns immediately, while a miss loads PostgreSQL and writes a time-limited entry.
RequestCheck RedisMiss: query DBSave with TTLReturn
CacheLives whereTypical purpose
Next.js framework cacheNext.js runtime/storageRoute and data rendering
RedisSeparate serviceShared application data
Browser cacheUser browserClient resources
CDN cacheEdge/CDNPublic delivery

Use predictable keys such as posts:list and post:123. Scope private entries to the authorized user or tenant, never put secrets in key names, monitor memory, and design invalidation. Solve cache stampedes only when measurement shows the need.

Traefik and Multi-Instance Next.js

If Traefik owns the proxy network, add only the app to it and keep data services isolated. Point the router at port 3000 and avoid publishing the app port. For multiple Next.js instances, a generic Redis cache does not automatically coordinate framework revalidation. Current self-hosting guidance requires a compatible cache handler and tag coordination.

Diagram 10: Multi-instance coordinationTraefik balances two Next.js instances using an explicitly implemented shared cache and tag coordination layer.
Traefik
Next.js ANext.js B
Shared cache handlerTag coordination

Production Security and Attack Surface

Keep Redis and PostgreSQL private, patch images and the host, run the app as non-root, restrict Docker socket access, protect secrets, use least-privilege database roles, apply Redis ACLs/authentication where appropriate, encrypt untrusted network traffic, and size resource limits from observed workloads. Network isolation helps but does not replace the session and authorization controls in Blog #13: Authentication, backups, or monitoring.

Diagram 11: Reduced attack surfaceThe internet reaches only HTTPS on Traefik; app, database, and Redis interfaces remain progressively more restricted.
InternetTraefik :443App internalPostgres privateRedis private

Backups, Resources, Logs, and Monitoring

Measure application behavior using the principles in Blog #14: Performance Optimization, and make recovery and rollback part of the release process from Blog #15: Deployment.

Recovery

  • Automate PostgreSQL backups off-host
  • Define Redis persistence from the use case
  • Retain versioned configuration
  • Practice restore and record RPO/RTO
  • Test upgrades on copied data

Operations

  • Monitor health, restarts, latency, disk and memory
  • Set Redis maxmemory and eviction deliberately
  • Rotate bounded logs without secrets
  • Alert on backup failure and storage pressure
  • Patch pinned images through a tested process

Common Docker Compose Errors

ECONNREFUSED localhost

Use postgres or redis, confirm the shared network, then inspect health and credentials.

App starts too early

Add health checks, long-form dependencies, and bounded application retries.

Data disappears

Verify the version-specific mount path, volume, host storage, and backup restoration.

Redis is unreachable

Check protected mode/auth, network aliases, ACLs, probe, and port assumptions.

Traefik returns 502

Confirm the proxy network, app health, internal port, router rule, and selected network.

Migration races

Remove migrations from replica startup and run one controlled release job.

Diagram 12: Troubleshooting pathDebug from service state through logs, health, DNS and ports, configuration presence, then a minimal connection test.
compose pslogshealthDNS + portsconnection test

Best Practices

  • Audit existing managed services, networks, proxy, ORM, and Redis client before adding containers.
  • Use immutable app tags and pinned, regularly updated data-service versions.
  • Keep PostgreSQL and Redis off public host ports.
  • Use service DNS names, not localhost or container IP addresses.
  • Keep connection URLs server-side and never log credential values.
  • Use health-aware dependencies plus runtime retries.
  • Mount the data path matching the pinned official image major.
  • Treat volumes as persistence, not backups.
  • Run reviewed migrations exactly once per release.
  • Add Redis only with TTL, invalidation, memory, and durability decisions.
  • Monitor restore ability, not only uptime.
  • Validate with docker compose config in the real Next.js repository.

Frequently Asked Questions

How do I run Next.js with PostgreSQL and Redis in Docker Compose?

Define app, postgres, and redis services on private Compose networks, use service names in server-only connection URLs, add health checks and named volumes, and expose only the reverse proxy publicly.

Does Next.js need Redis?

No. Add Redis only for a demonstrated need such as shared caching, rate limiting, sessions, queues, locks, or repeated expensive results.

Why use PostgreSQL with Docker Compose?

Compose can provide a repeatable self-hosted database service, private networking, health checks, and persistent storage on one host. You still own upgrades, backups, monitoring, and recovery.

How does Next.js connect to PostgreSQL inside Docker?

Use the Compose service hostname postgres on port 5432. Localhost inside the app container points back to the app container itself.

How does Next.js connect to Redis inside Docker?

Use a server-only URL such as redis://redis:6379, adapted for the authentication and TLS model you actually configure.

Should PostgreSQL port 5432 be public?

Usually no when only the application needs access. Keep PostgreSQL on a private data network and use a protected administrative path when necessary.

Should Redis port 6379 be public?

Usually no. Isolate Redis from the internet and add appropriate authentication, ACLs, firewall rules, and encryption for the threat model.

What is the difference between a Docker volume and a backup?

A volume keeps data outside a container lifecycle on the same storage system. A backup is a separate, restorable copy with retention and recovery testing.

Does depends_on wait for PostgreSQL to be ready?

Short-form depends_on only establishes order. Long-form depends_on with condition service_healthy waits for the dependency health check, while the app should still retry transient failures.

How do I persist PostgreSQL data?

Mount a named volume at the path required by the pinned official image. PostgreSQL 18 and newer use /var/lib/postgresql, while 17 and older use /var/lib/postgresql/data.

Should Redis use persistence?

It depends. Rebuildable cache data may need none; sessions, queues, or important state require an explicit RDB, AOF, replication, and recovery decision.

Can Redis replace Next.js caching?

No. Redis is a separate application data service; Next.js framework caches, browser caches, and CDN caches have different responsibilities.

How do I use Redis for caching in Next.js?

Check a predictable user-safe key in server-only code, load the authoritative source on a miss, store a result with an appropriate TTL, and invalidate it when required.

Can one Compose host provide high availability?

No. A single host remains a failure domain. High availability needs replication, multiple failure domains, routing, and tested failover.

When should database migrations run?

Run reviewed migrations once as a controlled release job after confirming backup and compatibility, not independently in every app replica or during image build.

Current Official References

Next Steps

You now have a production-minded model for a one-host Next.js Docker Compose stack: private service discovery, version-aware PostgreSQL storage, intentional Redis use, health-aware startup, controlled migrations, backups, and a narrow public edge. Apply Redis to real caching and abuse-control needs in Blog #22: Next.js 16 Redis Caching & Rate Limiting.

WhatsApp