Modern Drupal is a Composer project with configuration in YAML, and its deployment should be a Git push followed by four Drush commands, not an FTP session. Most shared hosts cannot give you that; a platform environment can. This post follows a Drupal developer building the workflow on MassiveGRID PaaS, which is built on Virtuozzo Application Platform, formerly Jelastic, and then scaling the result when traffic arrives.
The developer builds sites for a non-profit federation we will call Riverbank Alliance, whose flagship Drupal 10 site runs campaigns that occasionally go viral. The Drupal package installs Drupal via Composer with Drush available, on an NGINX PHP application server with OPcache and the required extensions, plus a MariaDB database.
The repository is the site
Riverbank's repository is a standard Composer-managed Drupal project: composer.json and the lock file, custom modules and theme under web/modules/custom and web/themes/custom, and the exported configuration under config/sync. Contributed modules and core are not committed; Composer installs them from the lock file. The developer connects the repository to the PHP layer through the platform's Deployment Manager, deploying the main branch to the web root, and sets the document root to the project's web directory in the server's configuration.
Four Drush commands as a post-deploy hook
The Deployment Manager runs hooks before and after a deployment. The post-deploy hook is Drupal's canonical deployment sequence:
cd /var/www/webroot/ROOT
composer install --no-dev --optimize-autoloader
vendor/bin/drush deploy -y
drush deploy runs database updates, imports the configuration from config/sync, runs deploy hooks and rebuilds the cache, in the right order. A configuration change made in the site's admin UI is exported with drush config:export on a development clone, committed, and arrives on production through this hook; nobody changes production configuration by hand. The platform's zero-downtime deployment mode for PHP deploys into a new release directory and switches atomically, so a visitor never sees a half-installed vendor directory, and the hook runs against the new release before the switch completes.
Secrets (database credentials, API keys for the donation processor) are environment variables on the layer, read by settings.php; the settings file in the repository contains no credentials.
Drush over Web SSH, and cron
The platform's Web SSH console gives the developer a shell on the PHP node, where Drush works as on a laptop: drush cr, drush uli for a one-time login link, drush sql:dump before anything scary. Drupal's cron runs from the platform's cron editor as drush cron every fifteen minutes on the node, which is more reliable than Drupal's request-triggered automated cron and does not slow a visitor's page load. When the layer later has two nodes, the cron entry stays on the master node only.
Redis, before the traffic arrives
Drupal's cache and its render pipeline are database-heavy by default, and the single largest performance change for a content site is moving the cache bins to Redis. The developer adds a Redis node from the topology wizard, installs the Redis module via Composer, and configures it in settings.php as the default cache backend with the node's internal hostname. Anonymous page loads stop touching MariaDB for cached content, and the database node's CPU graph drops to a flat line. With Drupal's internal page cache and dynamic page cache on top, a campaign page is served from memory.
| Layer | Role | Change made |
|---|---|---|
| NGINX PHP (1 node, later 2) | Drupal, deployed from Git with drush deploy hook | ZDT deploy mode, cron on master node |
| Redis | Cache bins, lock, flood | Redis module, settings.php backend |
| MariaDB | Content and configuration | Backup add-on nightly |
| Shared storage (added with node 2) | public and private files directories | Mounted at the same path on every PHP node |
The campaign that made the news
When a Riverbank campaign was covered on national television, traffic rose thirtyfold in an hour. Vertical scaling carried the first wave: the PHP node grew from 8 to its 32-cloudlet limit (a cloudlet is 128 MiB of RAM plus 400 MHz of CPU) as PHP-FPM workers multiplied, and Redis absorbed the reads. The developer then did what she had rehearsed: scaled the PHP layer to two nodes from the dashboard. The platform added an NGINX load balancer automatically, the second node was created from the master with the deployed code, and the balancer began splitting requests. The one prerequisite she had prepared in advance was shared files: Drupal's public and private files directories live on a shared storage container mounted into every PHP node, so an image uploaded through one node appears on the other. Without that, half the visitors would have seen broken images.
Afterwards she added horizontal scaling triggers so the next campaign scales itself: add a node at 65% CPU for two minutes, remove at 20% for fifteen. The site normally runs one node again.
Development clones
Every feature branch gets a clone of production for the developer to build against real content, with the Deployment Manager pointed at the branch. Configuration is exported from the clone and committed; the clone is deleted after the merge. Because a clone copies the Redis and MariaDB nodes too, it is a faithful test of drush deploy against the real database, which is how the developer found a failing update hook on a clone rather than on the day of a campaign. The clones cost cents an hour and are the reason the workflow has no staging server.
Frequently Asked Questions
Can I keep using FTP or SFTP for quick fixes?
SFTP works on the node, but a file changed on the server is overwritten by the next deployment and is not in Git, so the workflow above avoids it. For a genuine emergency, fix on the node, then immediately commit the same change so the next deploy carries it.
How do I handle Drupal core and module updates?
Update the lock file with Composer on a clone (composer update drupal/core-recommended --with-dependencies), run drush deploy there, test, commit the lock file, and let the production deploy hook apply it. Security releases follow the same path within the day.
Where should the private files directory live?
Outside the web root, on the shared storage container when there is more than one PHP node, with the path set in settings.php. Keep it out of the deployment directory so a release switch never replaces it.
Push, deploy, cache, scale
The Drupal package on MassiveGRID PaaS gives you Composer, Drush and a tuned PHP server in one click. Deploy from Git with zero downtime, add Redis and a second node from the topology wizard, pay per cloudlet-hour. Free 14-day trial.
Drupal on MassiveGRID PaaS