Infrastructure

Docker Setup

Grit uses Docker Compose to run all infrastructure services locally. One command gives you PostgreSQL, Redis, MinIO, and Mailhog — ready to go.

Overview

Every Grit project ships with two Docker Compose files:

  • docker-compose.yml Development infrastructure (databases, caching, storage, email)
  • docker-compose.prod.yml Production deployment (includes API, web, admin containers)

You do not need Docker to run the Go API or Next.js apps directly — Docker is only required for the infrastructure services (PostgreSQL, Redis, etc.). The API and frontends run natively during development for faster iteration.

Development Compose File

The development docker-compose.yml spins up four services. Start them all with a single command:

terminal
$ docker compose up -d
docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    container_name: myapp-postgres
    restart: unless-stopped
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: grit
      POSTGRES_PASSWORD: grit
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U grit"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

  minio:
    image: minio/minio
    container_name: myapp-minio
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    volumes:
      - minio-data:/data
    command: server /data --console-address ":9001"

  mailhog:
    image: mailhog/mailhog
    container_name: myapp-mailhog
    restart: unless-stopped
    ports:
      - "1025:1025"
      - "8025:8025"

volumes:
  postgres-data:
  redis-data:
  minio-data:

Service Details

ServicePort(s)CredentialsPurpose
PostgreSQL5432grit / gritPrimary database
Redis6379No authCache, sessions, job queues
MinIO9000 / 9001minioadmin / minioadminS3-compatible file storage
Mailhog1025 / 8025No authEmail testing (SMTP + Web UI)

Accessing Services

MinIO Console

http://localhost:9001

Web-based file browser for your S3-compatible storage. Create buckets, upload files, manage access policies. Login with minioadmin / minioadmin.

Mailhog UI

http://localhost:8025

Catches all outgoing emails from your application. View HTML emails, check headers, and test email flows without sending real emails.

PostgreSQL

localhost:5432

Connect using any database client (pgAdmin, TablePlus, DBeaver). Connection string: postgres://grit:grit@localhost:5432/myapp?sslmode=disable

Redis

localhost:6379

Connect with redis-cli or any Redis GUI client (RedisInsight, Medis). No authentication required in development.

Production Compose File

The docker-compose.prod.yml builds and runs your entire application stack including the Go API, Next.js web app, and admin panel alongside PostgreSQL and Redis.

docker-compose.prod.yml
services:
  api:
    build:
      context: ./apps/api
      dockerfile: Dockerfile
    container_name: myapp-api
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      APP_ENV: production
      DATABASE_URL: postgres://grit:grit@postgres:5432/myapp?sslmode=disable
      REDIS_URL: redis://redis:6379
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    container_name: myapp-web
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NEXT_PUBLIC_API_URL: http://api:8080

  admin:
    build:
      context: .
      dockerfile: apps/admin/Dockerfile
    container_name: myapp-admin
    restart: unless-stopped
    ports:
      - "3001:3000"
    environment:
      NEXT_PUBLIC_API_URL: http://api:8080

  postgres:
    image: postgres:16-alpine
    container_name: myapp-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: grit
      POSTGRES_PASSWORD: grit
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U grit"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres-data:
  redis-data:

Deploy to production with:

terminal
$ docker compose -f docker-compose.prod.yml up -d --build

Dockerfiles

Go API (Multi-Stage Build)

The API Dockerfile uses a multi-stage build. The first stage compiles the Go binary with all dependencies, and the second stage copies only the binary into a minimal Alpine image. The final image is typically under 20MB.

apps/api/Dockerfile
# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server

# Run stage
FROM alpine:3.19

RUN apk --no-cache add ca-certificates tzdata

WORKDIR /app

COPY --from=builder /app/server .

EXPOSE 8080

CMD ["./server"]

Next.js (Standalone Build)

The Next.js Dockerfile also uses a multi-stage build. It installs dependencies, builds the app with standalone output, and runs the production server as a non-root user. Both the web and admin apps share this same Dockerfile pattern.

apps/web/Dockerfile
# Build stage
FROM node:20-alpine AS base

RUN corepack enable

# Install dependencies
FROM base AS deps
WORKDIR /app

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/web/package.json ./apps/web/
COPY packages/shared/package.json ./packages/shared/

RUN pnpm install --frozen-lockfile

# Build
FROM base AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
COPY . .

RUN pnpm --filter web build

# Run
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder /app/apps/web/public ./apps/web/public

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "apps/web/server.js"]

Common Commands

terminal
$ docker compose up -d

Start all development services in the background

terminal
$ docker compose down

Stop and remove all containers

terminal
$ docker compose down -v

Stop all containers and delete volumes (resets all data)

terminal
$ docker compose logs -f postgres

Follow logs for a specific service

terminal
$ docker compose logs -f

Follow logs for all services

terminal
$ docker compose ps

Show running containers and their status

terminal
$ docker compose restart redis

Restart a specific service

terminal
$ docker compose exec postgres psql -U grit myapp

Open a psql shell inside the PostgreSQL container

terminal
$ docker compose exec redis redis-cli

Open a Redis CLI session inside the container

Data Persistence

Docker named volumes keep your data safe across container restarts:

VolumeMounted ToContains
postgres-data/var/lib/postgresql/dataAll database tables and data
redis-data/dataCached data, sessions, job queue state
minio-data/dataUploaded files and bucket data

Use docker compose down -v to delete all volumes and reset to a clean state. This is useful when you want to start fresh or if your database schema has diverged.

Troubleshooting

Port already in use

Another process is using port 5432, 6379, etc. Stop the conflicting process or change the port mapping in docker-compose.yml. For example, change "5432:5432" to "5433:5432" and update your .env DATABASE_URL accordingly.

Container keeps restarting

Check the logs with "docker compose logs <service>". Common causes: incorrect credentials, corrupted volume data. Try "docker compose down -v && docker compose up -d" for a clean start.

Cannot connect from API to PostgreSQL

Ensure the API uses "localhost" (not the container name) when running outside Docker. The connection string in .env should be: postgres://grit:grit@localhost:5432/myapp?sslmode=disable

MinIO bucket not found

MinIO starts with no buckets. Open the MinIO console at http://localhost:9001, login with minioadmin/minioadmin, and create your bucket. Or set MINIO_DEFAULT_BUCKETS in the compose file.

Docker Compose V1 vs V2

Grit uses "docker compose" (V2, no hyphen). If you see errors, make sure Docker Desktop is updated. The old "docker-compose" (V1, with hyphen) is deprecated.