Skip to main content

Drupal

Running Drupal here — the manifest, where updates run, and the settings file that reads the environment.

#The manifest

version: 1
type: drupal

runtime:
  php: '8.4'

services:
  - mariadb: '11.8'
  - valkey: '8'
  # Only for the theme build below; nothing of it runs beside the site.
  - node: '24'

build:
  steps:
    - composer install --no-dev --optimize-autoloader
    # The PHP image has no npm, so the theme builds in the Node image.
    - name: Theme
      image: node
      run: npm ci --prefix web/themes/custom/acme && npm run build --prefix web/themes/custom/acme

deploy:
  steps:
    - 'drush deploy'
  on_failure: rollback

cron:
  - name: drupal-cron
    schedule: '*/15 * * * *'
    command: 'drush cron'

Composer and npm are cached between builds without being asked for — there is no build.cache here because nothing else in this build writes a cache worth keeping. See Configuration for when to add one.

services is checked, not obeyed, for the database. A service such as Valkey or Node is started by the next deploy if the environment does not run it yet, but a database cannot be added or changed from a commit — moving means migrating everything in the old one. The database named here has to be the one the environment was created with, or the deploy refuses. Each name needs a version after it, and that version is what the environment runs from the next deploy — see Service upgrade before you change one that keeps data.

A step with no image runs in the application's own PHP image, which is what you want for Composer — the PHP that installs the dependencies is the PHP that runs them — and which has no Node in it. That is why the theme step names node, and why node is listed under services: a step can only run in an image the environment has.

The document root is web/, so the repository is expected to have the drupal/recommended-project layout, with composer.json at the top and Drupal under web/.

#Cron

With no cron in the manifest, the platform runs drush cron hourly. Declaring any cron replaces that job rather than adding to it, which is why the manifest above names drush cron itself — every fifteen minutes, because a site that sends digests or clears expired content usually wants it more often than once an hour.

Cron runs on one machine only, however many web servers the environment has, so a job never runs twice at once.

#Where updates run

drush deploy in deploy.steps, which is the one command that runs database updates, imports configuration and rebuilds the cache in the order Drupal wants them. Running drush updb and drush cim yourself works and is the same thing in more lines — the order matters, and drush deploy is the order.

It runs on the machine, after the release is live, not during the build. A build has no database: it produces an artifact that could be deployed to staging or production, and neither of their databases is its business. See vallic.yaml for what separates the two phases.

Nothing runs there unless you say so. With no deploy.steps, a deploy puts the new code live and stops — no updates, no configuration import — so a Drupal site needs drush deploy written down. On an environment with more than one web server the steps run once, on one of them, rather than once per machine.

When a backup is restored, the platform runs drush cache:rebuild afterwards, so the site does not serve pages cached from the database the restore replaced.

#The settings file

A Drupal site needs to know where its database is, where uploads go, what to salt its hashes with and which Host headers to trust — and every one of those differs between environments. On the platform they all come from the variables the environment carries, so a site reads them instead of committing them.

The file below does that. Copy it to web/sites/default/settings.vallic.php and include it from settings.php, last, so it wins over whatever the defaults above it say:

if (file_exists($app_root . '/' . $site_path . '/settings.vallic.php')) {
  include $app_root . '/' . $site_path . '/settings.vallic.php';
}

Nothing in it applies anywhere else. Under DDEV, on a laptop, on another host, VALLIC_ENVIRONMENT is not set and the file returns before touching a setting, so it can be committed and forgotten. The platform never edits it: it is your file, and the names it reads are the contract.

#The file

The control plane you are reading this in is a Drupal site on the platform, and this is the file it runs on — the handbook shows it rather than a copy that would drift from it.

sites/default/settings.vallic.php is not part of this build.

#What it decides

The database from DB_HOST, DB_PORT, DB_DRIVER, DB_NAME, DB_USER and DB_PASSWORD. The credentials were generated once with the environment; nothing about them is in the code.

The hash salt from VALLIC_ENTROPY, which is generated once per environment and never changes — so sessions and one-time login links survive a deploy, and the salt is never in a repository.

Files from VALLIC_PUBLIC_DIR and VALLIC_PRIVATE_DIR. Both are mounted into every release, so uploads outlive the code that received them. The public one is web/sites/default/files; the private one is private/ at the root of the repository, beside web/ rather than inside it, so it is never served. Everything else in the release is read-only while the site runs, which is why nothing but these directories should be written to.

Trusted hosts from VALLIC_HOSTNAMES: exactly the hostnames the edge routes to this environment, nothing else. Add a domain in the console and the pattern follows on the next reconcile.

The reverse proxy. The edge terminates TLS and forwards over plain HTTP, so the file trusts the address a request arrives from — which is only ever the edge — and reads the client's address, scheme and port from the X-Forwarded headers it sets. Without this, Drupal would build http:// links and log the edge as every visitor.

Redis when the stack has one (REDIS_HOST, whether it is Redis or Valkey) and the site ships the redis module. The module does not need to be installed for the cache to move there: its services are registered from the file, and the container cache lives in Redis from the first request.

The kind of environment from VALLIC_ENVIRONMENT_TYPE: production hides errors from visitors, everything else shows them, and an environment indicator is coloured accordingly if the site has that module.

Mail, when the stack runs a relay (SMTP_HOST). Drupal's default sends through a sendmail binary the container does not have, so the file points Symfony Mailer — 1.x or 2.x — at the relay on port 25 instead. A site that uses neither module is unaffected; one that sends through a hosted provider overrides it with its own transport.

Sessions need nothing. Drupal keeps them in the database, which every web server shares, so a visitor stays logged in whichever machine answers. The Redis block above moves the cache, not the sessions.

#What it leaves to you

Some settings are wiring the platform can name but a site has to place, because they live in a module's own configuration rather than in $settings. Add these to the same file as the site needs them.

Solr, when the stack runs it. The search_api server is configuration, so override it with the host and the login the platform names. Solr refuses a request without the login, so the server needs the basic-auth connector:

if (getenv('SOLR_HOST')) {
  $solr = &$config['search_api.server.solr']['backend_config'];
  $solr['connector'] = 'basic_auth';
  $solr['connector_config']['host'] = getenv('SOLR_HOST');
  $solr['connector_config']['port'] = 8983;
  $solr['connector_config']['core'] = getenv('VALLIC_SLUG');
  $solr['connector_config']['username'] = getenv('SOLR_USER');
  $solr['connector_config']['password'] = getenv('SOLR_PASSWORD');
  unset($solr);
}

Varnish, when the stack runs it. A purger needs the cache's address and the hostnames a purge applies to — both named by the platform. A purge sent to VARNISH_HOST from the site needs no key; one arriving from outside needs VARNISH_PURGE_KEY in an X-VC-Purge-Key header:

if (getenv('VARNISH_HOST')) {
  $config['varnish_purger.settings.YOUR_PURGER_ID']['hostname'] = getenv('VARNISH_HOST');
  $config['varnish_purger.settings.YOUR_PURGER_ID']['port'] = 6081;
}

Object storage, when a bucket is attached: the S3_FILES_* names feed the s3fs module's configuration in the same way. See storage.

The config sync directory is a project convention rather than a platform fact — ../config/sync on most projects — and stays in settings.php.