SuperGeekery: A blog probably of interest only to nerds by John F Morton.

A blog prob­a­bly of inter­est only to nerds by John Morton.


Dock­er, Craft, Forge, Arcustech, and Heroku

I have recent­ly embraced using Dock­er for my local devel­op­ment. To be clear, I’ve had stum­bles along the way. Luck­i­ly, I have a friend who is always there to help me out. (Thanks, Andrew!) As with almost every­thing 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 some­thing out of it too.

Why do this?

Like many peo­ple in the Craft com­mu­ni­ty, I see what Andrew Welch is doing and think, dang, I want to do that too.” I expect if you’re read­ing this, you might be on the same jour­ney. If you haven’t done it yet, be sure to check out Andrew’s site, espe­cial­ly his arti­cle on Dock­er, An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment.

Local dev history

Many years ago, my local web dev envi­ron­ment was with MAMP. Over time, I switched to Valet. I do not want to throw any asper­sions in the direc­tion of either of these solu­tions. They help me a great deal while I used them.

Valet worked great for me until it did­n’t. I had numer­ous occa­sions where I need­ed to change a PHP ver­sion, install a mod­ule like ImageMag­ick, and some­thing would break with my Valet envi­ron­ment. A break like this would take down every local site, not just the one I was try­ing to work on. I start­ed to think I need­ed to reeval­u­ate my local dev solution. 

I ini­tial­ly thought about mov­ing to a Dock­er work­flow back in 2017. I found an online class, paid for it, and had every inten­tion of div­ing in imme­di­ate­ly. Then got I busy. Then years passed. I end­ed up stay­ing with Valet because it worked well enough for what I had to get done at the time.

When the Craft team intro­duced Nitro, I even­tu­al­ly start­ed using it. Nitro v1 worked for me most of the time, but I had instances where it would not sim­ply not work. Reboot­ing my com­put­er would even­tu­al­ly get it work­ing again, but I nev­er fig­ured out what the issue that caused it to fail. By that point, Nitro 2 was on the hori­zon and was going to be based on Dock­er. That sound­ed great to me but I’d already decid­ed I want­ed to fig­ure out a Dock­er work­flow for myself. If you’re just now start­ing though, be sure to check out Nitro 2. You’ll see the Nitro 2 instruc­tions begin with, First install Dock­er…” Nitro 2 might be the solu­tion that works for you. 

Start­ing with an exist­ing repo

I start­ed my Dock­er jour­ney by down­load­ing Andrew’s repo, https://​github​.com/​n​y​s​t​u​d​i​o​107​/​craft. This is his starter Craft project that runs in Docker.

This is a detailed, full-fea­tured project. For my uses, I ripped out stuff that I don’t use right now. For exam­ple, his web­pack build is part of this repo. I just com­ment­ed out those lines because the site that I brought over had a build sys­tem already. (Get­ting that work­ing is on my list.”)

To make mat­ters more com­pli­cat­ed, I’m on an M1 Mac and as I was start­ing this round of my Dock­er jour­ney, Dock­er for Apple Sil­i­con (aka the M1 chip) was still in beta and some pack­ages are not built for the M1 yet. Luck­i­ly, Andrew’s repo uses Mari­aDB for the data­base, but I had pre­vi­ous­ly start­ed with anoth­er build that used MySQL 8, and that build did­n’t work on the M1 chip. My ini­tial attempts at get­ting Dock­er run­ning end­ed in noth­ing but error mes­sages. I was frus­trat­ed but I pushed on.

As I write this post though, I’ve got it work­ing and I want­ed to share what I’ve learned along the way in case oth­ers might ben­e­fit from it as well. Since this has been about a 3‑week jour­ney, I will not cap­ture every twist and turn, but I’ll try to be as detailed as pos­si­ble. (Also, as I’m now edit­ing this post, Dock­er for Apple Sil­i­con has been offi­cial­ly released.)

Read your error messages

If I can tell you one tip upfront, it’s to read your error mes­sages. They are real­ly help­ful, but it does mean wad­ing through a lot of text in the ter­mi­nal. Embrace the ter­mi­nal. This is the game we’re playing.

My Dock­er workflow

