Skip to main content

Configuration

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.

#A complete example

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

#What goes here, and what does not

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.

#services

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.

#Every key

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, and the project it runs in

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.

#build

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.

#build.cache

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.

#deploy

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.

#runtime

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.

#mounts

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 and workers

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.

#health

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.

#Files beside it

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.

#env.required

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.

#When a deploy refuses

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