Optimizing the deploy script of a Ddev-GitLab-cPanel workflow

In a recent post I talked about a Ddev-GitLab-cPanel workflow that removed most of the pain points and irritants related to the deployment of routine updates or new features to cPanel hosted websites.

In this post, I drill down into key details of the cPanel's repository architecture and deploy script (.cpanel.yml) for which I feel there isn't quite enough information to easily achieve an ideal and secure setup.

When creating a cPanel repo from the UI (cPanel Β» Tools Β» Files Β» Git Version Control), by default the Repository Path is /home/your-cpanel-username/repositories/repo-name. Of course, it's easy to change this to e.g. public_html/drupal1, which is what I did after reading through the docs and not finding any explicit warnings against that. I got it wrong! It took a while before I could fully realized that using a project root as a repository path was not best practice.

This 'wrong' had advantages: no code duplication, I could commit changes from Prod back to the remote repo and getting the deploy script to work was easy, so it seemed. But there was a somewhat invisible downside to this: it meant the .git and .ddev landed in the project root and that's not secure.

As it turns out, the suggested Repository Path i.e. /repositories/repo-name is both best practice and secure because it lies outside of public_html. Here's a before and after comparison:

Directory Structure Comparison

Development vs Production File Organization

❌ Not Best Practice
πŸ“public_html/
β”œβ”€β”€ πŸ“simple_html_site/
β”‚ β”œβ”€β”€ πŸ“.git
β”‚ β”œβ”€β”€ πŸ“css/
β”‚ β”œβ”€β”€ πŸ“files/
β”‚ β”œβ”€β”€.cpanel.yml
β”‚ β”œβ”€β”€.gitignore
β”‚ β”œβ”€β”€index.html
β”‚ └──…
β”œβ”€β”€ πŸ“drupal_site/
β”‚ β”œβ”€β”€ πŸ“.ddev/
β”‚ β”œβ”€β”€ πŸ“.git
β”‚ β”œβ”€β”€ πŸ“config/
β”‚ β”œβ”€β”€ πŸ“vendor/
β”‚ β”œβ”€β”€ πŸ“web/
β”‚ β”‚ └──.htaccess
β”‚ β”œβ”€β”€.cpanel.yml
β”‚ β”œβ”€β”€.gitignore
β”‚ β”œβ”€β”€composer.json
β”‚ β”œβ”€β”€composer.lock
β”‚ └──…
└──…

⚠️ Issues:

  • Development files (.git, .ddev) exposed in web directory
  • Security risk - version control accessible via web
  • Mixed development and production environments
βœ… Best Practice
πŸ“public_html/
β”œβ”€β”€ πŸ“simple_html_site/
β”‚ β”œβ”€β”€ πŸ“css/
β”‚ β”œβ”€β”€ πŸ“files/
β”‚ β”œβ”€β”€index.html
β”œβ”€β”€ πŸ“drupal_site/
β”‚ β”œβ”€β”€ πŸ“config/
β”‚ β”œβ”€β”€ πŸ“vendor/ (full)
β”‚ β”œβ”€β”€ πŸ“web/
β”‚ β”‚ └──.htaccess
β”‚ β”œβ”€β”€composer.json
β”‚ β”œβ”€β”€composer.lock
└──…
πŸ“repositories/
β”œβ”€β”€ πŸ“simple_html_site/
β”‚ β”œβ”€β”€ πŸ“.git
β”‚ β”œβ”€β”€ πŸ“css/
β”‚ β”œβ”€β”€ πŸ“files/
β”‚ β”œβ”€β”€.cpanel.yml
β”‚ β”œβ”€β”€.gitignore
β”‚ β”œβ”€β”€index.html
β”‚ └──…
β”œβ”€β”€ πŸ“drupal_site/
β”‚ β”œβ”€β”€ πŸ“.ddev/
β”‚ β”œβ”€β”€ πŸ“.git
β”‚ β”œβ”€β”€ πŸ“config/
β”‚ β”œβ”€β”€ πŸ“vendor/ (empty)
β”‚ β”œβ”€β”€ πŸ“web/
β”‚ β”‚ └──.htaccess
β”‚ β”œβ”€β”€.cpanel.yml
β”‚ β”œβ”€β”€.gitignore
β”‚ β”œβ”€β”€composer.json
β”‚ β”œβ”€β”€composer.lock
β”‚ └──…
└──…

