Running Drupal here — the manifest, where updates run, and the settings file that reads the environment.
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/.
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.
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.
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 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.phpis not part of this build.
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.
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.