ZeroStarter

Docker Deployment

Deploy ZeroStarter with Docker and Docker Compose.

Overview

ZeroStarter includes Docker configurations for both development and production deployments. This guide covers running the application with Docker Compose and building individual Docker images.

Quick Start

Run the entire stack with Docker Compose:

docker compose up

This starts both the frontend (Next.js) and backend (Hono) services:

Docker Compose Configuration

The docker-compose.yml file defines two services:

services:
  api:
    build:
      context: .
      dockerfile: api/hono/Dockerfile
      secrets:
        - dotenv
    env_file:
      - .env
    environment:
      - INTERNAL_API_URL=http://api:4000
    ports:
      - "4000:4000"

  web:
    build:
      context: .
      dockerfile: web/next/Dockerfile
      secrets:
        - dotenv
    env_file:
      - .env
    environment:
      - INTERNAL_API_URL=http://api:4000
    ports:
      - "3000:3000"

secrets:
  dotenv:
    file: .env

The dotenv build secret is required: both Dockerfiles mount it at build time (--mount=type=secret,id=dotenv,target=/app/.env,required=true), since env validation runs during the build.

Key Configuration

SettingDescription
context: .Build context is the repository root (for monorepo access)
env_file: .envLoads environment variables from root .env file
INTERNAL_API_URLEnables Docker networking: api uses it for Postgres URL rewrite (localhosthost.docker.internal), web uses it for server-side API calls

Building Individual Images

Both Dockerfiles need the dotenv build secret, so pass --secret id=dotenv,src=.env to docker build.

Backend (Hono API)

docker build --secret id=dotenv,src=.env -f api/hono/Dockerfile -t zerostarter-api .
docker run -p 4000:4000 --env-file .env zerostarter-api

Frontend (Next.js)

docker build --secret id=dotenv,src=.env -f web/next/Dockerfile -t zerostarter-web .
docker run -p 3000:3000 --env-file .env zerostarter-web

Image internals

Both Dockerfiles share the same shape:

  • Base image oven/bun:1.3.10-alpine.
  • Four stages: base -> prepare -> builder -> runner. The prepare stage runs turbo prune <workspace> --docker to produce a pruned, workspace-only build context, so each image installs and builds just the dependencies it needs.
  • The final runner stage runs as the non-root USER bun.

The web image additionally relies on Next.js output: "standalone": the builder copies .next/static and public into the standalone bundle manually, then the runner starts it with bun server.js. The api image just copies its bundle/ output and runs bun bundle/index.mjs.

Environment Variables

.env.example is the source of truth for the full variable set. Copy it to .env, fill in every value, and set NODE_ENV=production for a production build (the example ships with NODE_ENV=local).

cp .env.example .env

NEXT_PUBLIC_NODE_ENV is required by the web env schema but is derived automatically from NODE_ENV (it is not a separate line you set), so setting NODE_ENV covers it.

The .dockerignore excludes every .env* file except .env.example, so the build never bakes your real .env into the image. Secrets reach the build only through the dotenv build secret (mounted at /app/.env) and reach the running container through --env-file .env (or docker-compose env_file).

Internal Communication

When running with Docker Compose, services communicate using Docker's internal network:

  • The web service connects to api using http://api:4000 (service name resolution)
  • External clients connect using the mapped ports (localhost:3000, localhost:4000)

INTERNAL_API_URL serves multiple purposes:

  • Build time (web): Set via ENV in the web Dockerfile, baked into Next.js rewrites so /api/* routes proxy to http://api:4000
  • Runtime (web): Set via environment in docker-compose, used by server-side API client calls (SSR, server actions) to reach the api container directly
  • Runtime (api): Triggers Postgres URL rewrite (localhosthost.docker.internal) so the api container can reach the host database

Production Considerations

Database Connection

For production, use a managed PostgreSQL service:

This project requires PostgreSQL. MySQL-compatible providers like PlanetScale are not supported without substantial rework (schema migration, SQL/driver changes).

Migrations

Docker images do NOT run database migrations. Run bun run db:migrate separately before or alongside a rollout.

Migrations are applied only by the Vercel build, via .github/scripts/migrate-on-deploy.ts. The Docker image has no equivalent build step.

Container Registry

Push images to a container registry for deployment:

# Tag and push to Docker Hub
docker tag zerostarter-api username/zerostarter-api:latest
docker push username/zerostarter-api:latest

# Or use GitHub Container Registry
docker tag zerostarter-api ghcr.io/username/zerostarter-api:latest
docker push ghcr.io/username/zerostarter-api:latest

Health Checks

The API includes a health check endpoint:

curl http://localhost:4000/api/health

Add health checks to your Docker Compose for production:

services:
  api:
    # ... other config
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Development with Docker

For development, you might prefer running services locally with bun dev for hot reloading. Use Docker Compose primarily for:

  • Testing production builds locally
  • CI/CD pipelines
  • Consistent environments across team members
  • Deploying to container orchestration platforms (Kubernetes, ECS, etc.)

Troubleshooting

Port Already in Use

# Find and kill process using port 3000
lsof -i :3000
kill -9 <PID>

# Or use different ports
docker compose up -d
docker compose run -p 3001:3000 web

Build Cache Issues

# Rebuild without cache
docker compose build --no-cache

# Remove all containers and volumes
docker compose down -v

Environment Variable Issues

# Verify env vars are loaded
docker compose config

# Confirm the resolved .env values
docker compose run --rm api env