Deploy CraftCMS via DeployHQ on Hetzner Cloud

Open Source, PHP, Tips & Tricks, and Tutorials

Deploy CraftCMS via DeployHQ on Hetzner Cloud

Craft CMS is a flexible, developer-focused content management system built on PHP and Yii2. Unlike WordPress, Craft stores content as structured entries with custom fields, making it a strong fit for content-heavy sites that need editorial control without the plugin bloat.

This guide walks through deploying a Craft CMS site to a Hetzner cloud server using DeployHQ — from local development with DDEV, through server configuration, to automated zero-downtime deployments.


What You'll Need

Cost

Component Monthly Cost
Hetzner CX22 (2 vCPU, 4 GB RAM, 40 GB SSD) €4.35
IPv4 address €0.50
DeployHQ Free plan €0
Total €4.85/month

Step 1: Set Up Craft CMS Locally with DDEV

DDEV provides a Docker-based local development environment that mirrors your production server. If you don't have DDEV installed, follow the DDEV installation guide.

Create your project repository on GitHub (or GitLab/Bitbucket), clone it, then set up Craft:

git clone git@github.com:your-username/your-craft-project.git
cd your-craft-project

# Configure DDEV for Craft CMS
ddev config --project-type=craftcms --docroot=web --create-docroot

# Start the environment
ddev start

# Install Craft CMS via Composer
ddev composer create -y --no-scripts craftcms/craft

# Run the Craft installer (sets up admin account and database)
ddev craft install

# Verify it's working
ddev launch

You should see the Craft CMS welcome page. The admin panel is at /admin.

Export the Database for Initial Import

For the first deployment, you need to seed the production database with your local data:

ddev export-db -f craft-initial.sql.gz

Keep this file — you'll import it to your production server in a later step.

Configure Craft for Git-Based Deployments

Craft CMS supports Project Config, which stores your content model (fields, sections, entry types) as YAML files in config/project/. These files are tracked in Git and applied on the server during deployment — so structural changes flow through your deployment pipeline, not manual admin panel clicks.

Make sure your .env file (local) is in .gitignore:

echo ".env" >> .gitignore

Commit and push everything:

git add .
git commit -m "Initial Craft CMS setup"
git push origin main

Step 2: Provision the Hetzner Server

Log in to the Hetzner Cloud Console and create a server:

  • Location: Choose the region closest to your audience
  • Image: Ubuntu 24.04
  • Type: Shared vCPU, x86, CX22 (2 vCPU, 4 GB RAM, 40 GB SSD)
  • SSH Keys: Add your public key
  • Name: craft-production

Create a Deploy User

ssh root@YOUR_SERVER_IP

# Create a dedicated deploy user
adduser --disabled-password --gecos "" deploy
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

# Allow deploy user to restart PHP-FPM and Nginx
echo "deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart php8.3-fpm, /usr/bin/systemctl reload nginx" > /etc/sudoers.d/deploy

# Disable root SSH login
sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart sshd

Install the LEMP Stack

# Update packages
apt update && apt upgrade -y

# Nginx
apt install -y nginx

# PHP 8.3 and required extensions for Craft CMS
apt install -y php8.3-fpm php8.3-cli php8.3-mbstring php8.3-xml \
    php8.3-curl php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath \
    php8.3-mysql php8.3-pgsql php8.3-imagick

# MariaDB (Craft supports MySQL/MariaDB and PostgreSQL)
apt install -y mariadb-server

# Composer
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Certbot for SSL
apt install -y certbot python3-certbot-nginx

# Firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

Create the Database

mysql -u root << 'EOF'
CREATE DATABASE craft_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'craft_user'@'localhost' IDENTIFIED BY 'GENERATE_A_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON craft_db.* TO 'craft_user'@'localhost';
FLUSH PRIVILEGES;
EOF

Import the Initial Database

Copy the database dump from your local machine to the server:

# From your local machine
scp craft-initial.sql.gz deploy@YOUR_SERVER_IP:/tmp/

Then import it on the server:

gunzip -c /tmp/craft-initial.sql.gz | mysql -u craft_user -p craft_db

Step 3: Configure Nginx for Craft CMS

