Docker, Craft, Forge, Arcustech, and Heroku
I have recently embraced using Docker for my local development. To be clear, I’ve had stumbles along the way. Luckily, I have a friend who is always there to help me out. (Thanks, Andrew!) As with almost everything I post on this site, this post serves as a note to my future self, but I hope you (you, being you, not me) gets something out of it too.
Why do this?
Like many people in the Craft community, I see what Andrew Welch is doing and think, “dang, I want to do that too.” I expect if you’re reading this, you might be on the same journey. If you haven’t done it yet, be sure to check out Andrew’s site, especially his article on Docker, An Annotated Docker Config for Frontend Web Development.
Local dev history
Many years ago, my local web dev environment was with MAMP. Over time, I switched to Valet. I do not want to throw any aspersions in the direction of either of these solutions. They help me a great deal while I used them.
Valet worked great for me until it didn’t. I had numerous occasions where I needed to change a PHP version, install a module like ImageMagick, and something would break with my Valet environment. A break like this would take down every local site, not just the one I was trying to work on. I started to think I needed to reevaluate my local dev solution.
I initially thought about moving to a Docker workflow back in 2017. I found an online class, paid for it, and had every intention of diving in immediately. Then got I busy. Then years passed. I ended up staying with Valet because it worked well enough for what I had to get done at the time.
When the Craft team introduced Nitro, I eventually started using it. Nitro v1 worked for me most of the time, but I had instances where it would not simply not work. Rebooting my computer would eventually get it working again, but I never figured out what the issue that caused it to fail. By that point, Nitro 2 was on the horizon and was going to be based on Docker. That sounded great to me but I’d already decided I wanted to figure out a Docker workflow for myself. If you’re just now starting though, be sure to check out Nitro 2. You’ll see the Nitro 2 instructions begin with, “First install Docker…” Nitro 2 might be the solution that works for you.
Starting with an existing repo
I started my Docker journey by downloading Andrew’s repo, https://github.com/nystudio107/craft. This is his starter Craft project that runs in Docker.
This is a detailed, full-featured project. For my uses, I ripped out stuff that I don’t use right now. For example, his webpack build is part of this repo. I just commented out those lines because the site that I brought over had a build system already. (Getting that working is “on my list.”)
To make matters more complicated, I’m on an M1 Mac and as I was starting this round of my Docker journey, Docker for Apple Silicon (aka the M1 chip) was still in beta and some packages are not built for the M1 yet. Luckily, Andrew’s repo uses MariaDB for the database, but I had previously started with another build that used MySQL 8, and that build didn’t work on the M1 chip. My initial attempts at getting Docker running ended in nothing but error messages. I was frustrated but I pushed on.
As I write this post though, I’ve got it working and I wanted to share what I’ve learned along the way in case others might benefit from it as well. Since this has been about a 3‑week journey, I will not capture every twist and turn, but I’ll try to be as detailed as possible. (Also, as I’m now editing this post, Docker for Apple Silicon has been officially released.)
Read your error messages
If I can tell you one tip upfront, it’s to read your error messages. They are really helpful, but it does mean wading through a lot of text in the terminal. Embrace the terminal. This is the game we’re playing.
My Docker workflow
Getting this blog working in Docker was my first attempt at a real-world attempt at getting the Docker workflow working for me. Let me describe that workflow though to set your expectations.
I want to use Docker on my local machine to spin up a development environment. Once I’m happy with my local environment, I want to push it to Github in a
staging branch. From there I want to deploy it from Github to my staging server. If I like how it’s working on my staging server, I want to merge the changes from my
staging branch into the
main branch on my local machine, push
main to Github and then deploy to the production server. Note that my staging and production servers have nothing to do with Docker. I’m using Docker only for local development.
A recommended Docker course
My initial tinkering with Andrew’s repo left me a bit overwhelmed. Andrew recommended a Docker course and it happened to be the same one I initially bought back in 2017. As I mentioned earlier, I didn’t actually even start the course at the time I purchased it, but luckily my login credentials still worked. The course is called Docker Mastery. Here’s the URL: https://www.udemy.com/course/docker-mastery There are 117 lesson videos. I’ve done about half of them and have enough knowledge to figure out what I wanted to accomplish. I highly recommended taking this course. I did the initial 60 lessons in about 3 – 4 days.
After the course, I dove back into using Andrew’s repo. With that coursework under my belt, his repo started making more sense to me.
Craft and PHP versions
One issue I ran into was that my blog was still at Craft 3.5, meaning it was at was not compatible with PHP 8. I needed Craft 3.6 to be compatible with PHP 8. If you looked at Andrew’s repo, it uses PHP 8. With my recently acquired knowledge, I knew I needed to change the PHP container version. The way Andrew’s repo is set up, there are actually two PHP containers. One is for PHP with xdebug turned on and one where there is no xdebug. Switching the PHP version is easy. In the Dockerfile for each PHP container,
docker-config/php-prod-craft/Dockerfile, I commented out the
FROM 8.0 line and added a
FROM 7.4 line like this:
# FROM nystudio107/php-prod-base:8.0-alpine FROM nystudio107/php-prod-base:7.4-alpine
Now my older Craft version would work because the PHP containers were back to a version it was compatible with. I could now log into Craft and update it to a version that was compatible with PHP 8. At that point, I was able to remove my changes to these Docker files.
For most of my projects, I use Laravel Forge to manage servers. I had to make a change to my deployment script on Forge to deal with the new structure of my repo now that my Craft CMS wouldn’t be living at the root directory anymore. In the Docker workflow, the
cms directory now contains all my application’s files (i.e. all my Craft files). So my deployment script needed to reflect this. See the fourth line below where I have it change to the
cd /home/forge/example.com/ git pull origin main # Change to the 'cms' directory where the composer should be run from cd /home/forge/example.com/cms/ $FORGE_COMPOSER install --no-interaction --prefer-dist --optimize-autoloader ( flock -w 10 9 || exit 1 echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock if [ -f artisan ]; then $FORGE_PHP artisan migrate --force fi ./craft migrate/all ./craft project-config/sync ./craft clear-caches/all
The other thing I changed in Forge was the web directory in the Meta tab. It needs to be updated from
I also use Arcustech for some clients. The update to my Arcustech workflow happened after the Forge workflow process. I don’t have a web interface on Arcustech to help me deploy, but I do have a fancy deployment script. I need to log into the Arcustech server via SSH and run it.
I don’t have it well documented yet, but you can check out the script here: https://gist.github.com/johnfmorton/afddda967583aaa2fc4e40ad52dcea1b.
The comment at the beginning of the script tells as complete a story about it as I have written so far:
It will clone the git main branch from a private repo into a
deploymentsdirectory and then create symlinks for the static assets:
.env, and 3 directories of assets. It then does a composer install of the Craft site. The scripts in the composer file are like this file from Andrew’s repo These scripts update Craft, clear caches, etc. Finally, it will symlink the web directory in the newly downloaded files to the public folder which is the one used by Arcustech.
There are comments throughout the script to help you customize it if you’d like. One thing my write-up does not mention is that the script keeps 3 total versions of your site. That means in theory, rolling back to a previous version is as simple as changing the symlink from the currently deployed version to the previously deployed version.
Also, I have not written a script to revert to one of the previous versions. That’s “on the list” of to-dos. So, if you want to revert, you’ve got to manually update the symlink to the “live” directory yourself. 🤷
The reason this is relevant to Docker is in line 54 of the script. The script changes into the
cms directory that my local Docker development expects.
I also have some Craft sites on Heroku. I have a client that prefers Heroku, so that’s what we use.
Getting Heroku to do its installation from the
cms directory instead of the root directory had me stumped for a while. I tried a variety of things. I found Deploying subdirectory projects to Heroku which suggested a couple of solutions.
The first solution sounds like it would have worked but also felt pretty icky to me was to use
git subtree. You can read about it at the link above, but the second solution suggested in the post felt more right. Using a Heroku buildpack seemed like a better solution to the Heroku problem.
When your app lives in a sub-directory
The article mentioned searching the Heroku Elements directory for
subdir. I visited that link and looked at the options. The one I tried was https://github.com/timanovsky/subdir-heroku-buildpack and it did exactly what I needed. To use it, you need to have an environmental variable by the name of
PROJECT_PATH set. In my case, it is set to
cms because that is the subdirectory where Craft lives.
subdir-heroku-buildpack needs to be the first buildpack in your chain of buildpacks. This is important because it’s basically telling Heroku to go to that directory and copy everything from the
cms directory to the root directory before proceeding. This will replace anything you have inside the root directory of your repo before the regular installation process takes place. That means your
Procfile should live inside your
cms directory of your repo. It gets moved to the root by the
subdir-heroku-buildpack. That also means you leave the public directory set to
web, and not
Let’s review that again. The Profile lives in the
cms directory in your repo and it points the
Initially, I had the Profile in the root directory, where a Profile is supposed to live, and had it point to the
cms/web directory like this.
web: vendor/bin/heroku-php-apache2 cms/web
subdir buildpack moves the contents of the
cms directory, replacing everything in the root, my Procfile was destroyed the process. That’s why in the repo it lives in the
cms directory and looks like this:
web: vendor/bin/heroku-php-apache2 web
What about mysqldump?
In my case, I’m on Apache. I use the
heroku/php buildpack for PHP. That does not include
mysqldump though. The Craft migration process tries to backup your database when it does a migration in case there are errors, but if you don’t have
mysqldump in your PATH it will fail. Well, in keeping with a theme, there’s a buildpack for that.
Nano, too, please?
While we’re at it, I confess that I’m not very good at VIM. I like nano. I’m a simple guy! Let’s get nano on this Heroku box as well.
This way, when you log in with
heroku ps:exec -a my-app-name you can look at text file with
Here’s the order I have my buildpacks in:
I have probably left out stuff, but I hope this will help you on your Docker journey. If you’ve got questions about this, hit me up on Twitter @johnmorton.