Deploying Craft CMS with DeployHQ
Craft CMS is a modern, developer-focused content management system built on PHP and the Yii2 framework. It uses Twig for templating, Composer for dependency management, and offers one of the strongest content modeling systems available in any CMS. Unlike WordPress or similar platforms, Craft treats content architecture as a first-class concern — custom fields, sections, entry types, and Matrix blocks give content teams genuine flexibility without plugin sprawl.
Deploying Craft CMS properly requires understanding its project structure, environment configuration, and migration workflow. This guide covers how to set up automated Craft CMS deployments with DeployHQ, including Project Config synchronization, Composer builds, and multi-environment handling.
Craft CMS Project Structure
A standard Craft installation follows a predictable directory layout:
my-craft-project/
├── config/ # Application configuration (general.php, db.php, custom configs)
├── modules/ # Custom Yii2 modules
├── storage/ # Runtime files, logs, rebrand assets (not committed to Git)
├── templates/ # Twig templates
├── web/ # Document root (index.php, cpresources/, uploaded assets)
│ └── cpresources/ # Auto-generated Control Panel assets
├── .env # Environment variables (never committed)
├── composer.json # Dependencies and plugins
├── composer.lock # Locked dependency versions
└── craft # CLI entry point
The key distinction for deployment: config/ and templates/ are version-controlled and deployed normally. storage/ and web/cpresources/ are runtime directories that must persist across deployments — they should never be overwritten by a fresh release.
Composer and Plugin Management
Craft manages all dependencies through Composer, including plugins. When you install a plugin through the Control Panel or CLI, Craft adds it to composer.json like any other package:
composer require craftcms/ckeditor
This means your composer.lock file is the single source of truth for your production dependencies. Always commit composer.lock to your repository and never run composer update in production — only composer install, which resolves from the lock file.
Your .gitignore should exclude the vendor/ directory entirely. Dependencies get installed during the build phase of each deployment.
Project Config and Content Migrations
Craft's Project Config system is one of its most powerful deployment features — and the one most teams initially get wrong. Project Config tracks structural changes to your Craft installation: field layouts, section definitions, entry types, plugin settings, user group permissions, and similar schema-level configuration. These are stored as YAML files in config/project/.
When you make changes in the Control Panel on your local environment (adding a field, modifying a section, configuring a plugin), Craft writes those changes to config/project/*.yaml. You commit these files to Git, and on deployment, the target environment applies them automatically.
This is how content migrations work in Craft — not through traditional migration files for structural changes, but through Project Config synchronization. The command to apply pending Project Config changes is:
php craft project-config/apply
For traditional database migrations (schema changes from plugins or custom modules), Craft uses a separate migration system:
php craft migrate/all
A production deployment should always run both commands, in this order: migrations first, then Project Config apply.
Important: Project Config handles structure, not content. Entry data, asset files, and user accounts are stored in the database and are not part of Project Config. If you need to move actual content between environments, you will need database syncing or Craft's built-in seed migration support.
Environment Configuration
Craft uses .env files for environment-specific values and PHP config files for application behavior. A typical .env file contains:
CRAFT_APP_ID=my-craft-site
CRAFT_ENVIRONMENT=production
CRAFT_SECURITY_KEY=your-unique-security-key
CRAFT_DB_DRIVER=mysql
CRAFT_DB_SERVER=127.0.0.1
CRAFT_DB_PORT=3306
CRAFT_DB_DATABASE=craft_production
CRAFT_DB_USER=craft
CRAFT_DB_PASSWORD=secret
CRAFT_DB_SCHEMA=public
CRAFT_DB_TABLE_PREFIX=
CRAFT_DEV_MODE=false
CRAFT_ALLOW_ADMIN_CHANGES=false
CRAFT_DISALLOW_ROBOTS=false
PRIMARY_SITE_URL=https://example.com
The config/general.php file references these environment variables and can define per-environment overrides:
use craft\config\GeneralConfig;
return GeneralConfig::create()
->defaultWeekStartDay(1)
->omitScriptNameInUrls()
->devMode(App::env('CRAFT_DEV_MODE') ?? false)
->allowAdminChanges(App::env('CRAFT_ALLOW_ADMIN_CHANGES') ?? false)
->disallowRobots(App::env('CRAFT_DISALLOW_ROBOTS') ?? false);
Setting CRAFT_ALLOW_ADMIN_CHANGES=false on production is critical. This prevents structural changes from being made directly in production — all changes must flow through Project Config from your development environment. Without this, you risk Project Config drift between environments that becomes painful to reconcile.
Multi-Environment Setup
A typical Craft deployment pipeline has three environments:
- Development (local):
CRAFT_ALLOW_ADMIN_CHANGES=true,CRAFT_DEV_MODE=true. All structural changes happen here. - Staging: Mirrors production configuration. Used to verify Project Config changes apply cleanly before going live.
- Production:
CRAFT_ALLOW_ADMIN_CHANGES=false,CRAFT_DEV_MODE=false. Receives deployments only through the automated pipeline.
Each environment has its own .env file with appropriate database credentials, site URLs, and feature flags. The application code and Project Config YAML are identical across all environments — only the .env values differ.
Setting Up DeployHQ for Craft CMS
Create Your Project
In DeployHQ, create a new project and connect your Git repository. Set the branch you want to deploy from (typically main or production).
Build Pipeline with Composer
Craft requires Composer dependencies to be installed during deployment. Create a .deploybuild.yaml file in your repository root to handle this in DeployHQ's build pipeline:
build:
- composer install --no-dev --optimize-autoloader --no-interaction
The --no-dev flag excludes development dependencies, --optimize-autoloader generates a classmap for better performance, and --no-interaction prevents Composer from waiting for input.
Environment Configuration via Config Files
Your .env file should never be in your repository. In DeployHQ, use the Config Files feature to manage environment-specific .env files. Create a config file entry with the path .env and populate it with your production environment variables. DeployHQ will place this file in the deployment directory on each release.
Zero-Downtime Deployments
Enable zero-downtime deployments in your server settings. This creates a new release directory for each deployment and symlinks a current/ directory to the latest release, so the switchover is atomic.
For Craft, you need to configure shared paths so that runtime data persists across releases. Add these as shared symlinks in your zero-downtime configuration:
storage/— runtime data, logs, and rebrand assetsweb/cpresources/— compiled Control Panel resources
Without shared paths, each deployment would start with an empty storage/ directory, losing logs and forcing Craft to regenerate all Control Panel assets.
If your site handles user-uploaded files stored locally (rather than on S3 or another remote volume), add your asset volume paths as shared symlinks too — for example, web/uploads/.
Post-Deploy SSH Commands
After each deployment, DeployHQ needs to run Craft's maintenance commands via SSH. Add these as post-deploy commands in your server configuration:
cd %deploy_path% && php craft migrate/all --no-interaction
cd %deploy_path% && php craft project-config/apply --no-interaction
cd %deploy_path% && php craft clear-caches/all --no-interaction
The order matters. Database migrations run first because Project Config changes may depend on updated database schema from plugin migrations. Project Config apply synchronizes structural changes. Cache clearing ensures the application starts fresh with no stale data.
Deployment Triggers
Set up automatic deployments by enabling the webhook for your repository provider (GitHub, GitLab, Bitbucket). Every push to your deployment branch will trigger a build and release.
Craft 5 Considerations
If you are running Craft 5 or planning an upgrade from Craft 4, the deployment workflow remains fundamentally the same. Craft 5 requires PHP 8.2 or later and introduces a revised configuration syntax using fluent config objects rather than arrays. The config/general.php example above already uses the Craft 5 syntax.
The most significant deployment-related change in Craft 5 is that allowAdminChanges is false by default in production environments. If you were relying on the old default and making structural changes in production, you will need to adjust your workflow to use Project Config properly — which is the recommended approach regardless of Craft version.
Ensure your DeployHQ server's PHP version meets the 8.2 minimum before deploying a Craft 5 project. Check your build environment supports the same version for Composer dependency resolution.
Troubleshooting Common Issues
"Error establishing database connection" after deploy: Verify your .env file has the correct CRAFT_DB_SERVER value. Many hosting providers use a specific hostname rather than 127.0.0.1.
Project Config changes not applying: Ensure php craft project-config/apply runs in your post-deploy commands. If it errors out, check for conflicts — two developers making structural changes simultaneously can create conflicting YAML files.
Blank page or 500 error: Enable CRAFT_DEV_MODE=true temporarily to see the full error. Usually a missing Composer dependency or PHP extension.
Control Panel assets missing: If styles or scripts are broken in the admin panel, clear the web/cpresources/ directory. This is a shared path — if misconfigured during zero-downtime setup, it may point to an old release directory.
Storage directory not writable: Craft needs write access to storage/. After your first deploy, run chown -R www-data:www-data storage/ on the server.
A well-configured Craft CMS deployment pipeline with DeployHQ handles the full lifecycle: Composer installs your dependencies, environment-specific .env files configure each target, zero-downtime releases prevent downtime, shared paths preserve runtime data, and post-deploy commands run migrations and sync Project Config automatically.
For more framework-specific walkthroughs, browse the DeployHQ guides library, or visit support for detailed documentation. Ready to automate your Craft deployments? Sign up for DeployHQ and connect your repository in minutes.
Questions? Reach out at support@deployhq.com or find us on Twitter/X.