A container can be easy to start once and hard to start the same way every time. A `docker run` command grows as you add ports, environment variables, volumes, and other options; several containers mean several commands to remember. Docker Compose puts those runtime choices in a YAML file, usually `compose.yaml`, so you can build and run the application with repeatable commands. It is a first step in container orchestration: making containers run as intended, initially by building and starting them. section 6.2.1 section 6.2
Start with a working application and Dockerfile. Under `services`, give each containerized application a service name. For a Python web application, `build.context` selects the build directory, `build.dockerfile` selects its Dockerfile, `ports` maps a host port to a container port, and `environment` sets variables. These replace arguments you would otherwise pass to `docker build` and `docker run`. Run `docker compose build` to build, `docker compose up` to start, and `docker compose down` to stop. For a build-and-start in one step, use `docker compose up --build`. section 6.2.2 section 6.2.1
Listing 6.3 Docker Compose for our Python app
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- PORT=8080
A small Python `compose.yaml` makes the translation concrete: one `web` service specifies its Dockerfile, port mapping, and `PORT` environment variable. section 6.2.2
As the application gains state, add a `volumes` entry to mount data into its container; `docker compose exec web` then runs commands inside that service without requiring its container ID. An `env_file` entry can supply variables from a `.env` file instead of putting their values in the Compose file. Keep sensitive `.env` files out of Git. Be careful with volume cleanup: `docker compose down -v` attempts to remove Compose-managed volumes, while a mounted local directory is not removed by Compose. section 6.3.2 section 6.4.3
Multiple services get a network for container-to-container communication. Within it, a service name acts as a hostname: a Python `web` container can request an NGINX service by name even when NGINX has no port published to the host. The same pattern lets a web application reach a Redis service using a host value supplied through `.env`. Host-facing port mappings and service-to-service addresses solve different problems. section 6.4.1 section 6.4.3
Listing 6.9 NGINX service in Docker Compose
services:
my-nginx-service:
image: nginx
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- PORT=8080
volumes:
- mywebdata:/app/data
volumes:
mywebdata:
The two-service configuration shows a built Python web container alongside an NGINX image. Only the web service publishes a host port; both services can use the Compose network. section 6.4.1
For another environment, select a different file with `docker compose -f compose.prod.yaml up`; a production file can choose a tagged image instead of a local build. On a virtual machine, install Docker Engine and the Compose plugin, then provide the configuration, images, and required environment values. A GitHub Actions deployment can copy the production Compose file and `.env` over SSH, run `docker compose pull`, and start containers with `docker compose up -d`. The plugin uses `docker compose`; older installations used `docker-compose`. section 7.2.2 section 7.3.3 section 7.1.3
Compose works well for a handful of development or production services, but it is not a complete answer to scaling. Its stated limits include easy replication of one service, health assessment beyond whether a container runs, remote runtime access without SSH, multi-VM deployment, and native ingress or public-IP provisioning. Duplicating services behind NGINX can provide load balancing, but adds configuration rather than removing those limits. section 7.4 section 7.4.1














