See all blogs

July 11, 2026

Set Up Local PostgreSQL and Redis with Docker for Next.js 15+

Set Up Local PostgreSQL and Redis with Docker for Next.js 15+Set Up Local PostgreSQL and Redis with Docker for Next.js 15+

Reference: Docker Compose docs ยท Prisma docs ยท ioredis

This guide follows the same kind of setup used in this app: Next.js App Router, TypeScript, Prisma ORM, a self-hosted PostgreSQL instance, and a traditional Redis instance โ€” both running locally via Docker, ready to be deployed the same way on a VPS later.

backstory

I wanted a local database setup that mirrors production exactly, instead of relying on a hosted free-tier database that behaves differently once real traffic hits it.

Docker Compose is useful because it gives you two separate pieces that work together:

  1. A PostgreSQL container that behaves exactly like the Postgres instance you'll eventually run on a VPS.
  2. A Redis container with password auth and persistence, so caching and counters survive restarts.

The important idea is this:

Your app talks to Postgres and Redis over normal network connections (localhost:5432, localhost:6379) โ€” it has no idea they're running inside Docker.

That means your Prisma schema, your DATABASE_URL, and your Redis client code stay identical whether you're running locally or on a production server.


what we are building

By the end, you will have this:

docker-compose.yml
.env
lib/
  redis.ts
  prisma.ts
scripts/
  test-redis.ts
prisma/
  schema.prisma

Two running containers:

postgres   โ†’ localhost:5432
redis      โ†’ localhost:6379

prerequisites

Before setting this up, make sure your machine already has:

  • Docker Desktop installed and running (with WSL2 backend if on Windows)
  • Next.js 15+ App Router project
  • TypeScript configured
  • Prisma installed (pnpm add prisma @prisma/client)
  • A terminal you're comfortable running commands in (VS Code's integrated terminal works fine)

Check Docker is ready before continuing:

docker --version
docker compose version

If either command fails, install Docker Desktop first and restart your machine.


step 1: check that your ports are free

PostgreSQL defaults to port 5432, Redis to 6379. Before writing any config, confirm nothing else on your machine is already using them:

netstat -ano | findstr :5432
netstat -ano | findstr :6379

If both commands return nothing, you're clear to use the default ports. If either returns a process, either stop that process or change the left-hand port number later in docker-compose.yml (e.g. '5433:5432') โ€” the right-hand number always stays the same, since that's the container's internal port.


step 2: create docker-compose.yml

Create this file at your project root:

version: '3.9'
 
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: craftui
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: craftui
    ports:
      - '5432:5432'
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U craftui']
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
    ports:
      - '6379:6379'
    volumes:
      - redisdata:/data
    healthcheck:
      test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'ping']
      interval: 5s
      timeout: 5s
      retries: 5
 
volumes:
  pgdata:
  redisdata:

What this file does:

  • postgres runs a lightweight Alpine-based Postgres 16 image, with data persisted in a named Docker volume (pgdata) so it survives container restarts.
  • redis runs Redis 7, protected with a password (--requirepass) and configured for AOF persistence (--appendonly yes) so cached data isn't lost if the container restarts.
  • healthcheck blocks let Docker (and you) verify each service is actually accepting connections, not just that the container started.
  • Both services use environment variables (${POSTGRES_PASSWORD}, ${REDIS_PASSWORD}) instead of hardcoded passwords, which are supplied by your .env file next.

step 3: choose safe passwords

Before writing your .env file, pick passwords that are safe to use inside connection URLs.

Avoid @, :, /, #, and ? in your passwords. These characters have special meaning inside a URL (like DATABASE_URL) and will break parsing unless carefully percent-encoded. Stick to letters, numbers, hyphens, and underscores.

For example, avoid:

WatuuloB54F20@

Use instead:

WatuuloB54F20x

step 4: create your .env file

POSTGRES_PASSWORD=your_strong_password_here
REDIS_PASSWORD=your_strong_redis_password_here
 
DATABASE_URL="postgresql://craftui:your_strong_password_here@localhost:5432/craftui"
REDIS_URL="redis://:your_strong_redis_password_here@localhost:6379"

Make sure .env is excluded from version control:

grep -n "^\.env$" .gitignore

If nothing prints, add it:

echo ".env" >> .gitignore

step 5: start the containers

Run this from your project root, in the same terminal session you use for pnpm dev:

docker compose up -d

The -d flag runs both containers in the background. The first run pulls the postgres:16-alpine and redis:7-alpine images, which takes a minute or two depending on your connection.


step 6: verify both containers are healthy

docker compose ps

You should see both postgres and redis listed with a status of Up (healthy). If either shows Up (health: starting) for more than 30 seconds, or unhealthy, check its logs:

docker compose logs postgres
docker compose logs redis

step 7: test the PostgreSQL connection directly

docker compose exec postgres psql -U craftui -d craftui -c "SELECT version();"

A Postgres version string printing back confirms the database is reachable inside the Docker network.


step 8: test the Redis connection directly

docker compose exec redis redis-cli -a your_strong_redis_password_here ping

A reply of PONG confirms Redis is running and accepting authenticated connections.


step 9: push your Prisma schema

With both containers healthy and your DATABASE_URL pointing at the running Postgres container:

