vallic.yaml — every key it takes, what each one does, and what a deploy refuses.
The file at the root of your repository. It is required: a deploy without one fails, naming the file.
That is deliberate. Without it the platform has to infer what your code needs, and the inference is silent when it is wrong — an environment running a database major your code cannot talk to is a query failing in production, weeks later, on a page nobody was looking at. The file turns that into a deploy that refuses with a reason.
version: 1
type: drupal
runtime:
php: '8.4'
memory_limit: 512M
services:
- mariadb: '11.8'
- valkey: '8'
- solr:
version: '9'
environment:
SOLR_HEAP: 1g
mounts:
- private/exports
cron:
- name: nightly-import
schedule: '0 3 * * *'
command: 'drush queue:run import'
workers:
- name: queue
command: 'drush queue:run heavy --time-limit=0'
replicas: 2
deploy:
steps:
- 'drush deploy'
on_failure: rollback
health:
path: /health
timeout: 120
env:
required:
- SENDGRID_API_KEY
Three categories, and the split is the point.
Application facts live here. Build steps, cron, the health path, the PHP version. They change with your code, in the same commit, reviewed alongside it and rolled back with it. A build command kept in a control panel is a deploy that can half-fail: the code arrives expecting one thing while the platform is still doing the other.
Infrastructure is chosen in the console. How big a machine is, which region it sits in, which services are provisioned. It costs money and needs somebody with the authority to spend it. In a repository, anyone who can push a branch could commit a machine that bills at many times what the one beside it does.
Services are declared here and started for you. Name a cache, a search
engine or a queue under services and the next deploy starts it — the file
travels with the code that needs it, so a branch that starts using Redis brings
Redis with it and the review of the commit is the review of the change.
What it may reach is bounded, because anybody who can push a branch can write this file. Only services this platform runs, only ones that fit your application's runtime, and never your database: moving one means moving everything in it, which is a migration rather than a setting. A manifest naming a different database refuses the deploy and tells you to talk to us.
Adding is all it does. Deleting a line does not delete a container — the file says what your code needs, not what your environment may keep.
env.required is the other half and provisions nothing: it names variables you
cannot start without, and the values live in the console, because a repository
is not where a credential belongs.
Every service you depend on, with the version you were written against. The version is required, and it is the version that runs — from the next deploy, matched to the current build of it the way a runtime version is. Changing it on a service that keeps data — a database, a search engine, the queue — is not always something that service survives: read Service upgrade first.
services:
- mariadb: '11.8' # a bare value is the version
- solr:
version: '9'
environment:
SOLR_HEAP: 1g
Both forms mean the same thing. Under a service name only version and
environment are understood — anything else is reported by name when the file
is read, rather than accepted and quietly ignored.
The service names are the ids in Software stacks: mariadb,
mysql, postgres, valkey, redis, memcached, solr, meilisearch,
rabbitmq, vinyl (Varnish), node (Node.js for builds), and the web bundle's
own php, nginx and opensmtpd when you want to give them settings.
Which variables you may set is per image, and most images allow none: nearly everything an image takes decides where it connects, what it is, or whether it starts. Each service's page says what it allows — see Software stacks.
A service you name is started on the next deploy, and the deploy waits for it: the machine is brought in line first, so your code lands on the stack it asked for rather than on the one that happened to be running. Naming one you already run changes nothing. Naming a different cache or search engine replaces the one running — a group that holds one thing holds the one you named, and what is lost is an index that reindexes or a cache that warms up.
The console's Services tab on each environment shows what is running,
which version, and which of them your vallic.yaml asked for.
| Key | What it is |
|---|---|
version |
The manifest format. 1. |
type |
What the application is — drupal, wordpress, laravel, symfony, php, nodejs (or node), golang (or go). It has to agree with the language your project runs; see below. |
runtime |
Which language version to run, and its settings. |
start |
The command that serves. Node and Go only — PHP-FPM is the process for every PHP framework. Required for Go; Node falls back to npm start. |
port |
What that command listens on. Defaults to 3000 for Node and 8080 for Go, and setting it moves both the PORT handed to your app and the port the platform reaches. |
services |
What runs beside the application. |
build |
steps to make the artifact, and cache to carry between builds. |
deploy |
steps to run once the release is live, and on_failure. |
workers |
Long-running processes the platform keeps up. |
cron |
Scheduled commands. |
mounts |
Directories that outlive a release. |
health |
How the platform decides the site is answering. |
env |
Variables the deploy refuses without. |
Anything else is refused rather than ignored, so a key with a typo in it is a deploy that says so rather than a setting that silently did nothing.
type does not pick your stack — the project type you chose in the console
does that, and decides the Nginx configuration, the default cron and the
directories kept between releases. What type is checked for is the
language. A build runs its steps inside your application's own container, so a
repository that says nodejs on a project whose machines run PHP has no npm
to run: the first step exits with "command not found".
The platform refuses that build rather than running it, and says so on the project page before you push.
The language is chosen when the project is bought, because it is what the machines were built for. You can still change it for as long as nothing has deployed: Configuration → Project → Change, where Application sits beside the name. The containers are rebuilt on the next reconcile, within the minute. Once a release is serving, changing it is a migration rather than a setting: ask support.
Commands run when the artifact is made, each in its own container on a build machine. This is where dependencies are installed and assets compiled.
build:
steps:
- composer install --no-dev --optimize-autoloader
- npm ci
- npm run build
The commands go under steps, not directly under build. A list written
straight under build: is refused and says so — it used to be accepted and
ignored, which meant a deploy that ran nothing and a site with no vendor
directory.
A step can also be a mapping, which is how you name one or run it in a different image:
build:
steps:
- name: Dependencies
run: composer install --no-dev --optimize-autoloader
- name: Theme
run: npm ci && npm run build
image: node
name is what the build log calls it. image runs that step in another
service's image — a PHP project whose theme needs Node asks for node here
rather than hoping the PHP image has it, because it does not. The image has to
be one your environment runs: node is Node.js for builds,
and a step naming something the environment does not have is refused, listing
what it does have.
A build has at most twenty steps, and twenty-five minutes for all of them.
Composer, npm, yarn and Go are already cached. The platform mounts a cache for each, per project, on every build — there is nothing to declare and nothing to configure. A second build does not download what the first one did.
build.cache is for directories your own build writes and would like back
next time:
build:
cache:
- .cache/turbo
- node_modules/.vite
steps:
- npm ci
- npm run build
Paths are relative and must be inside the repository — a cache outside it would be a way to write anywhere on the machine. They are restored before the steps run and kept after, and they are per project: a cache holds a private repository's packages, and another tenant's build must never be able to read them.
A cache is an optimisation, never an input. A build must work with an empty one, because the first build after a machine is replaced has exactly that.
Nothing is inferred. The platform does not look for composer.json and
guess — it ran the tool it guessed and was wrong in both directions: a
repository that commits its dependencies had an install run over it anyway, and
an application with an unusual build had no way to say otherwise. An empty
build is a complete instruction meaning "pack the checkout as it is", which
is what makes deploying a commit that needs no build just a push.
A build has no database and no environment of its own. The artifact it produces can be deployed to staging or production, and neither of their databases is its business. Anything that touches data belongs below.
Commands run on the machine, against the release, after its code is live. Deployments covers the whole sequence around them.
deploy:
steps:
- 'drush deploy'
on_failure: rollback
This is where database updates go — drush deploy for Drupal,
php artisan migrate --force for Laravel, doctrine:migrations:migrate for
Symfony. There is a worked manifest for each:
Drupal, WordPress,
Laravel, Symfony, and
Node and Go.
on_failure: rollback puts the previous release back if a step fails. Worth
setting: a migration that fails half-way leaves a site running new code against
an old schema, and the previous release is the only thing that definitely
works.
Each step is given twenty minutes. Generous, because a migration on a large database is slow and killing one half-way is worse than waiting.
Language settings.
runtime:
php: '8.4'
memory_limit: 512M
The version key is the language's: php, node or go, whichever your
project runs. It picks the image your application runs on and the one your
build steps run in, so dependencies are resolved against the version that will
serve them. Leave it out and you get the default — see PHP-FPM,
Node.js and Go for what is on offer.
Pin the version, not the build. You depend on PHP 8.4, not on one build of
it. Name 8.4 and the platform matches it to the current build, so a security
rebuild reaches you without anybody editing a repository. A version that is not
on offer runs the default instead.
A Drupal project may also say drupal: '10' or drupal: '11', which picks the
matching Nginx configuration. Anything else there is ignored.
memory_limit is PHP's, and it is clamped to what your plan was sold, on
a fixed ladder:
| Plan memory | Largest memory_limit |
|---|---|
| under 1 GB | 96M |
| 1 GB | 128M |
| 2 GB | 256M |
| 4 GB | 512M |
| 8 GB | 1024M |
| 16 GB and up | 2048M |
Asking for more gives you the maximum rather than an error. It is measured against your plan and not the machine, because on a shared host a share of the machine each adds up to more than the machine.
Directories that survive a release. The platform already keeps what your
framework's convention names — Drupal's public files, a private directory,
Laravel's storage. mounts is for anything else your application writes to.
mounts:
- my_files
- exports/generated
Each becomes that path at the root of your codebase, backed by storage that
outlives any one release. Names only — letters, digits, hyphens and
underscores, with slashes between them. An absolute path, a .. segment, a
dot or whitespace is refused.
A mount is not extra storage. It lives on the same disk as everything else in the environment and counts against the same allocation — what it buys you is that a release replacing the codebase does not take the directory with it. If you need more room, that is a bigger disk, not another mount.
cron is scheduled work. Declaring any replaces the framework's default
— drush cron hourly for Drupal, schedule:run every five minutes for Laravel,
wp cron event run --due-now hourly for WordPress — so if you
still want that, include it. Plain PHP, Go and Node.js projects have no default
job. A Laravel application that runs schedule:work as a worker gets no
schedule:run either, since that would run every task twice.
cron:
- name: nightly-import
schedule: '0 3 * * *'
command: 'drush queue:run import'
workers are processes kept running for as long as the environment is up.
They run your application's own image on a different command, so anything in
your build works — a PHP loop, an artisan command, a compiled binary.
workers:
- name: queue
command: 'drush queue:run heavy --time-limit=0'
replicas: 2
A worker's name is lowercase letters, digits and hyphens, starting with a
letter. replicas is how many copies run, from 1 to 16; leave it out for one.
One worker per machine that can run one. A long-running process competes with your site for the CPU that answers requests, and the first thing you notice is a slow site rather than a slow worker. So an environment has room for as many different workers as it has worker machines, or, without any, as many as it has web servers — and never fewer than one. Declaring more refuses the deploy; add a worker machine or a web server, or fold the work into a worker you already have. See Shapes.
A path on your application that answers when it is ready, asked for after the
deploy steps have run. Anything from 200 to 399 is healthy; the deploy waits,
retrying, until it answers or the timeout runs out. A timeout that runs out
fails the deploy, which is what deploy.on_failure: rollback then acts on.
health:
path: /health
timeout: 120
The path must be on this application and start with a slash. A check pointed at another host reports that host's health, and a green tick that means nothing is worse than no tick.
The wait is never shorter than 60 seconds. That is also the default when you name a path and no timeout. A container that has just restarted and run its migrations is not answering in ten seconds, so a shorter timeout does not find an unhealthy application — it finds a slow one, and rolls back a release that was about to be fine. Ask for longer when your deploy steps are long; a smaller number is raised to the floor.
Name no path and there is no check: the release is good the moment its stack is up.
vallic.yaml is the manifest. Some things are not manifest entries but lists
of commands to run at a particular moment, and those live in .vallic/commands/:
| File | Runs |
|---|---|
.vallic/commands/sanitization.yml |
after data arrives — copied in from another environment, or restored from a backup. Never on production |
Each is a commands: list of one-line commands, at most fifty, run in your
application container in order. See Backups.
Two more files add rules to Varnish's cache policy, when your stack runs it:
.vallic/varnish/recv.vcl and .vallic/varnish/backend-response.vcl. See
Varnish.
Variable names your application cannot start without. Names only — the values live in the console, because a repository is not where a credential belongs. See Variables. A deploy refuses if one is missing, naming it.
Every reason at once, rather than one per attempt. The common ones:
| Message | What to do |
|---|---|
no vallic.yaml |
Add the file |
Service "redis" needs a version |
Write - redis: '8.6' |
needs postgres 18, and this environment runs a different database |
Contact support — changing a database is a migration |
needs elasticsearch 9, which this platform does not offer |
Use one it does; the message lists them |
requires the SENDGRID_API_KEY variable |
Set it in the console |
declares 2 worker(s) and this environment has room for 1 |
Add a worker machine or a web server, or fold the work into one worker |
which this environment cannot run: it is a php service |
That service belongs to another language — Nginx beside a Node server, say. Take it out |
has no search machine to run it on |
On a shape where each service has a machine of its own, add one on the environment's Machines tab |