Containerisation with Docker Compose: Development Environments That Match Production
If your development team has ever spent time debugging an issue that only appears in production, Docker Compose offers a practical path forward. By defining your complete application stack in a single YAML file, you can run the same services locally that your servers run in production. This removes the guesswork from environment differences and makes onboarding new developers faster.
What Docker Compose Actually Does
Docker Compose takes a declarative approach to defining multi-container applications. Instead of running several Docker commands manually, you describe your entire stack in a file called docker-compose.yml. This includes which services you need, how they connect, which ports they expose, and what environment variables they require.
When you run docker-compose up, Docker reads this configuration and builds the exact same environment every time. Whether a developer pulls the repository on macOS, Windows, or Linux, the stack behaves consistently. This consistency extends to matching your production environment as closely as possible, which reduces the surprises that typically appear when code reaches deployment.
Structure of a docker-compose.yml File
A well-structured Compose file starts with a version declaration, followed by a services section. Each service represents a container in your stack. Here is a straightforward example for a typical PHP application:
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./html:/usr/share/nginx/html
depends_on:
- php
php:
image: php:8-fpm
volumes:
- ./html:/var/www/html
database:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: app
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
This configuration defines three services: an Nginx web server, a PHP-FPM processor, and a MySQL database. The depends_on directive ensures that PHP starts after the database is ready. Named volumes persist database data between container restarts.
Running Your Stack Locally
After creating your Compose file, starting the environment takes a single command:
docker-compose up -d
The -d flag runs containers in detached mode, freeing up your terminal. To check what is running, use:
docker-compose ps
To view logs from all services:
docker-compose logs -f
When you are finished, bringing the stack down is equally straightforward:
docker-compose down
Adding the -v flag removes named volumes, giving you a completely clean slate. This is useful when you want to reset your database during development.
Managing Environment Differences
One of the practical challenges with Docker Compose is managing differences between local development and production. The standard approach is to use an override file. By default, Compose reads docker-compose.yml and then docker-compose.override.yml if it exists. Local-specific settings go in the override file, which typically stays out of version control.
For example, your base file might define a production-ready configuration, while your override file enables debugging tools, maps extra ports, or adjusts resource limits for local hardware.
Run with overrides enabled:
docker-compose up
Run with production-like settings, ignoring overrides:
docker-compose -f docker-compose.yml up
This separation keeps your production configuration clean and predictable while giving developers flexibility locally.
Connecting to External Services
Sometimes your stack needs to connect to a service that runs outside of Compose, such as a legacy database server or an external API. The external_links directive can connect containers to containers managed outside the current Compose project, though this should be used sparingly. A cleaner approach for external dependencies is to define them in your environment variables and reference them consistently across services.
Database Services and Persistence
Database containers require careful handling to prevent data loss. Always use named volumes for data directories. In the example above, db_data:/var/lib/mysql ensures that database files persist when containers restart or are rebuilt. Without this, every time you bring down the stack, your data disappears.
For production-like environments, consider running database containers with explicit health checks so that dependent services do not start before the database is ready to accept connections. You can define a health check condition in your service definition:
database:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
Your PHP service can then use depends_on with the condition option to wait for the database health check to pass before starting.
Rebuilding Containers After Configuration Changes
If you modify a Dockerfile or change an image requirement, you need to rebuild the affected service. Simply editing the Compose file does not trigger a rebuild automatically. Use:
docker-compose build service_name
docker-compose up -d service_name
To rebuild all services defined in your Compose file:
docker-compose build
docker-compose up -d
Adding the --no-cache flag forces a clean build without using the Docker build cache, which is useful when you suspect cached layers are causing unexpected behaviour.
Resource Limits and Performance
Running multiple containers on a development machine can consume significant memory and CPU. Compose allows you to set resource limits per service, which helps keep your system responsive:
php:
image: php:8-fpm
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 256M
These limits do not apply in all Docker contexts, but they are useful when running with Docker Swarm or in environments that enforce resource constraints.
Debugging Common Problems
If a service fails to start, checking the logs is the first step. Use docker-compose logs service_name to see what went wrong. Common issues include port conflicts, missing environment variables, and volume permission errors on Linux systems where directories are owned by root.
For permission issues, particularly with PHP writing to mounted volumes, you may need to set the correct user in your service definition or adjust the ownership of the host directory before starting the stack.
Network connectivity between services relies on Docker's internal DNS. Services can reach each other using the service names defined in your Compose file. If your application cannot connect to the database, verify that the hostname in your configuration matches the service name exactly.
Docker Compose in CI and Deployment Pipelines
Compose files are not limited to local development. Many teams use Docker Compose in their continuous integration pipelines to spin up a full test environment. This ensures that tests run against the same configuration that the application uses in production.
For deployment to production servers, Compose can be used directly on servers, though many teams prefer to use orchestration tools like Docker Swarm or Kubernetes for larger stacks. The Compose file format has become a standard, and tools like kompose can convert Docker Compose configurations into Kubernetes manifests.
Security Considerations
When using Docker Compose, there are a few security practices worth following. Avoid hardcoding sensitive values such as database passwords directly in your Compose file. Instead, use environment variables and pass them from a secure source, such as a CI secret or a dotenv file that is excluded from version control.
Regularly update the base images in your configuration to receive security patches. Outdated images with known vulnerabilities can expose your development and production environments to risk. Pulling updated images and rebuilding your services should be part of your regular maintenance routine.
Moving from Development to Production
The transition from a local Compose setup to a production environment requires planning. In production, you typically want to use specific image tags rather than latest, enable restart policies so containers recover automatically after server reboots, and centralise your logging to a service your team can monitor.
Using Docker Compose alongside a proper deployment tool gives you the consistency of your local configuration with the reliability your production environment demands. Many hosting providers and cloud platforms offer managed Docker services that accept Compose-style configurations, making the deployment process more straightforward.
If your team is considering moving towards containerised deployments or you need help reviewing your current setup, a practical assessment of your configuration files and deployment pipeline can identify the changes worth prioritising first.
Related practical reading
These related guides can help you connect this topic with the wider website, server, security, and support decisions around it.
- GraphQL in PHP vs REST: When GraphQL Is the Better Choice - useful background for related development decisions