Skip to content

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.