BYOD Policy: Secure Personal Device Use at Work

9 min read 1,701 words
BYOD Policy: Letting Staff Use Personal Devices Without Compromising Security featured image

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.

Frequently Asked Questions

What is the difference between Docker and Docker Compose?
Docker manages individual containers, while Docker Compose manages multi-container applications. With plain Docker, you run separate commands for each container and manually define how they connect. Docker Compose lets you describe the entire stack in one file and control it with a single command. Compose is particularly useful when your application depends on several services, such as a web server, application runtime, database, and cache.
Can Docker Compose environments exactly match production?
Docker Compose can mirror your production setup closely, but some differences are unavoidable. These usually relate to operating system-level behaviour, kernel features, and resource availability. The goal is to minimise environment-specific bugs rather than achieve a perfect match. Using the same base images, service versions, and configuration values in both environments significantly reduces the gap.
How do I handle sensitive data in Docker Compose?
Sensitive data such as passwords, API keys, and tokens should not be stored directly in your Compose file. Use environment variables and load them from a secure source. In development, a .env file that is excluded from version control works well. In production, pass secrets through your CI system, a secret manager, or environment variables set by your hosting platform.
Should I use Docker Compose in production?
Docker Compose can be used in production for smaller workloads or single-server deployments. For larger systems requiring high availability, automatic scaling, and distributed scheduling, an orchestration platform like Docker Swarm or Kubernetes is more appropriate. Many teams start with Compose locally and move to a full orchestrator as their infrastructure grows.
How do I update images used in Docker Compose?
To update an image, change the tag in your Compose file or pull the latest version of the image directly using docker-compose pull. Then restart the affected services with docker-compose up -d service_name. If the service uses a custom Dockerfile, you need to rebuild with docker-compose build service_name before restarting.