sudo tee /etc/nginx/sites-available/craft > /dev/null << 'NGINX'
server {
    listen 80;
    server_name your-domain.com;

    root /var/www/craft/current/web;
    index index.php;

    client_max_body_size 20M;

    # Craft CMS pretty URLs
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Block access to sensitive files
    location ~ /\.(env|git|htaccess) {
        deny all;
    }

    # Cache static assets
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
NGINX

sudo ln -sf /etc/nginx/sites-available/craft /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Note the root path uses /var/www/craft/current/web — this is the symlink path that DeployHQ's zero-downtime deployment creates.

Add SSL

Once your domain points to the server:

sudo certbot --nginx -d your-domain.com

Step 4: Create the Application Directory and .env

sudo mkdir -p /var/www/craft
sudo chown deploy:deploy /var/www/craft

# Create the production .env file
cat > /var/www/craft/.env << 'EOF'
CRAFT_APP_ID=your-app-id
CRAFT_ENVIRONMENT=production
CRAFT_SECURITY_KEY=GENERATE_A_SECURITY_KEY
CRAFT_DB_DRIVER=mysql
CRAFT_DB_SERVER=127.0.0.1
CRAFT_DB_PORT=3306
CRAFT_DB_DATABASE=craft_db
CRAFT_DB_USER=craft_user
CRAFT_DB_PASSWORD=YOUR_DB_PASSWORD
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://your-domain.com
EOF

chmod 600 /var/www/craft/.env

Important: CRAFT_ALLOW_ADMIN_CHANGES=false in production ensures structural changes can only come through Git (Project Config), not through the admin panel. This is a Craft CMS best practice for deployment workflows.

Generate a security key:

php -r "echo bin2hex(random_bytes(16)) . PHP_EOL;"

Step 5: Configure DeployHQ

Create a Project

Log in to DeployHQ and create a new project. Connect your Git repository.

Add Your Server

Go to ServersNew Server:

  • Name: Production
  • Protocol: SSH/SFTP
  • Hostname: Your Hetzner server IP
  • Username: deploy
  • Authentication: SSH Key (add DeployHQ's public key to the deploy user's authorized_keys)
  • Deployment Path: /var/www/craft
  • Zero-downtime deployments: Enabled

With zero-downtime deployments enabled, DeployHQ deploys to a new release directory and atomically switches the current symlink when everything is ready. Your site stays live throughout the entire deployment.

Configure the Build Pipeline

Go to Build PipelineNew Command. DeployHQ's build pipeline runs commands on DeployHQ's servers, so you don't need Composer installed on your production server.

Add a Composer build command:

composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader

Enable caching for vendor/** in Build ConfigurationCached Files to speed up subsequent deployments.

Add SSH Post-Deploy Commands

Go to SSH Commands and add a post-deploy command:

cd /var/www/craft/current

# Copy the .env from the parent directory (persists across releases)
ln -sf /var/www/craft/.env .env

# Apply database migrations and Project Config changes
php craft migrate/all --interactive=0
php craft project-config/apply --interactive=0

# Clear caches
php craft clear-caches/all

# Restart PHP-FPM to clear opcache
sudo /usr/bin/systemctl restart php8.3-fpm

This command:

  1. Symlinks the persistent .env file into the new release
  2. Runs any pending database migrations
  3. Applies Project Config changes (fields, sections, entry types) from the YAML files in Git
  4. Clears all Craft caches
  5. Restarts PHP-FPM to pick up new code

Use Config Files for Per-Environment Settings

If you deploy to both staging and production, use DeployHQ's config files to inject different .env values per server — different database credentials, CRAFT_ENVIRONMENT=staging, CRAFT_ALLOW_ADMIN_CHANGES=true for staging, etc.

Enable Automatic Deployments

Enable automatic deployments so every push to main triggers a deployment:

git push → DeployHQ → composer install → Deploy files → Migrate + apply config → Clear caches → Done

Step 6: Deploy and Verify

Trigger your first deployment from the DeployHQ dashboard. Once complete:

# Verify the symlink
ls -la /var/www/craft/current

# Check the site
curl -I https://your-domain.com

# Check Craft status
cd /var/www/craft/current && php craft app/health-check

Visit your domain — your Craft CMS site should be live.


Development Workflow

Once everything is set up, your daily workflow looks like this:

  1. Make changes locally in DDEV — edit templates, add fields, create sections
  2. Craft saves structural changes to config/project/*.yaml automatically
  3. Commit and push to Git: bash git add . git commit -m "Add blog section with categories" git push origin main
  4. DeployHQ deploys automatically — installs dependencies, transfers files, runs migrations, applies Project Config
  5. Production site is updated with zero downtime

Content (entries, assets) lives in the database and is managed through the Craft admin panel. Structural changes (fields, sections, settings) flow through Git and are applied automatically during deployment.


Common Questions

How do I handle uploads and assets?

Craft stores uploaded assets on disk by default. With zero-downtime deployments, each release gets a new directory — so you need to store assets outside the release directory. Configure an asset volume that points to a shared path:

# Create a shared assets directory
mkdir -p /var/www/craft/shared/assets

In Craft's admin panel (or config/volumes.php), set the asset volume's base path to /var/www/craft/shared/assets and the URL to /assets. Add a symlink in your SSH command:

ln -sf /var/www/craft/shared/assets /var/www/craft/current/web/assets

Can I use PostgreSQL instead of MariaDB?

Yes. Craft CMS supports PostgreSQL natively. Install postgresql instead of mariadb-server, create a PostgreSQL database, and update the CRAFT_DB_DRIVER and connection settings in your .env file.

What if a deployment fails?

With zero-downtime deployments, the current symlink still points to the previous release — your site stays live. Fix the issue, push again, and DeployHQ will create a new release. You can also roll back to any previous deployment from the DeployHQ dashboard.

How do I update Craft CMS itself?

Update Craft via Composer in your local DDEV environment:

ddev composer update craftcms/cms
ddev craft migrate/all

This updates the Composer lockfile and runs any new migrations locally. Commit the updated composer.lock and push — DeployHQ deploys the new version and runs migrations on the server automatically.

How do I deploy to staging and production?

Create two servers in DeployHQ. Map develop branch to your staging server and main to production. Use DeployHQ's config files to inject different .env values per environment, with CRAFT_ALLOW_ADMIN_CHANGES=true on staging so editors can make structural changes there, then export the Project Config YAML and merge it into main for production deployment.


The Bottom Line

Craft CMS with DDEV for local development, Hetzner for hosting, and DeployHQ for automated deployments gives you a professional, Git-based workflow for under €5/month. Structural changes flow through Project Config in Git, DeployHQ handles the deployment pipeline, and zero-downtime releases keep your site live throughout every deploy.

Start your free DeployHQ trial — no credit card required.

Written by

Facundo F

Facundo | CTO | DeployHQ | Continuous Delivery & Software Engineering Leadership - As CTO at DeployHQ, Facundo leads the software engineering team, driving innovation in continuous delivery. Outside of work, he enjoys cycling and nature, accompanied by Bono 🐶.