A Docker setup for running Craft CMS on Coolify.
A few people have asked whether I’ve managed to get Craft CMS running on Coolify, so I figure it might be helpful to share what I’ve got.
The project I’m running is only a small test site, but it’s been stable and happy. There are lots of ways to host a Craft site, and this is what I’d consider a bare minimum setup:
- PHP 8.4, along with the extensions Craft needs (plus ImageMagick and GD).
- A stable, independent queue runner so the AJAX-based
runQueueAutomaticallyoption can be disabled like nature intended. - Standalone MySQL and Redis containers. (Where you could just as easily use PostgreSQL.) -…
A Docker setup for running Craft CMS on Coolify.
A few people have asked whether I’ve managed to get Craft CMS running on Coolify, so I figure it might be helpful to share what I’ve got.
The project I’m running is only a small test site, but it’s been stable and happy. There are lots of ways to host a Craft site, and this is what I’d consider a bare minimum setup:
- PHP 8.4, along with the extensions Craft needs (plus ImageMagick and GD).
- A stable, independent queue runner so the AJAX-based
runQueueAutomaticallyoption can be disabled like nature intended. - Standalone MySQL and Redis containers. (Where you could just as easily use PostgreSQL.)
- Redis for cache and sessions.
I mentioned in an earlier post that I liked using base Docker images from Server Side Up. I’ve continued using them for all my Coolify-hosted PHP projects, and it’s taken a minimal amount of tailoring to be up and running.
I’m not a Docker expert, but I like them for a few reasons:
- They’re maintained by people that know PHP and Docker.
- They’re careful with permissions and defaults because they’re designed for production use.
- They’re customizable via well-documented environment variables and other patterns that are just plain handy, particularly if you use Laravel.
The Pieces
My Dockerfile uses their PHP 8.4 image, customizes some PHP settings and the document root, installs mysqldump, PHP extensions, and Node.js, then updates npm and Composer dependencies:
FROM serversideup/php:8.4-fpm-nginx
ENV PHP_OPCACHE_ENABLE=1
ENV PHP_MEMORY_LIMIT=1024M
ENV NGINX_WEBROOT=/var/www/html/web
ENV CRAFT_WEB_ROOT=/var/www/html/web
USER root
# Install mysqldump so control panel backups work
RUN apt-get update
RUN apt-get install -y lsb-release wget gnupg
RUN curl -sLO https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb
RUN dpkg -i mysql-apt-config_0.8.33-1_all.deb
RUN apt-get update
RUN apt-get install -y default-mysql-client
# Install PHP extensions
RUN install-php-extensions bcmath gd imagick intl
# Install Node.js v20
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
RUN apt-get install -y nodejs
# Tidy up
RUN apt-get clean all
COPY --chown=www-data:www-data . /var/www/html
USER www-data
RUN npm install
RUN npm run build
RUN composer install --no-interaction --optimize-autoloader --no-dev
That Dockerfile is used for the app and queue services I define in the Docker Compose configuration I give to Coolify:
# docker-compose.yaml
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./storage:/var/www/html/storage
healthcheck:
test: curl --fail http://localhost:8080/healthcheck || exit 1
interval: 10s
retries: 5
start_period: 10s
timeout: 10s
queue:
build:
context: .
dockerfile: Dockerfile
command: ["/usr/bin/nice", "-n 10", "php", "craft", "queue/listen", "--verbose"]
volumes:
- ./storage:/var/www/html/storage
healthcheck:
test: curl --fail http://localhost:8080/healthcheck || exit 1
interval: 10s
retries: 5
start_period: 10s
timeout: 10s
This means that the Coolify app has an always-running container for handling web requests, and another one that’s always running the queue. (That command: line leaves it up to Docker to make sure php craft queue/listen is always running; we don’t need to get cron or supervisor involved!)
The sharp reader may be alarmed at this point to not see any mention of MySQL or Redis.
As I pointed out in my much longer Coolify post, I use Coolify’s GUI to establish persistent services—in this case MySQL and Redis. That way they’re always running independently of deployments, and backups are ridiculously easy to manage via Coolify. The working app layout looks like this:
Once those database containers are running, my Docker Compose services can nearly come to life and interact with them.
Three last things are important:
- The Docker Compose services need to be able to reach the standalone databases—which means visiting the Advanced menu end enabling Connect To Predefined Network.
- Craft needs to be able to apply project config changes and migrations. I used the General menu’s Post-deployment setting to run
php craft up --interactive=0on theappcontainer. - Environment variables! You’ll need to set those in order to configure Craft for the Coolify environment.
My environment variable set looks like this:
CRAFT_ALLOW_ADMIN_CHANGES=false
CRAFT_APP_ID=•••
CRAFT_BACKUP_ON_UPDATE=false
CRAFT_DB_DATABASE=default
CRAFT_DB_DRIVER=mysql
CRAFT_DB_PASSWORD=•••
CRAFT_DB_SERVER=•••
CRAFT_DB_USER=mysql
CRAFT_DEV_MODE=false
CRAFT_DISALLOW_ROBOTS=true
CRAFT_ENVIRONMENT=staging
CRAFT_RUN_QUEUE_AUTOMATICALLY=false
CRAFT_SECURITY_KEY=•••
PRIMARY_SITE_URL=•••
REDIS_HOSTNAME=•••
REDIS_PASSWORD=•••
REDIS_PORT=6379
I’m assuming you already use Redis, but if not you’ll want to follow Craft’s examples using it for cache and/or session storage.
Fine Print
It took me a while to figure out how to get mysqldump working, so for a while I only had Coolify’s backups.
The only oddity I haven’t solved is that re-indexing assets ends with a generic error: “There was a problem indexing assets.” This will also pop some “A server error occurred.” messages. Re-indexing does, however, complete successfully.
The Steps
Here’s how I set things up from start to finish.
Create Standalone Services
- Commit
Dockerfileanddocker-compose.yamlto the root of your project. - In Coolify’s GUI, navigate to Projects and click + Add. Enter a Name and optional Description and click Continue.
- Click the newly-created production environment to set it up.
- Click + Add New Resource.
- Choose MySQL. You can leave everything as you see it here, but grab the MySQL URL (internal) value because you’ll need to share details from that with Craft.
- Your production environment will now include the exited MySQL database. Click + New and choose Redis. Grab the Redis URL (internal) value, too.
- Navigate into each of those services and click Start. Each one should build and spin up and turn green, meaning you‘ve got MySQL and Redis ready for Craft!
- Import a MySQL backup from the MySQL service’s Import Backups section, providing a
.sqldump from another environment.
Create the App Services
- Once again from the production environment, click + New. Now we want to point Coolify to the Craft CMS project repository. I used Private Repository (with GitHub App) with an already-established GitHub connection.
- Choose the project repository from the dropdown menu and click Load Repository.
- Choose the appropriate branch.
- For Build Pack, choose Docker Compose. Coolify should detect
/as the Base Directory and confirm that the/docker-compose.yamlfile exists there. - Click Continue.
- Enter your site’s base URL in Domains for App. (Example:
https://my-craft-site.example.) - Click Save.
- Navigate to Advanced and check Connect To Predefined Network.
- Navigate to Environment Variables to populate those.
-
You’ll need to get the MySQL and Redis connection details from those earlier connection strings.
-
MySQL:
mysql://{username}:{password}@{hostname}:{port}/{database} -
Redis:
redis://{username}:{password}@{hostname}:{port}/{database}
- Click Deploy, and everything should be built and deployed successfully.
- You should be able to visit the site’s URL in a browser, but you’ll get “/var/www/html/storage isn’t writable by PHP. Please fix that.” We have to set those permissions as a one-time thing.
- From the Coolify application service, click Persistent Storage adn grab the Source Path value.
- From Coolify, click Servers and the one your app is on.
- Click Terminal.
- Enter
chown -R www-data:www-data {path-you-copied}.
- After you’ve started the container for the first time, find Post-deployment, enter
php craft up --interactive=0and specifyappin the Container Name field. Click Save, then Redeploy to confirm everything works as expected.
You should be able to visit your front end and control panel and have a fully-working Craft site!
Post-Setup Steps
There are a few things you might want to know if this setup is new to you.
First, database backups are wonderfully straightforward to set up from the MySQL service’s Backups section. They can be kept locally, rotated regularly, and optionally uploaded to remote storage. Be aware that Coolify keeps binary backups, which are different from what Craft dumps out of its backup utility. (Both types are simple to restore from Coolify’s GUI.)
Next, you can pop into the app service’s Logs section to see (or stream) output from the web and queue containers. This can help confirm that the queue is running and confirm that web requests are being handled properly, but Craft’s own logs are still going to be in its storage/logs/ directory unless you configured things differently.
You can run console commands by visiting the app service’s Terminal section and connecting to the app container.
If you want to get to Craft’s persistent storage on the host filesystem (not in the container), you’ll find it at /data/coolify/applications/{app-id}/storage.
The End
I’m not as comfortable or confident with this setup as I would be relying on Servd or a trusty Ploi or Forge VPS, but it’s been running smoothly and auto-deploying for a few months now and it’s been painless to keep up to date.
I’m sure a few steps could be smoothed out here, but I hope this was easy enough to follow. If you’ve got any questions, additions, or corrections please send me an email—I’d love to hear from you!
Update: I learned after publishing this article that Samuel Reichör published a Craft+Coolify tutorial that takes a slightly different approach I may need to steal ideas from!