Get­ting this blog work­ing in Dock­er was my first attempt at a real-world attempt at get­ting the Dock­er work­flow work­ing for me. Let me describe that work­flow though to set your expectations. 

I want to use Dock­er on my local machine to spin up a devel­op­ment envi­ron­ment. Once I’m hap­py with my local envi­ron­ment, I want to push it to Github in a staging branch. From there I want to deploy it from Github to my stag­ing serv­er. If I like how it’s work­ing on my stag­ing serv­er, 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 pro­duc­tion serv­er. Note that my stag­ing and pro­duc­tion servers have noth­ing to do with Dock­er. I’m using Dock­er only for local development. 

A rec­om­mend­ed Dock­er course

My ini­tial tin­ker­ing with Andrew’s repo left me a bit over­whelmed. Andrew rec­om­mend­ed a Dock­er course and it hap­pened to be the same one I ini­tial­ly bought back in 2017. As I men­tioned ear­li­er, I did­n’t actu­al­ly even start the course at the time I pur­chased it, but luck­i­ly my login cre­den­tials still worked. The course is called Dock­er Mas­tery. Here’s the URL: https://​www​.ude​my​.com/​c​o​u​r​s​e​/​d​o​c​k​e​r​-​m​a​stery There are 117 les­son videos. I’ve done about half of them and have enough knowl­edge to fig­ure out what I want­ed to accom­plish. I high­ly rec­om­mend­ed tak­ing this course. I did the ini­tial 60 lessons in about 3 – 4 days.

After the course, I dove back into using Andrew’s repo. With that course­work under my belt, his repo start­ed mak­ing more sense to me.

Craft and PHP versions

One issue I ran into was that my blog was still at Craft 3.5, mean­ing it was at was not com­pat­i­ble with PHP 8. I need­ed Craft 3.6 to be com­pat­i­ble with PHP 8. If you looked at Andrew’s repo, it uses PHP 8. With my recent­ly acquired knowl­edge, I knew I need­ed to change the PHP con­tain­er ver­sion. The way Andrew’s repo is set up, there are actu­al­ly two PHP con­tain­ers. One is for PHP with xde­bug turned on and one where there is no xde­bug. Switch­ing the PHP ver­sion is easy. In the Dock­er­file for each PHP con­tain­er, docker-config/php-dev-craft/Dockerfile and docker-config/php-prod-craft/Dockerfile, I com­ment­ed 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 old­er Craft ver­sion would work because the PHP con­tain­ers were back to a ver­sion it was com­pat­i­ble with. I could now log into Craft and update it to a ver­sion that was com­pat­i­ble with PHP 8. At that point, I was able to remove my changes to these Dock­er files.


For most of my projects, I use Lar­avel Forge to man­age servers. I had to make a change to my deploy­ment script on Forge to deal with the new struc­ture of my repo now that my Craft CMS would­n’t be liv­ing at the root direc­to­ry any­more. In the Dock­er work­flow, the cms direc­to­ry now con­tains all my appli­ca­tion’s files (i.e. all my Craft files). So my deploy­ment script need­ed to reflect this. See the fourth line below where I have it change to the cms directory:

 cd /home/forge/
 git pull origin main

 # Change to the 'cms' directory where the composer should be run from
 cd /home/forge/

 $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 ) 

 if [ -f artisan ]; then
    $FORGE_PHP artisan migrate --force

 ./craft migrate/all
 ./craft project-config/sync
 ./craft clear-caches/all

The oth­er thing I changed in Forge was the web direc­to­ry in the Meta tab. It needs to be updat­ed from /web to /cms/web.


I also use Arcustech for some clients. The update to my Arcustech work­flow hap­pened after the Forge work­flow process. I don’t have a web inter­face on Arcustech to help me deploy, but I do have a fan­cy deploy­ment script. I need to log into the Arcustech serv­er via SSH and run it.

I don’t have it well doc­u­ment­ed yet, but you can check out the script here: https://​gist​.github​.com/​j​o​h​n​f​m​o​r​t​o​n​/​a​f​d​d​d​a​967583​a​a​a​2​f​c​4​e​40​a​d​52​d​cea1b.

The com­ment at the begin­ning of the script tells as com­plete a sto­ry about it as I have writ­ten so far:

