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.
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?
- Start the database manually
- Remember which port it uses
- Create data directories by hand
- Start Redis separately
- Hope your teammate copied the steps right
- 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
appis your actual project container. It builds from your Dockerfile and runs the development server.dbuses the official Postgres image, exposing port 5432 to your machine and storing data inpostgres_data.redisgives you a simple local cache or queue backend.
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.
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
app, db, redis, worker, not vague names that force people to guess.postgres:16 over floating tags when you care about consistency.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
dbas the hostname, notlocalhost. - 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
- Commit
compose.yamlto the repo. - Include a clean
.env.examplefile. - Document exactly one happy-path startup command.
- Keep optional services behind profiles.
- Use Makefile scripts or package scripts if your team likes shortcuts.
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.