Docker Compose Made Simple: Running Full Stacks Locally

Stop treating local setup like a scavenger hunt. With Docker Compose, you can boot your app, database, cache, and supporting services with one command, keep environments consistent across the team, and spend more time coding instead of fixing “works on my machine” issues.

Why Docker Compose is such a big deal locally

Most apps are no longer “just an app.” Even a modest project often needs a web server, a database, a cache, background workers, and sometimes an email sandbox or object storage emulator. Installing and coordinating all of that by hand is annoying, inconsistent, and fragile.

Docker Compose solves that by letting you describe your local stack in one YAML file. Instead of writing setup docs full of “install this, maybe use version X, then run these five commands,” you define the services once and run them the same way everywhere.

Consistency Everyone on the team uses the same service versions and startup rules.
Speed One command can bring up the entire stack.
Confidence CI and local environments become much easier to align.

The mental model: Compose is a recipe for your local environment

Compose is not magic. It is a structured way to define a set of containers that should run together. Each service gets its own configuration: image or build instructions, ports, environment variables, volumes, dependencies, restart behavior, and so on.

Think of a Compose file as the answer to a simple question: What services does this app need, and how should they talk to each other?

Without Compose
  • Start the database manually
  • Remember which port it uses
  • Create data directories by hand
  • Start Redis separately
  • Hope your teammate copied the steps right
With Compose
  • Define each service once
  • Commit the config to the repo
  • Run docker compose up
  • Let services share a network automatically
  • Get the same setup on every machine

A complete example: app + Postgres + Redis

Here is a practical Compose setup for a local web app. The app is built from your current directory, Postgres stores data in a named volume, and Redis is available for caching or queues.

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      DATABASE_URL: postgres://appuser:secret@db:5432/appdb
      REDIS_URL: redis://redis:6379
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      - db
      - redis
    command: npm run dev

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7
    ports:
      - "6379:6379"

volumes:
  postgres_data:

That is the core idea in one file. You define your services, map any ports you want available from the host, mount source code if you need live development, and persist state with named volumes.

What this file is doing

  • app is your actual project container. It builds from your Dockerfile and runs the development server.
  • db uses the official Postgres image, exposing port 5432 to your machine and storing data in postgres_data.
  • redis gives you a simple local cache or queue backend.
Important: inside Compose, services talk to each other by service name. That means your app should connect to Postgres using db, not localhost.

The commands you’ll actually use day to day

You do not need a giant command cheat sheet. A small handful covers most of local development.

# Start everything in the foreground
docker compose up

# Start everything in the background
docker compose up -d

# Rebuild images and start again
docker compose up --build

# Stop and remove containers
docker compose down

# Stop and remove containers, networks, and volumes
docker compose down -v

# View logs
docker compose logs

# Follow logs for one service
docker compose logs -f app

# Run a one-off command in a service container
docker compose exec app sh

# See running services
docker compose ps

Which ones matter most?

For most projects, your loop is simple: up -d, check logs, exec into the app if needed, and down when you’re done. That covers a surprising amount of real work.

Volumes, ports, and environment files without the confusion

Volumes

Volumes solve two different problems, and mixing them up creates a lot of beginner frustration.

  • Bind mounts map a folder from your machine into the container, like .:/app. These are great for live code editing.
  • Named volumes are managed by Docker, like postgres_data. These are ideal for persistent service data.
Use bind mounts for source code. Use named volumes for databases. That one rule prevents a lot of mess.

Ports

Port mappings follow the pattern HOST:CONTAINER. So 3000:3000 means “open port 3000 on my computer and route it to port 3000 in the container.”

If port 5432 is already taken on your machine, you can remap Postgres to something like 15432:5432 and still let your app talk to db:5432 internally.

Environment variables

Compose lets you define environment variables inline, but many teams prefer an external .env file for local values.

services:
  app:
    env_file:
      - .env
    environment:
      NODE_ENV: development

This keeps your Compose file cleaner and makes it easier to swap local settings without editing service definitions.

Small Compose habits that save a lot of time

Name services clearly. Use app, db, redis, worker, not vague names that force people to guess.
Keep dev commands explicit. Put your startup command in Compose so new teammates do not have to memorize it.
Use named volumes for state. Especially for databases, queues, and anything that should survive restarts.
Avoid baking source code into dev images. Bind mount your code for faster edits and rebuild less often.
Pin image versions. Prefer postgres:16 over floating tags when you care about consistency.
Separate dev and production concerns. Compose is amazing locally, but production deployments often need different patterns.

Use profiles for optional services

Sometimes you want admin tools like Mailpit, pgAdmin, or local object storage only when needed. Compose profiles let you keep them in the file without always starting them.

services:
  mailpit:
    image: axllent/mailpit
    ports:
      - "8025:8025"
    profiles:
      - tools

Then you can run them only when you want:

docker compose --profile tools up -d

How to debug common local Compose problems

“My app cannot connect to the database”

  • Make sure the app uses db as the hostname, not localhost.
  • Check that the database service is running with docker compose ps.
  • Inspect logs with docker compose logs db.

“Changes to my code are not showing up”

  • Verify that you mounted your project directory into the container.
  • Make sure your dev server actually supports watching files in containerized environments.
  • If needed, enable polling-based file watching for frameworks that struggle with mounted filesystems.

“The container starts and exits immediately”

  • Look at the service logs first.
  • Check whether the startup command fails right away.
  • Confirm required environment variables are present.

“Everything is weird; I want a clean reset”

Sometimes the right answer is to tear it down and start fresh.

docker compose down -v
docker compose up --build

That removes containers and named volumes, which is especially useful when local database state gets corrupted or stale.

A sane team workflow for Compose

The best Compose setups are boring in the best possible way. A new developer clones the repo, copies an example env file, runs one startup command, and gets to work. That should be the standard.

A simple pattern that works well

  1. Commit compose.yaml to the repo.
  2. Include a clean .env.example file.
  3. Document exactly one happy-path startup command.
  4. Keep optional services behind profiles.
  5. Use Makefile scripts or package scripts if your team likes shortcuts.
A great local environment is not the most clever one. It is the one the whole team can understand in five minutes.

When Compose is enough, and when it is not

Compose shines for local development, demos, integration testing, and small internal deployments. Once you move into heavier production orchestration, you may reach for platform tooling or orchestrators instead. But that does not reduce Compose’s value. In many teams, it is the fastest path to a reliable local stack.