It will clone the git main branch from a pri­vate repo into a deployments direc­to­ry and then cre­ate sym­links for the sta­t­ic assets: .env, and 3 direc­to­ries of assets. It then does a com­pos­er install of the Craft site. The scripts in the com­pos­er file are like this file from Andrew’s repo These scripts update Craft, clear caches, etc. Final­ly, it will sym­link the web direc­to­ry in the new­ly down­loaded files to the pub­lic fold­er which is the one used by Arcustech.

There are com­ments through­out the script to help you cus­tomize it if you’d like. One thing my write-up does not men­tion is that the script keeps 3 total ver­sions of your site. That means in the­o­ry, rolling back to a pre­vi­ous ver­sion is as sim­ple as chang­ing the sym­link from the cur­rent­ly deployed ver­sion to the pre­vi­ous­ly deployed version.

Also, I have not writ­ten a script to revert to one of the pre­vi­ous ver­sions. That’s on the list” of to-dos. So, if you want to revert, you’ve got to man­u­al­ly update the sym­link to the live” direc­to­ry yourself. 🤷

The rea­son this is rel­e­vant to Dock­er is in line 54 of the script. The script changes into the cms direc­to­ry that my local Dock­er devel­op­ment expects. 

cd gitreponame/cms/


I also have some Craft sites on Heroku. I have a client that prefers Heroku, so that’s what we use. 

Get­ting Heroku to do its instal­la­tion from the cms direc­to­ry instead of the root direc­to­ry had me stumped for a while. I tried a vari­ety of things. I found Deploy­ing sub­di­rec­to­ry projects to Heroku which sug­gest­ed a cou­ple of solutions.

The first solu­tion sounds like it would have worked but also felt pret­ty icky to me was to use git subtree. You can read about it at the link above, but the sec­ond solu­tion sug­gest­ed in the post felt more right. Using a Heroku build­pack seemed like a bet­ter solu­tion to the Heroku problem.

When your app lives in a sub-directory

The arti­cle men­tioned search­ing the Heroku Ele­ments direc­to­ry for subdir. I vis­it­ed that link and looked at the options. The one I tried was https://​github​.com/​t​i​m​a​n​o​v​s​k​y​/​s​u​b​d​i​r​-​h​e​r​o​k​u​-​b​u​i​l​dpack and it did exact­ly what I need­ed. To use it, you need to have an envi­ron­men­tal vari­able by the name of PROJECT_PATH set. In my case, it is set to cms because that is the sub­di­rec­to­ry where Craft lives.

The subdir-heroku-buildpack needs to be the first build­pack in your chain of build­packs. This is impor­tant because it’s basi­cal­ly telling Heroku to go to that direc­to­ry and copy every­thing from the cms direc­to­ry to the root direc­to­ry before pro­ceed­ing. This will replace any­thing you have inside the root direc­to­ry of your repo before the reg­u­lar instal­la­tion process takes place. That means your Procfile should live inside your cms direc­to­ry of your repo. It gets moved to the root by the subdir-heroku-buildpack. That also means you leave the pub­lic direc­to­ry set to web, and not cms/web.

Let’s review that again. The Pro­file lives in the cms direc­to­ry in your repo and it points the web directory. 

Ini­tial­ly, I had the Pro­file in the root direc­to­ry, where a Pro­file is sup­posed to live, and had it point to the cms/web direc­to­ry like this.

web: vendor/bin/heroku-php-apache2 cms/web

Since the subdir build­pack moves the con­tents of the cms direc­to­ry, replac­ing every­thing in the root, my Proc­file was destroyed the process. That’s why in the repo it lives in the cms direc­to­ry 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 build­pack for PHP. That does not include mysqldump though. The Craft migra­tion process tries to back­up your data­base when it does a migra­tion in case there are errors, but if you don’t have mysqldump in your PATH it will fail. Well, in keep­ing with a theme, there’s a build­pack for that.

Nano, too, please?

While we’re at it, I con­fess that I’m not very good at VIM. I like nano. I’m a sim­ple 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 nano.

Here’s the order I have my build­packs in:

Heroku buildpacks


I have prob­a­bly left out stuff, but I hope this will help you on your Dock­er jour­ney. If you’ve got ques­tions about this, hit me up on Twit­ter @johnmorton.

Good luck!