βœ… Benefits:

  • Clean separation of development and production
  • No development files exposed to web
  • Enhanced security and organization
  • Full dependencies in production, minimal in development

Going for Best Practices means accepting some code duplication, though with a proper .gitignore strategy this quickly becomes a non-issue, really.

The biggest impact of this architecture change is that the deploy script (.cpanel.yml) becomes a bit more complex because it now has to take into account the fact that the cPanel's repository and the site's root are separate locations. Here's a partial example that illustrates this:

---
deployment:
  tasks:
    # Set variables
    - export REPO_PATH=/home/bisonble/repositories/drupal_site
    - export DEPLOYPATH=/home/your-cpanel-username/public_html/drupal_site
    - export LOG_PATH=/home/your-cpanel-username/public_html
    # Initialize custom deploy.log
    - echo "=== Deployment Started ===" > $LOG_PATH/deploy.log
    - echo "Deployment started at $(date)" >> $LOG_PATH/deploy.log
    - echo "Current directory $(pwd)" >> $LOG_PATH/deploy.log
    - echo "" >> $LOG_PATH/deploy.log
    # Copy files and directories
    - echo "=== Copying visible directories and files ===" >> $LOG_PATH/deploy.log
    - /bin/cp -R config recipes web composer.json composer.lock $DEPLOYPATH/
    - echo "" >> $LOG_PATH/deploy.log
    # Run commands
    - echo "=== Running Composer Install ===" >> $LOG_PATH/deploy.log
    - cd $DEPLOYPATH && /usr/local/bin/composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev >> $LOG_PATH/deploy.log 2>&1 || echo "Composer install failed" >> $LOG_PATH/deploy.log
    - echo "" >> $LOG_PATH/deploy.log
    - echo "=== Restoring Custom .htaccess ===" >> $LOG_PATH/deploy.log
    - echo "Current directory $(pwd)" >> $LOG_PATH/deploy.log
    - cd $REPO_PATH && /bin/cp web/.htaccess $DEPLOYPATH/web/.htaccess >> $LOG_PATH/deploy.log 2>&1 || echo "Failed to restore .htaccess" >> $LOG_PATH/deploy.log
    - echo "" >> $LOG_PATH/deploy.log
    - echo "=== Running Drush Deploy ===" >> $LOG_PATH/deploy.log
    - cd $DEPLOYPATH && vendor/bin/drush deploy >> $LOG_PATH/deploy.log 2>&1 || echo "Drush deploy failed with exit code $?" >> $LOG_PATH/deploy.log
    - echo "" >> $LOG_PATH/deploy.log
    # It's a wrap
    - echo "=== Deployment Complete ===" >> $LOG_PATH/deploy.log
    - echo "Deployment finished at $(date)" >> $LOG_PATH/deploy.log
    - echo "" >> $LOG_PATH/deploy.log

Final Notes

Deploying in the cPanel UI occurs in 2 separate steps, each step has its dedicated button:

  • Update from Remote: clicking this button adds a pull job to a queue; I usually wait 10 minutes just to make sure the system can complete this process;
  • Deploy HEAD Commit: clicking this button adds a deploy job to a queue; stay on the page until you get the green successful system message; this can easily take 90 seconds if e.g. you are deploying a Drupal core minor version update, don't forget that a deployment requires composer install and drush deploy to run in succession;

About debugging tools:

  • By default, the cPanel provides a very useful log @ e.g. /home/your-cpanel-username/.cpanel/logs/vc_1721761233.5389_git_deploy.log, make sure to check it out;
  • Also, the .cpanel.yml example provided above shows how you can create your own log to help you debug and fine-tune your script;

Common errors

  • Error: (XID blabla) β€œ/usr/local/cpanel/3rdparty/bin/git” reported error code β€œ128” when it ended: git@remote-repo.com: Permission denied (publickey). fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.
  • Solution: The error suggests the SSH key authentication isn't properly set up in the cPanel.
    • On cPanel server, check if private key exists: ls -l ~/.ssh/your-cPanel-key*
    • Verify SSH connection: ssh -Tv git@remote-repo.com -i ~/.ssh/your-cPanel-key
    • Ensure key permissions are correct: chmod 600 ~/.ssh/your-cPanel-key && chmod 700 ~/.ssh
    • Add to SSH agent: eval $(ssh-agent -s) && ssh-add ~/.ssh/your-cPanel-key
    • Create/edit ~/.ssh/config:
       Host remote-repo.com
         IdentityFile ~/.ssh/your-cPanel-key
         User git