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 upThis starts both the frontend (Next.js) and backend (Hono) services:
- Frontend: http://localhost:3000
- Backend: http://localhost:4000
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: .envThe 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
| Setting | Description |
|---|---|
context: . | Build context is the repository root (for monorepo access) |
env_file: .env | Loads environment variables from root .env file |
INTERNAL_API_URL | Enables Docker networking: api uses it for Postgres URL rewrite (localhost → host.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-apiFrontend (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-webImage internals
Both Dockerfiles share the same shape:
- Base image
oven/bun:1.3.10-alpine. - Four stages:
base->prepare->builder->runner. Thepreparestage runsturbo prune <workspace> --dockerto produce a pruned, workspace-only build context, so each image installs and builds just the dependencies it needs. - The final
runnerstage runs as the non-rootUSER 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 .envNEXT_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
webservice connects toapiusinghttp://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
ENVin the web Dockerfile, baked into Next.js rewrites so/api/*routes proxy tohttp://api:4000 - Runtime (web): Set via
environmentin docker-compose, used by server-side API client calls (SSR, server actions) to reach the api container directly - Runtime (api): Triggers Postgres URL rewrite (
localhost→host.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:latestHealth Checks
The API includes a health check endpoint:
curl http://localhost:4000/api/healthAdd 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: 3Development 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 webBuild Cache Issues
# Rebuild without cache
docker compose build --no-cache
# Remove all containers and volumes
docker compose down -vEnvironment Variable Issues
# Verify env vars are loaded
docker compose config
# Confirm the resolved .env values
docker compose run --rm api env