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-dev-craft/Dockerfile
and 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.
Forge
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 cms
directory:
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 /web
to /cms/web
.
Arcustech
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
deployments
directory 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. :shrug:
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.
cd gitreponame/cms/
Heroku
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.
The 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 cms/web
.
Let’s review that again. The Profile lives in the cms
directory in your repo and it points the web
directory.
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
Since the 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.
https://github.com/daetherius/heroku-buildpack-mysql
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.
https://github.com/velizarn/heroku-buildpack-nano
This way, when you log in with heroku ps:exec -a my-app-name
you can look at text file with nano
.
Here’s the order I have my buildpacks in:
More?
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.
Good luck!