Docker provides an elegant way to define and manage multi-container applications. In this post, I’ll walk you through the Docker Compose configuration that builds the development environment for Willow CMS. This setup includes everything from the application server to development tools, making it easy to start developing right away. Get the code for Willow CMS here.

Core Services configured via docker-compose.yml

WillowCMS Application Server

The heart of our setup is the WillowCMS service:

willowcms:
  build:
    context: .
    dockerfile: docker/willowcms/Dockerfile
    args:
      - UID=${UID:-1000}
      - GID=${GID:-1000}
  ports: 
    - "8080:80"
  volumes:
    - .:/var/www/html/
    - ./docker/willowcms/config/app/cms_app_local.php:/var/www/html/config/app_local.php
    - ./logs/nginx:/var/log/nginx/
  environment:
    - REDIS_USERNAME=root
    - REDIS_PASSWORD=root

This configuration:

  1. Defines a service called willowcms
  2. Configures the service as a custom built container from our Dockerfile which will handle the installation and configuration of the container software
  3. Passes two named arguments UID (User ID) and GID (Group ID) which are used to ensure proper file permissions between the host and container.
  4. Maps port 8080 on the host to access the application on port 80 of the container
  5. Maps the entire project directory (.) from the host machine to /var/www/html/ inside the container. This allows you to use VS Code on the host to modify the source code and see instant resulting changes on the development server which hosts the code
  6. Maps the cms_app_local.php configuration file from the host to the container (with a new name inside the container. This allows us to maintain separate configuration settings for development without the risk of having the app configuration checked into the repo.
  7. Maps the nginx logs directory between the host and container, so we can view nginx logs directly from your host machine without having to enter the container.
  8. Defines 2 environment variables used to configure the redis service

You can access Willow CMS at http://localhost:8080 on your host machine.

I will dive into the details of the Dockerfile later. For now, know that it is responsible for installing and configuring Nginx and PHP-FPM and other software required to run Willow CMS (and any other CakePHP application).

Database Layer

We use MySQL 8.4.3 as our database:

mysql:
  image: mysql:8.4.3
  environment:
    MYSQL_ROOT_PASSWORD: password
  ports:
    - "3310:3306"
  volumes:
    - mysql_data:/var/lib/mysql
    - ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql

This configuration:

  1. Defines a service called mysql. This is important and the other containers can use mysql as the hostname for the MySQL service instead of an IP address.
  2. Uses the official Docker image of MySQL Server Community Edition v8.4.3 and configures the environment variable used to set the root password.
  3. Maps port 3310 on the host machine to 3306 on the container.
  4. Mounts a mysql_data folder which is managed by Docker into the container where MySQL stores all its database files. This allows us to persist data across different builds/instances of this container.
  5. Copies an initialization SQL script into the docker-entrypoint-initdb.d/init.sql directory within the container. This directory is pre-configured by the MySQL image to automatically execute any scripts it contains when the container is first initialized and is how the default and test databases and associated user accounts for the development environment are created.

You can access MySQL at http://localhost:3310 on your host machine using MySQL Workbench or something similar such as phpMyAdmin.

Development Tools

phpMyAdmin

For database management, I include phpMyAdmin:

phpmyadmin:
  image: phpmyadmin
  ports:
    - 8082:80
  environment:
    - PMA_HOST=mysql
    - PMA_USER=root
    - PMA_PASSWORD=password

You should see a pattern now, this configuration:

  1. Uses the official community maintained Docker image for phpMyAdmin
  2. Maps port 8082 on the host machine to port 80 on the container
  3. Configures the environment variables used within the image to configure the default host, user and password. Notice host is mysql the name we defined for the database service above.

Access phpMyAdmin at http://localhost:8082 for an easy-to-use database administration interface.

Jenkins CI/CD

Although right now I don’t use this container much in my development process, a Continuous Integration service is handled by Jenkins:

jenkins:
  build:
    context: .
    dockerfile: docker/jenkins/Dockerfile
  privileged: true
  user: root
  ports:
    - "8081:8080"
    - "50000:50000"
  volumes:
    - jenkins_home:/var/jenkins_home
    - /var/run/docker.sock:/var/run/docker.sock
    - ./docker/jenkins/jenkins.yaml:/var/jenkins_home/jenkins.yaml
  environment:
    - JAVA_OPTS=-Djenkins.install.runSetupWizard=false

This configuration:

  1. Defines a service called jenkins built using a DockerFile. We do this because I want to use configuration as code so that Jenkins is pre-configured with useful jobs and required software.
  2. Runs the container with elevated privileges (privileged: true) and as root user for full system access.
  3. Maps port 8081 on the host to Jenkins’ web interface port 8080 of the container and maps port 50000 for Jenkins agent communication.
  4. Uses a named volume ‘jenkins_home’ to persist all Jenkins data and configurations.
  5. Mounts the Docker socket on the host to allow Jenkins to create and manage Docker containers when running jobs.
  6. Provides initial configuration through jenkins.yaml using Jenkins Configuration as Code.
  7. Skips the Jenkins setup wizard by setting JAVA_OPTS environment variable (Jenkins is going to be ready to go with jobs out of the box!).

Access Jenkins at http://localhost:8081. In the future Jenkins will be used to run automated front-end tests in a browser. For now it has a job to run the PHPUnit tests on the main branch.

Email Testing with Mailpit

For email testing, since we don’t want to send real emails on the development environment, we use Mailpit to capture email from Willow CMS:

  mailpit:
    image: axllent/mailpit:latest
    ports:
      - "1125:1025"
      - "8025:8025"
    volumes:
      - mailpit_data:/data
    environment:
      - MP_MAX_MESSAGES=5000
      - MP_DATABASE=/data/mailpit.db
      - MP_SMTP_AUTH_ACCEPT_ANY=1
      - MP_SMTP_AUTH_ALLOW_INSECURE=1

This configuration:

  1. Creates a service called mailpit.
  2. Uses the latest official Mailpit Docker image (axllent/mailpit:latest).
  3. Maps the port 1125 on the host to Mailpit’s SMTP port 1025 on the container for capturing emails. This allows the willowcms container running the CakePHP code to send emails to mailpit:1025.
  4. Maps port 8025 on the host to Mailpit’s web interface port 8025 on the container.

Mailpit captures all outgoing emails and provides a web interface at http://localhost:8025 to view them.

Redis Management

Redis Commander provides a web interface for Redis management:

redis-commander:
  image: rediscommander/redis-commander:latest
  environment:
    - REDIS_HOST=willowcms
    - REDIS_PORT=6379
    - REDIS_PASSWORD=root
    - HTTP_USER=root
    - HTTP_PASSWORD=root
  ports:
    - "8084:8081"
  depends_on:
    - willowcms

This configuration:

  1. Uses the latest Redis Commander web interface image.
  2. Configures environment variables used in the image to connect by default to the Redis instance running on the ‘willowcms’ service at port 6379 with password root.
  3. Configures the web interface with HTTP basic authentication (username: root, password: root)
  4. Maps port 8084 on the host to Redis Commander’s web interface port 8081.
  5. Ensures the Redis service (willowcms) starts before Redis Commander.

Access Redis Commander at http://localhost:8084 to monitor and manage the WillowCMS Redis instance. It is used as the storage for the CakePHP Queue Plugin.

Persistent Storage

We define three volumes for data persistence:

volumes:
  mysql_data:
  rabbitmq_data:
  jenkins_home:

These volumes ensure that our data survives container restarts and rebuilds, although it doesn’t have to.

Accessing each Service

Here’s where to find each service once the environment is running:

Starting the Environment

Typically to start a Docker environment, you run:

docker-compose up -d

This will build and start all services in detached mode (meaning you can continue to use the terminal into which you typed the command) using the default docker-compose.yml file. However, I provide a handy script that gives you some options when it comes to starting/stopping the Docker development environment.

Instead of the above, run:

./setup_dev_env.sh

This script automates the management of the Docker development environment and the setup of Willow CMS. Here’s what it does:

First, it detects the operating system to determine whether sudo is needed for Docker commands (typically required on Linux but not on macOS). It then checks if the Docker containers for the project are already running, and if not, starts them using Docker Compose.

Once the containers are running, the script waits for the MySQL service to be ready by using the wait-for-it utility script. Note that the script is executed on the container hosting the willowcms service and uses the mysql host name. If I used the host mahine and port (127.0.0.1:3310) wait for it would assume MySQL was up an running as Docker will be listening on that host port before MySQL Server may have started to listen on it’s container port.

After MySQL is available, the script installs project dependencies using Composer within the Docker container.

The script then performs a check to see if the database has been previously set up by looking for a ‘settings’ table. This check is carried out by a custom CakePHP Command (more on those in a future blog post). If the table exists (indicating a previous setup), it presents the user with options to:

  • Wipe all data and start fresh
  • Rebuild the containers
  • Restart the environment
  • Continue with the current setup

If this is a first-time setup (or after a data wipe), the script performs initial setup tasks including:

  • Setting appropriate permissions on key directories
  • Running database migrations
  • Creating a default admin user
  • Importing default data into the database

Finally, regardless of whether it’s an initial or subsequent setup, the script clears the application cache before completing. This ensures a clean state for development work to begin.

Deep Dive - The Willow CMS Dockerfile Explained

Now you’re up to speed, lets take a dive into the Dockerfile used to build the container that hosts the Willow CMS codebase.

Base Image and Package Installation

The container starts with Alpine Linux v3.20, chosen for its small footprint and security. It installs a carefully selected set of packages including:

  • Redis for caching and queue management
  • Nginx as the web server
  • PHP 8.3 with essential extensions for CakePHP and Willow CMS
  • Supervisor for process management
  • ImageMagick for image processing
  • Development tools (curl, wget, unzip, bash)

Service Configuration

The Dockerfile sets up multiple services:

  1. Redis Configuration: Configures Redis with username/password protection and localhost binding. Environment configured in the docker-compose.yml are used here.
RUN echo "requirepass ${REDIS_PASSWORD}" >> /etc/redis.conf && \
    echo "bind 127.0.0.1" >> /etc/redis.conf && \
    echo "user ${REDIS_USERNAME} on >${REDIS_PASSWORD} ~* +@all" >> /etc/redis.conf
  1. Nginx Setup: Copies two configuration files:
# Configure nginx - http
COPY docker/willowcms/config/nginx/nginx.conf /etc/nginx/nginx.conf
# Configure nginx - default server
COPY docker/willowcms/config/nginx/nginx-cms.conf /etc/nginx/conf.d/default.conf
  1. PHP-FPM Configuration: Installs custom PHP configurations:
    • fpm-pool.conf for PHP-FPM process management
    • php.ini for PHP settings optimized for CakePHP

For easier maintenance we define a variable directly in the Dockerfile for the PHP_INI_DIR where the config files will go.

ENV PHP_INI_DIR /etc/php83
COPY docker/willowcms/config/php/fpm-pool.conf ${PHP_INI_DIR}/php-fpm.d/www.conf
COPY docker/willowcms/config/php/php.ini ${PHP_INI_DIR}/conf.d/custom.ini
  1. Composer Installation: Installs Composer for PHP dependency management. We don’t want to run composer from the host machine in the source code folder which is then mapped into the container. We could, but we will runn into file/folder permissions issues between the host and container that make things like cache generation in take a pain. Trust me, this is a better way to do this when coupled with setting up the user and groups permissions based on the host user ID and group ID which are set when running the docker-compose.yml or via the setup_dev_env.sh.

Permissions

The Dockerfile setups a user and group that helps with host/container permissions:

  1. Creates a non-root user (nobody) with configurable UID/GID taken from host machine user and their group.
  2. Sets appropriate ownership of web directories.
  3. Switches to the non-root user for running processes.
  4. Configures proper directory permissions for Nginx and PHP-FPM.
RUN deluser nobody && \
    addgroup -g ${GID} -S nobody && \
    adduser -u ${UID} -S -G nobody nobody

Process Management

Supervisord is used to manage multiple processes within the container:

  • Starts and monitors Nginx
  • Manages PHP-FPM
  • Handles Redis server
  • Ensures all services start in the correct order

We do this by copying the Supervisord config and setting the command for when the container is started.

Health Monitoring

The Dockerfile includes a health check that validates the container’s status by checking if PHP-FPM is responding. This is a nice to have that came from the production version of this Docker environment.

Conclusion

This Docker Compose configuration provides a complete development environment for Willow CMS. By containerising these services, we ensure consistency across different host machines and make it easy for new developers to get started with a known, working configuration. Whether you’re working on a new feature, testing email templates, or modifying the database, everything is just a port number away.

Tags

DockerCompose Docker CI Development CakePHP Infrastructure DevOps Dockerfile