Recently, I was working through a custom Docker Compose configuration for a small Laravel application, one composed of three services:
- One container with Apache for serving static files. It includes Prefork MPM to provide the PHP runtime
- A second container with MariaDB to provide the application’s database
- A third container with Redis to provide cache support
Nothing too complex, right? Correct! However, there was a small problem that I didn’t know how to solve: How would the database be provisioned during deployment?
This wasn’t an issue when running the application locally.
All I needed to do, once the application had started, was to exec into the Apache/PHP container and use Laravel’s Artisan Console to run the database migrations, by calling
php artisan migrate.
However, during deployment to a remote environment, running the migrations manually is not practical. I need an approach that can be automated in a CI/CD pipeline.
You don’t need to do a lot
At first, I went down the completely wrong path. For some weird reason, I thought that I’d have to use a multi-stage build to build and provision the database, so that it would be ready when the database container had fully started..
The first stage would:
- Copy the Laravel source files and artifacts into the database image
- Install PHP along with the required extensions and binaries for Composer and MySQL (readline, zip, and git, etc)
- Mount Composer from the official Compose Docker Hub image and install the project’s dependencies
- Start MySQL
- Run the database migrations
The second stage would:
- Copy the MySQL data files (
/var/lib/mysql/data) from the first stage (leave everything else behind).
This approach seemed logical.
But it wasn’t.
Why? Because to provision the database required MySQL to be running. And doing so while building the image was not practical. What’s more, it wasn’t necessary, as I’ll show in just a moment!
However, the time invested initially was not for nothing. During that period, I learned that, with a lot less effort and hassle, the database migrations could be run by the Apache/PHP container during startup.
Specifically, all I needed to do was to override the CMD directive in the Apache/PHP container’s configuration.
Take a look at the last line of the Dockerfile, of the official Docker Hub PHP image, where you’ll see the following line:
This launches Apache in the foreground, where it listens for incoming requests.
What I needed to do was to run
php artisan migrate --env=deployment as well.
Just one command, instead of installing a series of tools and libraries, starting a database, installing PHP dependencies, booting MySQL, and so on.
For what it’s worth, yes, database migrations can be destructive. So it’s not, always, smart to run them in production. However, it seemed acceptable in this case.
How do you override an image’s command with Docker Compose?
You use the command attribute, which overrides the underlying image’s default command. You can do this either directly in docker-compose.yml or you can create a shell script and call that instead. Let’s look at both approaches.
Approach #1: Run Multiple Commands
This is likely the simplest and quickest approach to take, as you specify the commands directly in docker-compose.yml. What’s more, this approach lets you take full advantage of YAML’s block scalar styles.
In my case, all I did was add the following to the Apache/PHP container’s configuration:
command: - /bin/sh - -c - | php artisan migrate apache-foreground
The configuration above will, effectively, be converted into the following two commands:
/bin/sh -c php artisan migrate /bin/sh -c apache-foreground
Now, at the end of the container startup, the database’s migrations will run, then Apache will be launched.
Approach #2: Call a Shell Script
The previous approach is quick to implement. However, writing complex or sophisticated commands in YAML would be quite challenging — and likely a nightmare to read and debug.
That’s why it can be beneficial, depending on your use case, to use a shell script instead. Here are a couple more benefits of doing so:
- You can develop the script in your editor (or IDE) of choice
- A script is easier to debug
- A script is easier to maintain
- A script could be shared between projects
I ended up writing a script for the application, which you can see below.
#!/bin/bash set -m apache2-foreground & php artisan migrate --env=development fg %1
- Enables Bash’s job control functionality. This allows for suspending and resuming processes.
- Starts Apache in the background. That way, it can then use Artisan to run the database migrations to provision the database.
- After the migrations are complete, Apache is brought back into the foreground to handle requests until the container shuts down
Which approach makes most sense to you?
That’s how to override an image’s command with Docker Compose
While you might feel that you need to extend an image’s Dockerfile directly, you don’t need to. With Docker Compose’s command attribute, you can enhance — or completely change — what a container does when it starts.
What’s more, you can do so in a cleaner and more manageable way, one that won’t break your CI/CD pipeline when the underlying image is updated.
I’d love to hear your thoughts and feedback in the comments.