A beginner-friendly guide to using different API URLs for local development and Docker Compose, with clear environment file examples.

Keeping local development and Docker Compose in sync is mostly about the right API base URL. This guide shows the minimum you need to switch between localhost and Docker without surprises.

Architecture at a glance

Local
┌──────────┐     http://localhost:3000          http://localhost:8080/api
│ Browser  │ ───────────────────────────────▶  Web app  ───────────▶ Backend
└──────────┘

Docker Compose (containers on same network)
┌──────────┐     http://localhost:3000          http://backend:8080/api
│ Browser  │ ───────────────────────────────▶  Web app ─────────────▶ Backend
└──────────┘

Why two endpoints?

  • Local dev: The web app runs on http://localhost:3000 and calls the API at http://localhost:8080/api.
  • Docker Compose: Containers use the internal service name, so the web app calls http://backend:8080/api.

1) Environment files for local vs. Docker

Create separate env files so you never mix the URLs:

Local override (not baked into images): web-app/.env.local

WEB_APP_API_URL=http://localhost:8080
WEB_APP_API_VER=/api
NEXT_PUBLIC_BACKEND_API_BASE=http://localhost:8080/api

Docker/default (used in containers): web-app/.env

WEB_APP_API_URL=http://backend:8080
WEB_APP_API_VER=/api
NEXT_PUBLIC_BACKEND_API_BASE=http://backend:8080/api

Add .env.local to .dockerignore so local-only values never get baked into images.

2) App code picks the right base

In app/page.tsx, read the public env and fall back to localhost for safety:

const API_BASE =
  process.env.NEXT_PUBLIC_BACKEND_API_BASE ?? 'http://localhost:8080/api';

This keeps local dev working even if the file is missing.

3) Dockerfile and compose wiring

Pass the public env through the Docker build and runtime:

Dockerfile

ARG NEXT_PUBLIC_BACKEND_API_BASE
ENV NEXT_PUBLIC_BACKEND_API_BASE=${NEXT_PUBLIC_BACKEND_API_BASE}

docker-compose.yml

web-app:
  build:
    context: ./web-app
    args:
      WEB_APP_API_URL: ${WEB_APP_API_URL:-http://backend:8080}
      WEB_APP_API_VER: ${WEB_APP_API_VER:-/api}
      NEXT_PUBLIC_BACKEND_API_BASE: ${NEXT_PUBLIC_BACKEND_API_BASE:-http://backend:8080/api}
  environment:
    NEXT_PUBLIC_BACKEND_API_BASE: ${NEXT_PUBLIC_BACKEND_API_BASE:-http://backend:8080/api}

4) Local dev commands

From web-app/:

npm install
rm -rf .next   # only if you changed env files
npm run dev    # picks up .env.local automatically

Make sure the backend is running at http://localhost:8080/api (or update NEXT_PUBLIC_BACKEND_API_BASE).

5) Docker Compose commands

Use the Compose values (http://backend:8080/api):

docker compose up --build web-app
# on Apple Silicon (if no multi-arch images):
DOCKER_DEFAULT_PLATFORM=linux/arm64 docker compose up --build web-app

6) Avoiding the wrong env in containers

  • Keep .env.local in .dockerignore.
  • Rebuild after env changes: docker compose up --build web-app.
  • Check your shell exports; none should override NEXT_PUBLIC_BACKEND_API_BASE when running locally.

7) Quick checklist

  • Local: .env.local → localhost API, npm run dev.
  • Docker: .env + compose build args/env → http://backend:8080/api.
  • After changes: rebuild containers.
  • Connectivity: curl http://backend:8080/api/report-runs from inside a container; curl http://localhost:8080/api/report-runs locally.

FAQ

Why do we need two different API URLs?
On your laptop the browser talks to http://localhost:8080/api, but inside Docker each container uses service names on the Compose network, so the web app reaches the backend at http://backend:8080/api instead.
What happens if I forget to rebuild after changing env files?
The container will keep using the old values baked into its image. Run `docker compose up --build web-app` to rebuild so the new env is picked up.

Welcome to The infinite monkey theorem

Somewhere a monkey just typed Shakespeare in TypeScript. Be the first to read the masterpieces (and the hilarious misfires) landing on the blog.

Subscribe to The infinite monkey theorem

We fling fresh posts—no banana peels attached—straight to your inbox.