Layer 1: Individual Docker Images
Run each service in its own Docker container separately to understand container isolation and networking.
Quick Start
Use scripts for automated setup:
# Start everything
./scripts/docker-run-local.sh
# Access services
# API: http://localhost:8000
# Web: http://localhost:4321
# Stop everything
./scripts/docker-stop-local.sh
See script documentation for details.
Manual Walkthrough (For Learning)
Build the API Image
docker build -t api:latest -f services/api/Dockerfile .
Verify:
docker images | grep api
Build the Web Image
docker build \
-t web:latest \
-f services/web/Dockerfile \
--build-arg PUBLIC_API_URL=http://localhost:8000 \
.
Note: PUBLIC_API_URL is compiled into the image at build time and cannot change at runtime.
Run Containers on a Shared Network
# Create network
docker network create local-platform
# Run API
docker run -d --name api --network local-platform -p 8000:8000 api:latest
# Run Web
docker run -d --name web --network local-platform -p 4321:4321 web:latest
Test
curl http://localhost:8000/health # API health
curl http://localhost:4321/ # Web loads
curl http://localhost:4321/status # Services communicate
If Web status shows "Backend is unavailable," the image was built with wrong PUBLIC_API_URL.
Fix: Rebuild Web with container-to-container address:
docker build \
-t web:latest \
-f services/web/Dockerfile \
--build-arg PUBLIC_API_URL=http://api:8000 \
.
docker rm web # Remove old container
docker run -d --name web --network local-platform -p 4321:4321 web:latest
Now Web can find API via service name discovery: http://api:8000.
The Container Networking Challenge
Container Network Isolation
Inside a container, localhost refers to the container itself, not the host machine.
Web is built with PUBLIC_API_URL=http://localhost:8000, so inside its container, it tries to reach localhost-where nothing is listening.
Solution: Rebuild Web with the correct internal address: http://api:8000 (service name on the Docker network).
This is why the same code behaves differently depending on where it's built. See Layer 2: Docker Compose for how Docker Compose automates this.
Network behavior summary:
- Containers use service names and Docker DNS to reach the API at
http://api:8000. - The browser uses localhost and port mappings to reach API and Web containers.
Key difference: Containers use service names and Docker DNS resolution. The host uses port mappings.
Configuration
| Variable | Behavior | How to Change |
|---|---|---|
PUBLIC_API_URL |
Compiled at build time; baked into image | Rebuild with new --build-arg |
LOG_LEVEL |
Runtime environment variable | docker run -e LOG_LEVEL=debug api:latest |
HOST, PORT |
Runtime or Dockerfile default | Override via -e at runtime |
For complete environment variable guide, see Environment Variables Guide.
Cleanup
docker stop api web && docker rm api web
docker network rm local-platform
Or use the stop script:
./scripts/docker-stop-local.sh
What Works / Doesn't Work
What Works
- Container isolation and security
- Production-like image builds
- Networking via service names
- Testing individual services
What Doesn't Work
- Manual container/network management (tedious)
- No health checks or startup ordering
- No automatic dependency resolution
- Gets complex with multiple containers
Next: Layer 2
Docker Compose automates network creation, health checks, and startup ordering. See Layer 2: Docker Compose.