Docker for Grit Developers
A practical guide to Docker for developers who want to use Grit. You don't need to be a Docker expert — just understand enough to start and stop the infrastructure services that power your Grit project.
What is Docker?
Think of Docker like a shipping container for software. Just as shipping containers let you move goods between ships, trucks, and warehouses without repacking, Docker containers let you run software on any computer without worrying about what's already installed there.
Without Docker, setting up a database like PostgreSQL means downloading an installer, configuring paths, managing versions, and troubleshooting OS-specific quirks. With Docker, you run a single command and get a fully working database in seconds — identical on every developer's machine.
This eliminates the "works on my machine" problem entirely. Every developer on your team gets the exact same PostgreSQL version, the exact same Redis configuration, and the exact same MinIO file storage — guaranteed.
In Grit
Docker runs your database (PostgreSQL), cache and job queue (Redis), file storage (MinIO), and email testing server (Mailhog). Your Go API and Next.js apps run natively on your machine for faster development — only the infrastructure services live in Docker containers.
Installing Docker
Docker Desktop is the easiest way to get started on Windows and macOS. It bundles the Docker engine, Docker Compose, and a graphical interface for managing containers. On Linux, you install Docker Engine and Docker Compose separately.
macOS
Download Docker Desktop from docker.com/products/docker-desktop and run the installer. Or use Homebrew:
Windows
Download Docker Desktop from docker.com/products/docker-desktop. Enable WSL 2 when prompted (recommended). Restart your computer after installation.
Linux (Ubuntu/Debian)
Install Docker Engine and the Compose plugin from the official repository.
macOS (Homebrew)
Linux (Ubuntu/Debian)
# Install Docker Engine$ curl -fsSL https://get.docker.com | sh# Add your user to the docker group (avoids needing sudo)$ sudo usermod -aG docker $USER# Log out and back in, then verify$ docker --version
Verify your installation
$ docker --versionDocker version 27.x.x, build abcdef0$ docker compose versionDocker Compose version v2.x.x
In Grit
Docker must be installed and running before you run grit new. The scaffolded project includes a docker-compose.yml that requires Docker Compose V2 (the docker compose command without the hyphen).
Images & Containers
An image is a blueprint — a read-only template that contains everything needed to run a piece of software: the operating system, the application, its dependencies, and configuration. Think of it like a recipe.
A container is a running instance of an image — the actual dish you cook from the recipe. You can run multiple containers from the same image, and each one is isolated from the others. When you stop a container, it's like throwing away the dish, but the recipe (image) remains for next time.
# Pull the official PostgreSQL image$ docker pull postgres:16-alpine# Run a container from that image$ docker run -d \--name my-postgres \-e POSTGRES_USER=grit \-e POSTGRES_PASSWORD=grit \-e POSTGRES_DB=myapp \-p 5432:5432 \postgres:16-alpine
In Grit
Grit uses official images from Docker Hub: postgres:16-alpine for PostgreSQL, redis:7-alpine for Redis, minio/minio for file storage, and mailhog/mailhog for email testing. The "alpine" variants are smaller, lightweight images based on Alpine Linux.
Dockerfile Basics
A Dockerfile is a text file with instructions for building a custom image. Each line is a step: choose a base image, copy files, install dependencies, and specify how to start the application. Here are the key instructions:
- FROM — Sets the base image (e.g.,
golang:1.21-alpine) - WORKDIR — Sets the working directory inside the container
- COPY — Copies files from your machine into the image
- RUN — Executes a command during the build (e.g., installing dependencies)
- EXPOSE — Documents which port the app listens on
- CMD — The command that runs when a container starts
A multi-stage build uses multiple FROM statements. The first stage compiles your code with all build tools, then the final stage copies only the compiled binary into a tiny base image. This keeps production images small and secure.
# ---------- Build stage ----------FROM golang:1.21-alpine AS builderWORKDIR /app# Download dependencies first (cached layer)COPY go.mod go.sum ./RUN go mod download# Copy source and buildCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server# ---------- Run stage ----------FROM alpine:3.19RUN apk --no-cache add ca-certificates tzdataWORKDIR /appCOPY --from=builder /app/server .EXPOSE 8080CMD ["./server"]
In Grit
Grit scaffolds two Dockerfiles: apps/api/Dockerfile builds the Go binary using a multi-stage build (final image is under 20MB), and apps/web/Dockerfile builds the Next.js app with standalone output. These are used by the production compose file, not during everyday development.
Docker Compose
Running individual docker run commands for each service gets tedious fast. Docker Compose solves this by letting you define all your services in a single docker-compose.yml file. One command starts everything.
A compose file defines services (containers to run), their images, port mappings, environment variables, volumes for data persistence, and dependencies between services. It's like a blueprint for your entire infrastructure.
services:postgres:image: postgres:16-alpineports:- "5432:5432"environment:POSTGRES_USER: gritPOSTGRES_PASSWORD: gritPOSTGRES_DB: myappvolumes:- postgres-data:/var/lib/postgresql/datavolumes:postgres-data:
In Grit
Every Grit project has a docker-compose.yml at the project root that defines all four infrastructure services. There's also a docker-compose.prod.yml for production that additionally builds and runs the Go API, web app, and admin panel as containers.
Grit's Docker Services
When you scaffold a new Grit project, the generated docker-compose.yml includes four services. Here's what each one does and why it's there:
PostgreSQL (port 5432)
Your primary relational database. Stores users, resources, and all application data. Grit uses GORM to auto-migrate your Go models into PostgreSQL tables. Default credentials: grit / grit.
Redis (port 6379)
An in-memory data store used for two things: caching API responses (via the cache middleware) and processing background jobs (via the asynq job queue). No authentication required in development.
MinIO (ports 9000 / 9001)
An S3-compatible object storage server. Handles file uploads like user avatars, documents, and images. Port 9000 is the API endpoint your Go code talks to. Port 9001 is a web console where you can browse files, create buckets, and manage storage. Login: minioadmin / minioadmin.
Mailhog (ports 1025 / 8025)
A fake SMTP server that catches all outgoing emails. Port 1025 receives emails from your Go API. Port 8025 is a web interface where you can view every email your app sent — perfect for testing password resets, welcome emails, and notifications without sending real mail.
services:# --- Primary Database ---postgres:image: postgres:16-alpinecontainer_name: myapp-postgresrestart: unless-stoppedports:- "5432:5432"environment:POSTGRES_USER: gritPOSTGRES_PASSWORD: gritPOSTGRES_DB: myappvolumes:- postgres-data:/var/lib/postgresql/datahealthcheck:test: ["CMD-SHELL", "pg_isready -U grit"]interval: 5stimeout: 5sretries: 5# --- Cache & Job Queue ---redis:image: redis:7-alpinecontainer_name: myapp-redisrestart: unless-stoppedports:- "6379:6379"volumes:- redis-data:/datahealthcheck:test: ["CMD", "redis-cli", "ping"]interval: 5stimeout: 5sretries: 5# --- S3-Compatible File Storage ---minio:image: minio/miniocontainer_name: myapp-miniorestart: unless-stoppedports:- "9000:9000"- "9001:9001"environment:MINIO_ROOT_USER: minioadminMINIO_ROOT_PASSWORD: minioadminvolumes:- minio-data:/datacommand: server /data --console-address ":9001"# --- Email Testing ---mailhog:image: mailhog/mailhogcontainer_name: myapp-mailhogrestart: unless-stoppedports:- "1025:1025"- "8025:8025"volumes:postgres-data:redis-data:minio-data:
In Grit
Start all four services with docker compose up -d. They'll run in the background. Your Go API connects to them via localhost and the ports listed above. All connection strings are pre-configured in the .env file.
Essential Commands
You only need a handful of Docker commands for day-to-day Grit development. Here's your cheat sheet:
| Command | What It Does |
|---|---|
| docker compose up -d | Start all services in the background (detached mode) |
| docker compose down | Stop and remove all containers (data is preserved in volumes) |
| docker compose logs | Show logs from all services |
| docker compose logs -f postgres | Follow (stream) logs for a specific service |
| docker ps | List all running containers |
| docker compose restart redis | Restart a specific service |
| docker compose down -v | Stop everything AND delete all data volumes (fresh start) |
| docker compose ps | Show the status of all compose services |
# Start your infrastructure$ docker compose up -d# Check that everything is running$ docker compose ps# When you're done for the day$ docker compose down
In Grit
In practice, you mainly need two commands: docker compose up -d to start your infrastructure at the beginning of a coding session, and docker compose down when you're done. Everything else is for troubleshooting.
Volumes & Data Persistence
By default, when you stop and remove a container, all its data disappears. Volumes solve this by storing data outside the container on your machine's filesystem. When you restart the container, the volume is re-attached and all your data is still there.
Docker supports two types of storage: named volumes (managed by Docker, recommended) and bind mounts (map a specific folder on your machine). Grit uses named volumes for all services because they're simpler and work the same on every operating system.
volumes:postgres-data: # Database filesredis-data: # Cache snapshotsminio-data: # Uploaded files
| Volume | Stores | Survives Restart? |
|---|---|---|
| postgres-data | All your database tables, rows, and indexes | Yes |
| redis-data | Cached data and job queue state | Yes |
| minio-data | Uploaded files and bucket metadata | Yes |
In Grit
Your PostgreSQL data persists across container restarts thanks to the postgres-data volume. If you need a completely fresh start (e.g., your schema is out of sync), run docker compose down -v to delete all volumes, then docker compose up -d to recreate everything from scratch. GORM will auto-migrate your tables on the next API startup.
Port Mapping
Containers run in their own isolated network. To access a service from your machine, you create a port mapping that connects a port on your computer (the host) to a port inside the container. The syntax is HOST:CONTAINER.
For example, "5432:5432" means "when something connects to port 5432 on my machine, route it to port 5432 inside the container." Your Go API code uses localhost:5432 to talk to PostgreSQL, and Docker transparently routes the traffic to the container.
ports:- "5432:5432" # HOST:CONTAINER# ^ ^# | |# | +-- Port inside the container# +-------- Port on your machine (localhost)# Your Go API connects to localhost:5432# which Docker routes to the container's port 5432
| Service | Mapping | Access From Your Code |
|---|---|---|
| PostgreSQL | 5432:5432 | localhost:5432 |
| Redis | 6379:6379 | localhost:6379 |
| MinIO API | 9000:9000 | localhost:9000 |
| MinIO Console | 9001:9001 | localhost:9001 (browser) |
| Mailhog SMTP | 1025:1025 | localhost:1025 |
| Mailhog UI | 8025:8025 | localhost:8025 (browser) |
Port busy? If a port is already in use, change the host side of the mapping. For example, change "5432:5432" to "5433:5432" and update the DATABASE_URL in your .env file to use port 5433.
In Grit
The .env file generated by grit new contains all connection strings pre-configured to point at these default ports. If you change a port mapping in docker-compose.yml, update the corresponding environment variable in .env to match.
Troubleshooting
Docker is generally reliable, but things can go wrong. Here are the most common issues and how to fix them:
Container won’t start
Check the logs to see what went wrong. Run docker compose logs <service> (e.g., docker compose logs postgres). Common causes include corrupted data or missing environment variables. If the logs mention data corruption, try docker compose down -v && docker compose up -d for a clean start.
Port already in use
Another process is using the port. On macOS/Linux, find it with lsof -i :5432 and kill it, or change the host port in docker-compose.yml (e.g., "5433:5432") and update your .env file. On Windows, use netstat -ano | findstr :5432 to find the process ID.
Out of disk space
Docker images and volumes accumulate over time. Run docker system prune to remove unused containers, networks, and dangling images. Add the -a flag to also remove unused images. Add --volumes to remove unused volumes (caution: this deletes data).
WSL issues on Windows
Docker Desktop on Windows requires WSL 2. If you see WSL-related errors, open PowerShell as admin and run wsl --update. Make sure WSL 2 is the default: wsl --set-default-version 2. Restart Docker Desktop after any WSL changes.
Permission errors on Linux
If you see "permission denied" when running docker commands, add your user to the docker group: sudo usermod -aG docker $USER. Then log out and back in (or run newgrp docker) for the change to take effect.
Docker daemon not running
On macOS/Windows, make sure Docker Desktop is open and running (check for the whale icon in your system tray). On Linux, start the daemon with sudo systemctl start docker. To make it start automatically: sudo systemctl enable docker.
# View logs for a specific service$ docker compose logs postgres# Follow logs in real-time$ docker compose logs -f redis# Check what's using a port (macOS/Linux)$ lsof -i :5432# Clean up unused Docker resources$ docker system prune# Nuclear option: remove everything and start fresh$ docker compose down -v && docker compose up -d
In Grit
For more detailed troubleshooting tips specific to Grit projects, see the Troubleshooting page in the Getting Started section. It covers API connection issues, frontend build errors, and other common problems beyond Docker.