pnpm dlx prisma generate
pnpm dlx prisma db push

db push reads prisma/schema.prisma, connects using DATABASE_URL, and creates every table your models describe directly inside the Postgres container.

Confirm the tables actually landed:

docker compose exec postgres psql -U craftui -d craftui -c "\dt"

step 10: install the Redis client

pnpm add ioredis

Create a singleton client so Next.js's hot-reload in development doesn't spawn a new connection on every file save:

import Redis from 'ioredis';
 
const globalForRedis = globalThis as unknown as { redis: Redis };
 
export const redis =
  globalForRedis.redis ??
  new Redis(process.env.REDIS_URL!, {
    maxRetriesPerRequest: 3,
  });
 
if (process.env.NODE_ENV !== 'production') globalForRedis.redis = redis;

step 11: write a standalone test script

Standalone scripts run outside Next.js's dev server, which means they don't automatically load .env the way next dev does. Load it explicitly:

pnpm add -D dotenv tsx
import 'dotenv/config';
import { redis } from '../lib/redis';
 
async function test() {
  await redis.set('test-key', 'hello from craft-ui');
  const value = await redis.get('test-key');
  console.log('Redis says:', value);
  process.exit(0);
}
 
test();

Use a relative import (../lib/redis) instead of the @/ alias in standalone scripts. tsx doesn't reliably resolve TypeScript path aliases outside the Next.js build pipeline, and this avoids Cannot find module '@/lib/redis' errors entirely.

Run it:

pnpm tsx scripts/test-redis.ts

You should see:

Redis says: hello from craft-ui

step 12: understand the important files

docker-compose.yml

This defines both services.

It answers:

  • What images run Postgres and Redis?
  • What passwords and databases do they use?
  • Where does their data persist between restarts?
  • How does Docker know each service is actually ready?

.env

This holds every secret and connection string.

It answers:

  • What password does each service require?
  • What full connection URL does Prisma use?
  • What full connection URL does the Redis client use?

lib/redis.ts

This is the shared Redis client.

It answers:

  • How does the app connect to Redis?
  • How is the connection reused instead of recreated on every hot reload?

prisma/schema.prisma

This defines every table Postgres will hold.

It answers:

  • What models exist?
  • What does prisma db push need to create?

common errors and fixes

NOAUTH Authentication required

This means your Redis client connected without a password โ€” usually because REDIS_URL wasn't loaded.

pnpm add -D dotenv

Add import 'dotenv/config'; as the very first line of any standalone script.

Cannot find module '@/lib/redis'

tsx can't resolve the @/ alias outside the Next.js build pipeline.

Use a relative import instead:

import { redis } from '../lib/redis';

Another next dev server is already running

Not a Docker issue, but common in the same workflow โ€” an orphaned Next.js process is holding the port. On Git Bash (MINGW64), taskkill /PID fails because Git Bash mangles the leading slash. Fix it with:

taskkill //PID 12345 //F

Password breaks DATABASE_URL

If your password contains @, :, /, #, or ?, the connection string will fail to parse correctly. Either choose a password without those characters, or percent-encode them (@ becomes %40) inside the URL only โ€” not inside POSTGRES_PASSWORD itself.

Containers show unhealthy

Check logs for the specific service:

docker compose logs postgres
docker compose logs redis

Common causes: a port conflict with another running service, or a malformed password in .env.


day-to-day commands

docker compose up -d        # start both services in the background
docker compose down         # stop and remove containers (data persists in volumes)
docker compose down -v      # stop AND wipe all data โ€” deletes your database
docker compose logs -f      # follow logs live
docker compose restart      # restart both services
docker compose ps           # check status of both services

final checklist

Use this checklist when setting up local Postgres and Redis with Docker:

  • Confirm Docker Desktop is installed and running
  • Confirm ports 5432 and 6379 are free
  • Create docker-compose.yml with both services and healthchecks
  • Choose passwords with no special URL characters
  • Create .env with POSTGRES_PASSWORD, REDIS_PASSWORD, DATABASE_URL, REDIS_URL
  • Add .env to .gitignore
  • Run docker compose up -d
  • Verify both containers are healthy with docker compose ps
  • Test Postgres directly with psql
  • Test Redis directly with redis-cli
  • Run prisma generate and prisma db push
  • Confirm tables exist with \dt
  • Install ioredis and create lib/redis.ts
  • Install dotenv for standalone scripts
  • Test the Redis connection with a standalone script

conclusion

The clean mental model is:

docker-compose.yml
  -> docker compose up -d
  -> Postgres + Redis running locally
  -> .env
  -> DATABASE_URL -> Prisma -> your tables
  -> REDIS_URL -> lib/redis.ts -> your app's cache and counters

Once both containers are healthy and both connection strings are correct, your Next.js app talks to them exactly the same way it will talk to their production counterparts on a VPS later โ€” no code changes required when you deploy.

The biggest advice is simple:

Keep passwords free of special URL characters, and always load .env explicitly in scripts that run outside next dev.

That's the difference between a five-minute setup and an hour of debugging NOAUTH and MODULE_NOT_FOUND errors.

resources

  • Docker Compose documentation
  • PostgreSQL Docker image
  • Redis Docker image
  • Prisma with PostgreSQL
  • ioredis documentation
  • dotenv documentation

See all blogs