# Vallic Cloud handbook


---

# Start here

> What you are choosing when you create a project, and in what order.

A **project** is one application — a site, a shop, an API. It holds your code, its
environments, and the machines those environments run on.

Creating one takes a name. Everything else is a separate step called
**Configure**, and the console will keep sending you back to it until it is
finished, because a project that is half configured cannot be built.

## The order things happen in

1. **Create the project.** Only a name. It belongs to the team the console is
   showing — switch team first if it is meant for another one. Starting a
   project takes the Admin role or above.
2. **Configure it.** One page, worked down step by step: the plan, the shape,
   the application, where it runs, the services beside it, its bandwidth and
   CDN, the add-ons, support, and how you are billed. Every choice reprices the
   total at once, so the figure on the right is the figure you will be billed.
3. **Build the first environment.** It is always production, and its name
   cannot be changed afterwards.

Steps 1 and 2 cost nothing and can be revisited. Step 3 is where machines are
bought.

## What you cannot change later

Most of a project is adjustable. Four things are not, and they are worth
getting right the first time:

| Setting | Why it is fixed |
| --- | --- |
| Plan | Shared and dedicated are different machines, not a setting on the same one — whose machines they are was settled when they were bought. |
| Shape | Flexible and Dedicated are different arrangements of hardware, and moving a running site between them is a migration of your data. Within Flexible the drawing still changes afterwards — see [Upgrades](upgrade.md). |
| The production environment's machine name | It is in DNS, in certificates, and in every log line already written. |
| Application | Changeable until the first release is serving. After that it decides the language the machines run, and changing it is a migration rather than a setting. |

Everything else — machine sizes, support level, uptime promise, disks, extra
environments — can be changed whenever you like. Disks only ever
[grow](storage.md); machines grow, and some kinds can come back down — see
[Machines](machines.md).

## What you can change later

- **Machine sizes.** Upwards on any plan, and down again on the kinds of
  machine that allow it.
- **Support and uptime.** Both up and down, from **Commercial terms** on the
  project's configuration page.
- **Disk.** Add space to the database, the web server, or any other service.
- **Layout, within Flexible.** The database, cache, search or queue can move
  onto a machine of their own once the site is built, and come back — see
  [Upgrades](upgrade.md).
- **Environments.** Add staging and development environments, and remove them
  again.

## Next

Read [Plans](plans.md) if you have not decided between shared and dedicated, or
[Shapes](shapes.md) if you already know you need more than one machine.

If you are setting up for other people rather than only yourself, start with
[Teams and people](teams.md) instead — who can change what is a decision worth
making before anybody is invited.

Source: https://docs.vallic.com/start.md

---

# Plans

> Shared or dedicated — what the difference actually is, and which one your site wants.

A plan decides what a project's production runs on. Environments beyond production — staging, development and
feature branches — are add-ons on either plan, bought with the project or
added later under Commercial terms.

Every price is on the public [price list](/cloud/pricing), with the machines
grouped the way the configurator groups them, cheapest first.

| Plan | What it is |
| --- | --- |
| Dedicated | Single project. Every topology is available. Ideal for medium to large projects. |
| Shared | Multiple projects on a single machine dedicated to your sites. Ideal for smaller projects. |

## Shared

Several of your small sites packed onto one machine. The word *shared* is about
the machine being shared between **your own** projects — not between customers.
Nobody else's site runs on it.

What that buys you is the cost of one machine across several sites. What it
costs you is flexibility:

- **One machine for production**, always. You can see how a site is laid out,
  but nothing can be moved onto a second machine and no worker or extra can be
  added — those are machines of their own, and a shared plan buys none.
  Additional environments, if you add them, share one machine of their own
  beside it, the same as on a dedicated plan.
- **Standard uptime only.** 99.9% a month, committed, with service credits
  when it is missed — the same as every project. Advanced and Premium are sold
  on a dedicated plan.

This is the right plan for a brochure site, a small shop, a landing page — the
sites where an hour of downtime during a kernel upgrade is an inconvenience
rather than an incident.

## Dedicated

Your own machines, and every option is open: any shape, and an uptime promise
that means something because there is more than one machine to keep the
promise with.

## Backups

Every production environment is backed up nightly and kept four days, **on
either plan**, with two copies offsite on different providers. How long a
series reaches back is not something the plan decides — it is bought for
production (thirty days instead of four), and changed whenever you like.
Thirty days catches "this has been wrong since before the last release and
nobody noticed", which is the restore people actually need and the one four
days does not cover.

The database can also be backed up every four hours rather than nightly — an
add-on on either plan and on either shape.
Staging and development are not copied offsite. See [Backups](backup-storage.md).

## Non-production environments

Production is included. Everything beyond it — staging, development, feature branches — shares **one machine**, and that machine is the purchase. You pick it when you set the project up, from a short list named for how many environments each one carries (`VC-PRE-E3-0` suits three, `VC-PRE-E7-0` seven), and resize it afterwards from its own page like any other machine.

How many environments you run on it is a separate lever, and a free one: the count divides the machine rather than buying anything. Staging takes two shares to a feature branch's one, so at three environments staging has half the machine and the other two a quarter each. Run more and each gets less; run fewer, or buy a bigger machine, and each gets more. Both are changed later under Commercial terms on the project's configuration page, and adding an environment past the count opens that dialog rather than failing.

Sold in bands — 2, 4, 6, 8, 10, 15, 20, 25 — so seven environments are bought as eight and the spare one is yours to start whenever. On a shared plan the ceiling is twenty-five, which is one non-production environment for each site the plan can carry.

The machine is built in production's own datacentre wherever one suitable is sold there, and in the nearest that has one otherwise; the configurator says which before you buy.

## Traffic

**Origin bandwidth** is what leaves your machines — to visitors directly, or to the CDN in front of them. Every machine that faces the internet includes some each month. How much depends on the machine and where it runs, and the configurator shows the figure beside each one as you choose and the total under **Edge** — so the allowance you are buying is on the screen rather than in a table here that would be right for some machines and wrong for others.

**The figure shown is yours to spend.** Like cores, memory and disk, traffic is quoted after the platform's own share has been taken out of it. Your backups leave the machine on the same connection your visitors arrive on, and so do your shipped logs, the container images pulled on every deploy, and the agent talking to the control plane — none of which appear in any log you can see. The allowance beside a machine is what is left once all of that is paid for, with room kept back for the month nobody plans: a crawler that finds a faceted search, a fortnight of retries against a webhook that broke quietly.

So the number is smaller than the one the underlying provider advertises, and it is the only one we will quote you. There is no larger figure to go looking for.

A site that will send more buys extra bandwidth by the terabyte a month, at the same price whatever the machine or datacentre, when the project is set up or later under Commercial terms.

**Requests are not limited.** Nothing is capped or charged per request, at the origin or at the edge: a page served a million times costs its bandwidth and nothing else.

## CDN

Serving your site from datacentres near your visitors, bought by the terabyte
a month. The platform runs the pull zone, attaches your domains and
gets the certificates; your application can clear the cache without holding a
credential. [Full details](cdn.md).

## Domains

Two of your own domains on a project are included — the name the site answers
on, and the one somebody always wants beside it. More are bought in three
bands — up to ten, twenty-five and a hundred — under Commercial terms.

## Included on every project

The **Basic WAF and bot management** at the [edge](edge.md) and
[log forwarding](logs.md#sending-a-copy-somewhere-else) to your own collector
are add-ons every project has, at no cost. They are on the invoice at
nothing, so what you have is written down.

## People

The first person on a project is included; past that, a project is in a band
of up to three, ten or thirty people. See [Teams](teams.md).

## Changing any of it later

Support, the uptime promise, the environment count, extra bandwidth, the CDN,
domains and production's backup schedule are all changed from **Commercial
terms** on the project's configuration page. Taking more is immediate and
prorated — you are charged for the part of the period you use it. Giving
something back takes effect from your next invoice and is not credited for the
rest of the period you already bought. Resizing a machine is the exception,
credited both ways; a disk cannot be removed. See [Billing](billing.md).

The **billing period** is the one thing that does not move mid-term. It is what
you agreed to and have been invoiced under, so it changes at renewal; the
dialog shows it and refuses it rather than leaving you to wonder where it went.

## Choosing

Start with the question of whether one machine is enough for production. If
an hour of downtime during a kernel upgrade is an inconvenience rather than an
incident, shared is enough. If it is an incident, you need dedicated — that is
the line, and it is a harder line than machine size or traffic.

You cannot move a project between shared and dedicated. They are different
machines, not a setting on the same one, so switching means creating the new
project and migrating to it. Choosing dedicated for a site that turns out not
to need it costs money; choosing shared for a site that turns out to need a
second machine costs a migration.

## Next

[Shapes](shapes.md) covers how many machines an environment runs on, which is
the choice inside a dedicated plan. [Billing](billing.md) covers how any of
this is actually charged — the period, what changing something mid-term costs,
and what happens if an invoice goes unpaid.

Source: https://docs.vallic.com/plans.md

---

# Shapes

> A layout you draw, from one machine up, or a redundant web tier with a machine per service — and what each one asks of your application.

A **shape** — topology, in the configurator — is how many machines an
environment runs on and what each of them does.

| Shape | What it is | Machines |
| --- | --- | --- |
| Flexible infrastructure | Your machines, your layout. Move any service onto a machine of its own whenever you want. | 1 |
| Dedicated | A balancer, two or more web servers, and every other service on a machine of its own. | 4 |

## Flexible infrastructure

Your machines, your layout — **from one machine up**.

Every service starts on the web server. Leave them there and you have one
machine running everything: the web server, the application, the database, the
cache, with files on local disk and no private network because there is nothing
to network to. That is what most sites are, and it is what every staging and
development environment is regardless of what production runs.

Move any of them onto a machine of its own, or onto one shared with a couple of
others, and you have as many machines as you drew — over a private network the
platform creates as soon as there is a second machine to reach. The database on
its own for its IO, Solr and the broker sharing a box, a worker keeping the
site's CPU free. Each machine is named for what it carries, so the server list
reads `db`, `search-queue`, `worker-cache`.

You do not change shape to grow. The same shape covers a site on one machine
and a site on six, so spreading out later is a matter of moving a service
rather than buying a different arrangement.

Worth doing when the database and the site are competing for the same memory,
which usually shows up as the site getting slower under load while neither
machine looks busy on its own. It is also the answer when the database has run
out of *room*: the database always stays on the machine, so the way to give it
more disk is to give it a machine, or a bigger one.

Varnish can sit on the web server or on a machine of its own, where it shares
with nothing. A worker machine never sits on the web server. Any uptime promise
can be bought on any number of machines. Entry-line machines are allowed here;
Premium support is not sold with one.

**On a shared plan there is nothing to draw.** The machine belongs to the
platform and other sites are on it, so your site runs on that one machine and
a worker or an extra — which are second machines — need a dedicated plan. See
[Plans](plans.md).

## Dedicated

Every service on a machine of its own — the database, the cache, search, the
broker — with nothing shared and nothing to draw, behind a load balancer that
is always there. **Two web servers at minimum**, so the tier survives losing
one. The only numbers to set are how many web servers beyond that and how many
workers. The database is backed up nightly, as everywhere; every four hours is
an add-on on either shape. Entry-line machines are not offered: the point of this shape is
hardware that stays up. From four machines up — the balancer, two web servers
and the database — and in practice five, because a Drupal stack also wants a
cache.

That minimum is the shape. If what you want is a machine per service and
nothing more, **Flexible** draws exactly that and costs less; what Dedicated
sells on top of it is a web tier that keeps answering when a machine goes
away, which one web server behind a balancer does not.

**Two web servers is a migration, not a checkbox.** It is the thing to settle
before you buy this shape rather than after. Three things that were local stop
being local; the platform handles one of them and the other two are yours:

- **Files.** Handled for you: one web machine holds the files directory and
  exports it to the others over the private network, so an upload landing on
  web-1 is there on web-2. Nothing to do in the application — but it is why
  the files live on a machine rather than beside each copy of the code, and
  why a bucket is still the better answer once the uploads are large.
- **Sessions.** A session stored on local disk logs the visitor out every time
  the balancer sends them to the other machine. Keep sessions in the database
  or the cache — Drupal and WordPress already do; Valkey is not required. For
  an application that cannot move them, **Keep each visitor on one web
  machine** in the environment's proxy options pins each visitor to one
  machine instead, at a price: a visitor loses their session when their
  machine goes down or restarts, which is the failure the second machine was
  bought for.
- **Scheduled work.** Only one machine runs your schedule, so nothing runs
  twice — but a job that assumes it is on the machine that took the upload,
  or writes to a local path, will not find what it expects.

Neither of the two left to you is unusual, and every framework we support can
do both. But they are work in your application, not a setting here, and worth
doing before you buy this shape rather than discovering afterwards.

## A machine for background work

On **Flexible** and **Dedicated** you can add a *worker*
machine. It is optional: without one, your schedule and any long-running
processes run on the web machine, alongside the site.

That is usually fine, and worth changing when it is not. A nightly import that
pins a CPU for twenty minutes is competing with the requests your visitors are
making, and the symptom is a slow site rather than a slow import — which is why
it is easy to miss.

With a worker machine, the schedule and every process you declared under
`workers` move there. They move rather than split: half the jobs in one place
and half in another is an arrangement nobody could reason about.

**It is a machine, and it is priced as one.** You choose its size like any
other, and it appears as its own line on the quote. A worker doing occasional
overnight work does not need what your web machine needs.

**Without a worker machine, long-running workers are bounded rather than
refused:** one declared worker per machine that can run one — each worker
machine where you have them, each web machine otherwise. A process that never
exits competes with the site for the CPU that answers requests, and one apiece
is the most that leaves the site able to answer. A deploy that declares more
than there is room for says so, and the answer is a worker machine, another
web server, or folding the work into a worker you already have — see
[`vallic.yaml`](configuration.md).

## Changing shape

You cannot move an existing project between **Flexible** and **Dedicated**, or
between plans: either would move your data between machines, and doing that
silently under a running site is how data gets lost. Adding capacity within a shape — bigger machines, more disk — is always
available.

Within **Flexible** there is one exception: the project's **Upgrade** tab can
give the database, cache, search or queue a machine of its own after the site
is built, and bring cache, search or queue back. It shows what will move and
what it costs in downtime before anything happens — see
[Upgrades](upgrade.md).

Within **Dedicated**, the same tab changes **how many** web servers and
workers there are. Every web server is the same size — traffic is shared
between them evenly, so a smaller one would set the pace for all of them —
and a new one is built at the size of the others. It is sent your live code
before it takes any traffic, so adding one never serves errors while it
catches up. Fewer takes away the highest-numbered machines, after the load
balancer has stopped sending them requests. The subscription follows the
machines either way. To change their size, resize any one of them on the
machines page: the others in the role move with it, one at a time.

## Next

[Software stacks](stacks.md) covers what actually runs on these machines.

Source: https://docs.vallic.com/shapes.md

---

# Teams and people

> Who a project belongs to, and what each role in a team can do.

Everything in the console belongs to a **team**. Projects, machines, disks,
integrations and access tokens are all owned by one, and a person sees exactly
what their teams own — nothing else. SSH keys are the exception: they belong to
you, because you are the same person in every team you are in.

The team is in every console address — `/{team}/projects/…`, where `{team}` is
a short id the team was given when it was made — so a link always says which
team it belongs to.

If you belong to a single team, you will rarely notice it exists: the console
fills it in for you and the field is not shown. It only becomes a choice once
you belong to more than one.

## Roles

Roles are a ladder. Each one can do everything the roles below it can, plus its
own.

| Role | Can |
| --- | --- |
| Viewer | See projects, environments, machines and their state. Changes nothing |
| Developer | Everything above, plus work on the sites: deploy, open a shell, edit variables, take and restore backups, and add staging or development environments where the subscription has room |
| Admin | Everything above, plus run the team: start projects and change their terms, resize and remove machines, move and add services on the Resources tab, delete environments, reset a database, switch the maintenance page on, change secret variables, manage integrations, notifications, people, invitations and the audit log |
| Owner | Everything above, plus the money: billing, invoices, cancelling, deleting a project, and making another owner. A team always keeps at least one |

The line worth knowing is between **Viewer** and **Developer**: it is the line
between reading the console and changing what it runs. **Developer** is kept
deliberately narrow, because it is the role you give an outside developer: it
works on the sites and decides nothing for the team. Anything that spends
money, removes something or changes who can do what is **Admin** and above.

A protected environment — production, unless you turned it off — needs the
Owner for anything that reshapes or overwrites it, a restore included.
Deploying to it stays ordinary Developer work.

## Adding somebody

Invite them by email address from the team's People page. They get a link, and
they join with the role you chose — an invitation is a role, not just an
opening.

An invitation you regret is withdrawn from the same page. A person you remove
loses access immediately, including anything they had open.

### Only some projects

An invitation covers every project the team owns unless you say otherwise.
When you invite somebody you can instead name the projects they should reach,
which is what a contractor brought in for one site wants: everything a full
member could do, inside those projects and nowhere else.

It narrows, never widens. The role still decides what somebody may *do*; this
only decides *where*. A viewer named against every project is still a viewer.
An Owner cannot be narrowed.
What belongs to the team rather than to a project — the team itself, its keys
and its billing — is untouched by it.

## How people are counted

The first person on a project is included. Past that, a project is in one of
three bands: up to 3, up to 10 and up to 30 people. What each costs is on the
[price list](/cloud/pricing).

Inviting somebody inside a band changes nothing on the invoice; the price moves
when the project moves to the next band. Past thirty, talk to us.

Counted for the people who can actually reach it, which is the point of
narrowing an invitation: somebody brought in for one site moves the price of
that site and no other. Somebody who works across the team counts everywhere,
because they can work everywhere.

## Integrations

Services your team already pays for, connected once and used by any project:
a log collector, storage for your backups, somewhere to send notifications,
and an APM account — see [Monitoring](monitoring.md) for what APM covers. Under your team's settings, and
managed by Admins and Owners.

Each one carries whatever that service needs to know which account it is —
a Datadog site, an Axiom dataset, a Loki endpoint — and its token. Tokens are
encrypted and never shown again; you can replace one, but not read it back.

**Why here and not on the project.** One token is usually used by several
projects, and it has to be rotated in one place. Putting it on the project
means finding every project that had a copy the day the token changes.

[Integrations](integrations.md) covers what can be connected and what each one
is used for.

## Audit log

Every change that costs money or alters access is recorded: machines bought and
destroyed, disks grown, people added and removed, roles changed. Admins can
read it from the team page.

It answers the question that comes up after something has gone wrong — *when
did this change, and who changed it* — which is exactly the question nobody can
answer from memory.

## Next

[Projects](projects.md) covers what a team actually owns,
[Shell access](shell.md) covers the SSH keys you add, and [The API](api.md)
covers access tokens.

Source: https://docs.vallic.com/teams.md

---

# Projects

> One application, its settings, and what changing them does.

A project is one application. It holds the repository the code comes from, the
environments that run it, and the settings that decide what those environments
are allowed to be.

## Creating one

You are asked for a name. Nothing else. The project belongs to the
[team](teams.md) the console is showing, and cannot be moved to another team
afterwards. Its machine name is generated for you rather than typed, and it is
permanent: it reaches hostnames, paths and backup prefixes. Support level and
uptime promise are not asked here, because neither is a decision anybody can
make before they have decided what they are building.

Starting a project, and everything on the Configure page, takes the Admin role
or above: it is where machines are bought.

The project then needs configuring, and until that is done every link to it
goes to the Configure page. This is deliberate: an unconfigured project cannot
be built, and a console that let you wander into its environments would be
offering pages that can only refuse you.

## Configure

One page, worked down in steps, with the total beside it. Every choice reprices
immediately, so the number in the summary is the number you will be billed.

1. **Plan** — dedicated or shared.
2. **Shape** — how many machines, and what each one does.
3. **Application** — what the code is.
4. **Location** — the region, the datacentre in it, and the kind of machine.
   The region narrows the datacentres to the ones there. The page opens on your
   billing country where there is a datacentre in it, and on its part of the
   world otherwise. Prices vary by datacentre — a machine in one genuinely costs
   more than the same machine in another — so the machines below switch when
   the datacentre does, rather than one price being shown everywhere and
   surprising you later.
5. **Services** — what runs beside the site, and the machines it runs on.
6. **Edge** — the origin bandwidth your machines include, more of it if you
   need it, and a CDN in front. See [Plans](plans.md#traffic).
7. **Add-ons** — the staging and development environments beside production,
   and longer backups.
8. **Configure project** — support, the uptime promise, custom domains and
   the team.
9. **Billing period** — monthly, quarterly or yearly.

A site joining a shared machine you already have skips what that machine has
already decided: its shape, its location, its edge, and its support, uptime and
billing period.

Nothing is bought until you build an environment.

## Support and uptime

Standard is included. The paid levels are priced as a share of what your
machines cost, so they scale with the size of what is being supported, and
support has a minimum as well: a guaranteed response is somebody on call
whatever the site costs. The configurator quotes the figure before you agree
to it, and the [price list](/cloud/pricing) has the shares.

| Level | Support response | Uptime |
| --- | --- | --- |
| Standard | 4-hour best-effort on urgent tickets | 99.9% committed |
| Advanced | 60-minute **guaranteed** on urgent tickets | 99.95% committed |
| Premium | 30-minute **guaranteed** on urgent tickets | 99.99% committed |

Support counts every machine, extras included: somebody answers about those
too. The uptime price counts the machines carrying the site, not the extras,
because the promise does not cover them.

The response times are for **urgent** tickets — a site that is down. Everything
else is answered too; it is the urgent number people are buying.

"Best effort" and "guaranteed" is the whole difference. Standard means we work
to four hours and owe you nothing if we miss; the paid levels mean we have
agreed to the figure.

Uptime is different: every level is a commitment, Standard included. Every
project is committed to 99.9%, Advanced to 99.95% and Premium to 99.99% — a
figure you can hold us to, with service credits when it is missed.

In a 30-day month that is about 43 minutes of downtime at 99.9%, 22 minutes
at 99.95%, and 4 minutes at 99.99%.

#### Service credits

Where a month misses the figure committed to, a share of that month's invoice
comes back. The share is worked out from the Monthly Availability
Rate, and it is a percentage of what was invoiced **for the resources that
missed** — a database machine that was down does not refund the CDN that
served perfectly alongside it.

It also depends on the level: the paid levels cost more for a tighter figure,
so missing it is worth more.

| Monthly availability | Standard | Advanced | Premium |
| --- | --- | --- | --- |
| Below the committed figure, at or above 99.0% | 5% | 10% | 15% |
| Below 99.0%, at or above 95.0% | 10% | 25% | 35% |
| Below 95.0% | 20% | 50% | 50% |

The first row runs up to the level's own figure — 99.9% on Standard, 99.95% on
Advanced, 99.99% on Premium.

**Premium support and the Advanced uptime SLA are sold on a three-month billing
period or longer, and the Premium uptime SLA on a yearly one.** A commitment
measured in minutes is not something to take on a month at a time. Either
uptime promise can be bought on any machines, one or many; the machines are
ours to keep up. Premium support is not sold with an entry-line machine.

### Storage

The machine's disk is divided between what fills it: the database, a search index, a queue, public files, private files, and a reserve for mounts. The configurator opens with a sensible split and lets you change each figure: on the machine holding the files the database takes a quarter and the files the rest, and a search index or a queue added beside them takes its share from the files, never from the database. On the flexible and dedicated shapes every machine has a storage box of its own, and a machine carrying two services that fill a disk, say the database beside a search index, halves it between them; either can still be given a disk of its own, and what stays takes the whole machine. Mounts start at nothing: they are the extra directories your application names in `vallic.yaml`, and whatever you reserve here is shared between them once they are named. A bucket that will not fit, or that you expect to grow, can be given a disk of its own right there, in 10 GB steps to 100 GB and 50 GB steps to 1 TB: one disk for the public files, one for the private files (the mounts go with them), one for search, one for the queue. The database always stays on the machine. Disks are made once the machine exists and are mounted before the first deploy, so nothing ever has to move. The environment's Storage tab shows each bucket against its budget.

Resizing a machine later does not change any of this. The machine's own disk stays the size it was, so the budget and the disks you bought stay as they are; what you moved onto its own disk stays there, and room on the machine is added by buying a disk rather than by a bigger machine.

### Billing period

Monthly by default. A quarter or a year paid ahead is discounted, the same for every plan. It is chosen last on the configurator, because it changes nothing about what is built, only what is paid at once.

A shared plan is always *Standard uptime* — 99.9%, committed, with credits —
and the field cannot be changed: Advanced and Premium are sold on a dedicated
plan.

## Changing a project

**Before any machine is built**, everything is still a choice on the Configure
page. Change the plan, the shape, the application, the support level, the
uptime promise — nothing has been bought.

**Once it is built**, the project's configuration page has two cards, each with
its own **Change**:

- **Project** — the name, the default branch, and the application until
  something has deployed.
- **Plan** — the plan, the shape and the billing period, which changes at
  renewal.
- **Commercial terms** — a card for each part, each with its own **Change**:
  support and uptime, preview environments, bandwidth and the CDN, custom
  domains, backups, and autoscale where the machines are Cloud Native. Each
  dialog quotes the change before you agree to it. Anything you add is
  available at once and charged for the rest of the period; anything you
  reduce stays yours to the end of the period you have paid for and comes off
  the next invoice.

| Setting | After it is built |
| --- | --- |
| Plan | Fixed |
| Shape | Fixed — within Flexible the [Resources](upgrade.md) tab still moves services onto machines of their own |
| Application | Changeable until the first release is serving, then fixed |
| Support level | Change any time |
| Uptime promise | Change any time |
| Machine sizes | Change any time, [up, and down on some kinds](machines.md) |

The ones that lock are not preferences at that point — they are the hardware
you are being billed for. Changing a plan or a shape means moving your data to
different machines, and changing the application under a running site means a
different container running different code. All three are migrations support
does for you. See [Plans](plans.md) and [Shapes](shapes.md).

## Next

[Machines](machines.md) covers resizing, [Environments](environments.md)
covers what actually runs, and [Support](support.md) and
[Monitoring](monitoring.md) cover what the support level and uptime promise
buy you.

Source: https://docs.vallic.com/projects.md

---

# Environments

> Production, staging and development — what each is for, and what you can add.

An environment is one running copy of your application: its own machines, its
own database, its own domain, its own variables.

## The first one is production

Every project's first environment is production, and its machine name cannot be
changed. The name is in DNS, in the certificates already issued, and in every
log line already written — renaming it does not rename any of those, it just
makes them disagree.

Machines get a `.vallic.cloud` address built from that name, so you have
somewhere working to point at before your own domain is ready.

Environments cannot be added until the project's initial setup is finished.
There is nothing to copy the shape and stack from until then.

## The other types

| Type | For | Machine | Backups kept | Protected by default |
| --- | --- | --- | --- | --- |
| Production | The live site | What you configured | Four days of dailies, or what you bought | Yes |
| Staging | Checking a release before customers see it | A share of the shared non-production machine — two shares | The last three days | No |
| Development | One feature branch | A share of the same machine — one share | The last day | Never |

A project has one staging environment, and it comes first: a development
environment is a branch on its way towards staging, so one cannot be added
until staging exists. Development environments are named after their branch —
`dev-` and the branch name.

Everything beyond production shares **one machine**, chosen when the project is
set up and resized afterwards like any other. How many environments you run on
it decides how much each one gets: staging takes two shares to a feature
branch's one, so three environments give staging half the machine and a quarter
to each of the others, and six give staging two sevenths and each of the rest
a seventh.

The count itself costs nothing. Raising it gives every environment less;
lowering it gives them more. Giving them all more means a bigger machine. Both
levers are on the configurator when the project is set up, and both change later
under Commercial terms on the project's configuration page. Adding an
environment past the count opens that dialog instead of failing, and a running
environment cannot be un-bought from there — delete it first.

Everything is backed up, but the environments beyond production keep less
history: they are rebuilt from a branch, and keeping weeks of them would spend
storage on something nobody will restore. See [Backups](backup-storage.md).

Protection is a default rather than a rule. Production arrives protected
because that is the safe answer to have without thinking about it, and it can
be turned off deliberately. A development environment is never protected: it
lives while its branch does, and on GitHub deleting the branch removes it.

## Shape

Every environment other than production runs as a single machine, whatever
production runs. There is no reason to pay for a high-availability pair to
check a release on — and if the thing being checked is the high-availability
behaviour itself, that is a production change with a rollback, not a staging
question.

## What lives on an environment

- **Its source**: the branch it tracks — no two environments in a project
  track the same one — and whether a push to that branch deploys it
- **Variables**, and the ones it inherits from the project — see
  [Variables](variables.md)
- **Domains** and their certificates
- **Service versions**: the database, cache and runtime versions from
  [the catalogue](stacks.md)
- **Disks**, and which service each one holds — see [Storage](storage.md)

## Domains

Every environment answers on a `…vallic.cloud` hostname from the moment it
exists, so there is somewhere working to point at before your own domain is
ready. That name is always a **direct route to the machine** — it is never put
behind a CDN — which is what gives you a way in that does not depend on a cache
when you are trying to work out whether something is broken.

Adding your own is two records, shown on the Routing tab with the values filled
in:

- A **CNAME** at the environment's own hostname (or at the CDN, if the project
  has one — the tab shows whichever applies).
- A **TXT** record proving you control the domain.

Publishing the CNAME proves control on its own, so the TXT record is only
needed if you want to be verified **before** moving traffic — which is what you
want when migrating a live site in without a gap. An **apex** domain cannot
hold a CNAME, so there the TXT record is the only proof, and the address is
given as A records or as ALIAS/ANAME if your provider has them.

### Canonical

One domain per environment is the canonical one, marked on the Routing tab.
Every other hostname it answers on **redirects there** — so a site with a real
domain stops being reachable at two addresses, which is what search engines and
absolute links both want.

The platform hostname stays a direct route to the machine and is not itself
redirected away until you promote one of your own domains; promoting an
unverified domain is refused, because pointing every address at a name nobody
has proved control of would take a working site off the air.

## Copying data between them

**Backups → Copy from another environment** copies a running environment's
database and files, as they are right now, into another environment of the same
project. It replaces rather than merges, and production can never be the one
copied into. Nothing is scrubbed on the way unless you declare it in
`.vallic/commands/sanitization.yml` — [Backups](backup-storage.md) has the details.

## Next

[Variables](variables.md) covers configuration that differs between them,
[Domains](domains.md) covers putting your own name on one, and
[Deployments](deployments.md) covers what happens when code reaches it.

Source: https://docs.vallic.com/environments.md

---

# Domains

> Adding your own domain, proving it is yours, choosing the canonical one, and where each certificate comes from.

Every environment answers on a platform hostname from the moment it exists,
and on your own domains once you have proved they are yours. Both are managed
on the environment's **Routing** tab.

## The platform hostname

Each environment gets one, built from the environment's name and the
project's:

```
production.gwfdxo99.vallic.cloud
```

It is created with the environment, already verified, and its DNS is ours.
It cannot be removed or switched off, and it does not change if you rename
the project — it is in certificates, bookmarks and other people's
configuration by then, and a name that moves breaks all of them.

It is never put behind the [CDN](cdn.md). That keeps it a direct route to the
machine: when you are trying to work out whether something is broken, it is
the address that does not depend on a cache.

## Adding a domain

**Routing → Add domain.** It asks for the hostname and, if something sits in
front of us, which CDN that is ([below](#behind-a-cdn-of-your-own)).

A hostname is refused if:

| Refused | Why |
|---|---|
| It contains `*` | Wildcards are issued by the platform, not added by hand |
| It is under `vallic.cloud` | Only the platform issues names there |
| It is already on this project, on any environment | One name routes to one place |
| Somebody else on the platform has already verified it | Whoever proves control first has it |
| It is an IP address | There is nothing to issue a certificate for |

A hostname somebody else added but never verified does not block you. An
abandoned claim is not a claim.

## Pointing DNS at us

When you add a domain, the tab shows the records to create, with the values
filled in and a Copy button beside each. Copy them rather than retyping:
the verification value is long enough that one wrong character reads as
"not propagated yet".

| Your domain | Record |
|---|---|
| A subdomain — `www.example.com`, `shop.example.com` | **CNAME** to the environment's CNAME target |
| An apex — `example.com` | **A** record to the environment's IPv4 address, or **ALIAS/ANAME** to the CNAME target if your provider offers it |
| An apex, with the platform CDN on | **ALIAS/ANAME** to the pull zone |

The **CNAME target** is shown at the foot of the tab. It is the platform
hostname, or the CDN's pull zone when the environment has one. It is a name
rather than an address on purpose: replacing the machine, or putting
something in front of it, is then one record we change rather than a message
asking you to edit your DNS.

An apex cannot hold a CNAME — that is a rule of DNS, not of ours. Prefer
ALIAS or ANAME where your provider has them, for the same reason: an A record
names an address, and an address can change. Behind the platform CDN there is
no address to give at all, so if your provider has neither, use a subdomain
such as `www` and redirect the apex to it.

Only IPv4 addresses are given. There is no AAAA record to create.

## Proving it is yours

A domain does not route until it is **verified**. Until then it shows
**Awaiting DNS** on the tab, and requests for it get nothing from us.

There are two ways to verify, and either is enough:

- **The CNAME itself.** Once the hostname's CNAME points at the environment's
  target, it is verified. Nothing else to do.
- **A TXT record** at `_vallic-challenge.` in front of the hostname — for
  `www.example.com`, at `_vallic-challenge.www.example.com` — holding the
  value shown on the tab.

The TXT record is what lets you verify **before** you move traffic. When you
are bringing a live site across, publish the TXT, let it verify, check
everything on the new environment, and only then change the CNAME — so there
is no gap where the domain points at something that is not ready.

**An apex can only verify by TXT.** An A record points at a shared address,
which proves the domain reaches us but not which environment you meant.

**Check now** on the row looks straight away, up to twelve times an hour per
domain. A pending domain is also checked on a schedule without you pressing
anything, and it keeps being retried until it verifies or you remove it. If a
check fails, the row says why — no TXT record found, a TXT record with the
wrong value, or the name already verified somewhere else.

**Once verified, a domain stays verified.** It is not re-checked, so you can
remove the TXT record afterwards. It also means changing the domain's DNS to
point somewhere else does not remove it from the environment — remove it here
as well.

## The canonical domain

One hostname per environment is **canonical**, marked on the Routing tab. At
first it is the platform hostname.

**Make canonical** on one of your own verified domains, and every other
hostname the environment answers on — the platform hostname, the apex, `www`,
an old domain you are migrating away from — redirects permanently to it,
keeping the path. A site stops being reachable at two addresses, which is what
search engines and absolute links both want.

There is no separate apex-to-`www` setting: that is this. Add both, make the
one you want canonical, and the other redirects.

An unverified domain cannot be made canonical. Pointing every address at a
name nobody has proved control of would take a working site off the air.

To stop redirecting, make the platform hostname canonical again — or remove
your canonical domain, and the platform hostname takes the role back on its
own.

## HTTPS

**Everything is HTTPS.** A request on plain HTTP is redirected permanently to
the same address over HTTPS, on every hostname. The only exception is the
path certificate authorities use to prove a domain, which has to be reachable
on HTTP for a certificate to be issued at all.

You do not choose how a certificate is obtained. It follows from what is in
front of the domain, and the row on the Routing tab shows which applies:

| Shown as | Covers | Issued by | You need to |
|---|---|---|---|
| Platform wildcard | The platform hostnames | Us, one wildcard per project, renewed 30 days before it expires | Nothing |
| Let's Encrypt | Your domain, pointed straight at us | Let's Encrypt, on the machine, renewed automatically | Verify the domain and point it at us |
| Terminated at the CDN | Your domain, behind a CDN | The CDN | Nothing, if it is the platform CDN; your CDN's own certificate otherwise |

A Let's Encrypt certificate is asked for once the domain is verified **and**
resolves to us, and not before: the authority checks by connecting to the
name, and a name that still points at your old host proves nothing. So there
is a short window after you change DNS where the domain answers but its
certificate is still arriving.

**You cannot upload a certificate of your own**, and you cannot add a
wildcard domain. If you need either — an EV certificate your contracts
require, or a thousand customer subdomains — talk to us before you build
around it.

## Behind a CDN of your own

If your domain goes through Cloudflare, Fastly, Akamai, your own Bunny
account or another proxy before it reaches us, say so under **CDN in front**
when adding or editing it. Two things depend on it.

**The certificate.** A CDN in front answers the visitor's connection itself,
so it is the CDN's certificate the visitor sees, and it also intercepts the
check Let's Encrypt would use. We do not obtain a certificate for that name on
the machine. Your CDN has to connect to us over HTTPS — plain HTTP is
redirected to HTTPS, which a CDN following it will do in a loop — and it will
not find a publicly-issued certificate for your domain at our end.

**Who the visitor is.** Behind a CDN every request reaches us from the CDN.
The platform reads the visitor's real address from the header that network
sets on every request: `CF-Connecting-IP` for Cloudflare, `Fastly-Client-IP`
for Fastly. That address is what [allow and deny lists](edge.md) are matched
against.

**The header is believed only from the CDN's own addresses.** Anyone can
connect to your site directly and send the header themselves, so it is read
only on a request that arrived from one of the network's published edge
ranges, which the platform fetches daily. A request from anywhere else is
judged by the address it connected from.

Akamai publishes no list of its edges, and neither can a CDN we have no entry
for, so for those the header is never believed: the rules see the CDN's own
address. A block list behind them stops nobody and an allow list stops
everybody. Bunny sets no header of its own either; with your own Bunny account
you are in the same position.

With the [platform CDN](cdn.md) none of this is yours: the pull zone,
its certificate and its header are set up for you, and the field is not shown.

## How many domains

**Two custom domains per project are included**, across all its environments
— the name the site answers on, and the one somebody always wants beside it.
Platform hostnames do not count. A domain awaiting verification does. The
count holds wherever a domain is added: past it, the console refuses the
domain, and the API and `vallic domain add` answer 409
`domain_allowance_spent`.

More are bought under **Commercial terms** on the project's configuration
page, in three bands: up to 10, up to 25 and up to 100 domains. What each
costs is on the [price list](/cloud/pricing).

Past 100, talk to us. Adding a domain past the count is refused with a message
saying so, rather than quietly billed.

## Removing a domain

**Remove** on the row. The name stops routing on the next reconcile and its
certificate is no longer renewed. **Your DNS is not touched** — records still
pointing at us are yours to remove, and until you do, visitors get nothing
useful from them.

The canonical domain cannot be removed while it is canonical; make another
one canonical first. You can add a removed name back later.

## Who can do what

| | Role needed |
|---|---|
| See the Routing tab | Viewer |
| Add, edit, check, make canonical | Developer |
| Remove | Admin |

On a **protected** environment — production, unless you have changed it —
every change needs the Owner. See [Teams](teams.md).

The API can list, add, verify and remove domains. Making one canonical and
declaring a CDN are done in the console.

## Next

- [The edge](edge.md) — what happens to a request once it has reached us
- [CDN](cdn.md) — putting the platform's CDN in front

Source: https://docs.vallic.com/domains.md

---

# The edge

> The proxy in front of every environment — what it refuses on its own, what you can switch on, and taking a site offline.

Every request to your site passes through a proxy on the machine before it
reaches your application. It terminates HTTPS, routes the hostname to the right
environment, and applies the rules below. Some of them are the platform's and
apply to everyone. The rest are yours: most are set once for the whole project
on its **Edge** tab, and who may reach each environment is set on that
environment's **Routing** tab.

All of it is the **Basic WAF and bot management** add-on, included on every
project at no cost: what is refused below, SQL injection counted or refused,
and every request named by the kind of client that sent it. It is on your
invoice at nothing, so you can see you have it.

## In order

A request meets the checks in this order, and the first one that refuses it
answers:

1. **HTTPS.** Plain HTTP is redirected to HTTPS — see [Domains](domains.md#https).
2. **Probe guard** — the platform's, what is refused by default for your kind
   of project, and your own probe rules, below.
3. **Blocked addresses, allowed addresses and blocked user agents.**
4. **Password protection.**
5. **Rate limit.**
6. **Compression and HSTS**, on the way back out.
7. **The canonical redirect**, if the hostname is not the canonical one.

So a scanner is turned away before it is ever asked for a password, and an
address you have blocked never gets as far as counting against a rate limit.

## What is refused for everyone

The **probe guard** turns away requests for files no site should serve, before
they reach your application: the ones an automated scanner asks every server on
the internet for, hoping one of them answers. Hidden files, paths that try to
climb out of the web root, and the well-known names of configuration and
credential files — along with backup copies of those.

`/.well-known/` is the exception, because certificates and app links live
there. Matching ignores case.

A refused request gets a **404**, not a 403. A 403 tells a scanner the file
exists and is worth coming back for; a 404 says there is nothing here, which is
also true of what it can reach.

It is deliberately narrow, and it is not a firewall that understands your
application: it will not stop an attack on a URL your application really
serves. What it does is take the noise away, so your logs and your PHP workers
are spent on visitors.

**You cannot turn it off or change what it covers.** If a path your application
genuinely needs is refused, tell us. You can add to it, though — below.

### Refused by default

Three more, decided by what your project is, each on unless you turn it off on
the project's Edge tab. They are refused the same way — a 404 and your probe
page — and they apply on shared machines too, where your other proxy options do
not.

| Refused | Where | What it covers |
|---|---|---|
| Requests with no user agent | Every project | A request that names no user agent at all. Every browser, crawler, HTTP library — curl, Python's requests, Go, Node's fetch — and every payment or Git webhook sends one; a request without is a script written to scan |
| WordPress paths | Projects that are not WordPress | `/xmlrpc.php`; anything under `/wp-admin`, `/wp-includes`, `/wp-json`, `/wp-content/plugins`, `/wp-content/themes` and `/wp-content/mu-plugins`; `wp-*.php` anywhere (`wp-login.php`, `wp-cron.php`); `/wp-admin/` and `/wp-includes/` under a subdirectory; code files (`.php`, `.phtml`, `.phar`) under `/wp-content/`; and PHPUnit's `vendor/phpunit/` and `eval-stdin.php` |
| `.php` | Projects that run Node.js or Go | A `.php`, `.phtml` or `.phar` file anywhere in the path, `/index.php/admin` included |

Not refused, because real sites serve them: `/wp-content/uploads/` (a site
moved off WordPress keeps those links), `/vendor/` as a whole (Laravel
publishes its packages' files there), `/config.json` (single-page applications
load one) and `/robots.txt`.

### SQL injection

The probe guard also knows the shapes of SQL injection a scanner tries —
`UNION SELECT`, `' OR 1=1`, `SLEEP(5)`, `; DROP TABLE` — in the address: the
path and the query string, never what a form posts. On the Edge tab, under
**Refused by default**, choose what it does with them:

| Setting | What happens |
|---|---|
| Count only | The default. The request reaches your site, and the [Metrics tab](monitoring.md#traffic) says how many carried it |
| Refuse | Answered as a probe, a 404 with your probe page, and counted as refused |
| Off | Neither |

Count first, and look at what was counted: if it is all scanners, refuse. It
stops a scanner working through its list, not somebody who has studied your
site — queries with bound parameters are what keep a database safe.

**Test a path** on the Edge tab says when one of these is what refuses it, and
which switch to turn off. Turn one off if a client of yours really does call
your site without a user agent, or if your site serves a path from the list.

### Your own probe rules

On the project's Edge tab, under **Probe rules**: paths your site never serves
and scanners ask it for anyway — `/xmlrpc.php` on a WordPress that does not use
it, an admin path you moved, backups left lying around. They are refused the
same way as the platform's — a 404 and your probe page — on every environment,
after the platform's own rules.

| Kind | Example | Matches |
|---|---|---|
| Exact paths | `/xmlrpc.php` | That path, ignoring case and a trailing slash |
| Path prefixes | `/old-admin/` | Everything beneath it, ignoring case |
| Globs | `*.bak`, `/wp-json/*/users` | `*` within one segment, `**` across segments, `?` one character, ignoring case. Without a slash it matches the last segment at any depth; with one, the whole path from the start |
| Patterns | `^/api/v[0-9]+/debug` | A regular expression in RE2 syntax, anywhere in the path unless anchored, case-sensitive unless you add `(?i)` |

One rule per line, at most 50 in all, each up to 200 characters. A rule that
would refuse your home page — and with it the site — is refused, and so are the
pattern features RE2 lacks: look-ahead, look-behind, back-references.

**Test a path** under the rules says which rule would refuse it, yours or the
platform's, against the rules as saved.

Some rules worth having, by what they are for:

| Kind | Rule | Refuses |
|---|---|---|
| Exact path | `/xmlrpc.php` | WordPress's XML-RPC endpoint, on a site that does not publish from an app |
| Exact path | `/user/register` | Drupal's sign-up form, on a site where nobody signs up |
| Path prefix | `/old-admin/` | An admin area you moved, which bots still try |
| Path prefix | `/wp-json/wp/v2/users` | WordPress's user list, which hands out login names |
| Glob | `*.bak`, `*.old`, `*.orig` | Backup copies of any file, at any depth |
| Glob | `*.sql`, `*.sql.gz`, `*.zip` | Database dumps and archives left in the web root |
| Glob | `/wp-content/uploads/**/*.php` | A PHP file in uploads, which is never one you put there |
| Pattern | `^/api/v[0-9]+/debug` | A debugging route in every API version |
| Pattern | `(?i)/(phpmyadmin|pma|adminer)` | Database tools that are not installed, at any depth |

Test a few real paths of your own site afterwards — a rule that is broader than
meant refuses pages your visitors want, with a 404 that looks like a missing
page.

A machine needs agent 0.5.136 or later to apply them. Until it has upgraded,
which it does on its own, the tab names it and it keeps the platform's rules
alone.

## Your rules

Two places, because they answer two different questions:

| Set on | What | Applies to |
|---|---|---|
| The project's **Edge** tab | Blocked addresses, blocked user agents, rate limit, compression, HSTS, your own probe rules, and the pages the edge serves in place of your site | Every environment of the project |
| An environment's **Routing** tab | Allowed addresses, password protection, keeping a visitor on one machine | That environment |

Who is turned away and how fast a visitor may ask are decisions about the site,
so they are made once. Who may reach a copy of it differs between copies:
staging can be locked to the office while production is open.

The Edge tab is one form with one **Save**: the options and the pages are
checked together, and if anything is refused nothing is kept.

**Not on shared infrastructure.** On a machine the platform shares between
customers the proxy is not yours alone, so none of these apply there — neither
the project's options nor an environment's own. The Edge tab names any
environment they skip, and an environment's Routing tab says so. Your own
machines, dedicated or shared between your own environments, have them all.
The pages the edge serves apply everywhere.

### Allowed and blocked addresses

One address or range per line, IPv4 or IPv6 — `203.0.113.7`,
`203.0.113.0/24`, `2001:db8::/32`.

- **Allowed addresses**, per environment: when there are any, only those can
  reach the environment. Empty means everyone. This is how a staging site is
  kept to the office.
- **Blocked addresses**, for the whole project: refused on every environment
  **even if its allow list permits them**. That is what you want when the
  address is a scraper rather than a stranger.

A refused visitor gets a **403**.

```
# Allowed addresses on staging: the office, and the VPN everybody else uses
203.0.113.0/24
198.51.100.14

# Blocked addresses on the project: a scraper's network, IPv4 and IPv6
192.0.2.0/24
2001:db8:42::/48
```

**Behind a CDN, the address is the one the CDN reports.** Every request
arrives from the CDN, so the rules are matched against the visitor address it
passes on — the platform CDN's, or the header of the network you named on
your domain — and only on requests that came from that CDN's published edge
addresses. Anything connecting directly is judged by its own address, so the
header cannot be forged past the rules. For a CDN that publishes no edge
addresses — Akamai, or one the platform has no entry for — the header is never
believed: a block list stops nobody and an allow list stops everybody.
[Domains](domains.md#behind-a-cdn-of-your-own) has the details.

### Blocked user agents

One per line, matched anywhere in the user agent and ignoring case — `GPTBot`
is enough. A match gets the same 403.

This is for crawlers that name themselves honestly. Scanners send an ordinary
browser's user agent, so blocking by name will not stop them — that is what the
probe guard and the address lists are for.

```
# Crawlers gathering text to train language models
GPTBot
ClaudeBot
CCBot
Bytespider
meta-externalagent

# SEO tools crawling for somebody else's report
AhrefsBot
SemrushBot
MJ12bot
```

Leave `Googlebot` and `bingbot` alone unless you mean to leave the search
results: blocking them is what that does.

### Password protection

One `user:hash` per line, the format `htpasswd` writes:

```
htpasswd -nB reviewer
```

A plain password is refused. It would be readable by everyone who can open the
Routing tab.

The line `htpasswd -nB reviewer` prints, once it has asked for the password, is
what goes in the box:

```
reviewer:$2y$05$4kcvXm6qX1xJt8Lr2dYQ3e7oQ0Sx9uM3cB1tFhZ5u3pQe6rA2wC1y
```

One per person who reviews the site, so one can be taken away without telling
everybody a new password.

It covers **every hostname of the environment**, the platform hostname
included — a password that only guarded your own domain would leave the site
open at its `vallic.cloud` address. The two things that pass without it are the
challenge a certificate authority uses to issue your certificate, and the
platform's own health check.

Use `-B` (bcrypt). The form accepts some older hash formats as well, but bcrypt
is the one to rely on.

### Rate limit

**Requests per second** a single visitor can average, and a **Burst**
it may go over that by for a moment. Zero means no limit; a burst left at zero
is the same as the rate. A client over the limit is answered **429 Too Many
Requests** until it slows down.

The address counted is **your visitor's**. With nothing in front, that is the
address that connects. Behind our CDN, Cloudflare or Fastly it is the visitor
address the CDN reports — believed only from that CDN's own edge servers, so
somebody going round the CDN cannot pick an address to be counted as.

Reasonable starting points, to watch and adjust:

| Site | Requests per second | Burst |
|---|---|---|
| A brochure site or a blog | 10 | 50 — one page and its images arrive together |
| A shop, behind a CDN that serves the images | 15 | 40 |
| An API that apps call | 5 | 10 |

Set it with the site's own traffic in mind: a visitor loading a page that pulls
thirty images and scripts from your origin needs a burst of thirty to see it
whole.

Behind any other CDN the platform has no way to tell the visitor's address
from a forged one, so the limit counts **per CDN edge server**: everyone that
server carries shares the budget. The page says so when it applies; set the
limit with that in mind, or leave rate limiting to the CDN.

The limit is set once for the project and counted on each environment
separately: a visitor's requests to staging do not use up their allowance on
production.

### Compression

**Compress responses** has the proxy compress what your application sends —
gzip, Brotli or zstd, whichever the browser asks for. On unless you turn it off.
A response your application or a CDN already compressed says so, and is passed
on as it is: nothing is compressed twice.

### HSTS

**HSTS max-age, in seconds** tells browsers to refuse plain HTTP for your site
for that long. Zero, the default, does not send the header.

**Browsers remember it**, so a value set by mistake outlives the mistake. 604800
(a week) is a sensible first step; raise it once you are sure.

| Value | For |
|---|---|
| `300` | Five minutes: trying it out, on a site you can afford to be wrong about |
| `604800` | A week: the first real step |
| `31536000` | A year: once every hostname of every environment has served HTTPS for a while | The header is
sent without `includeSubDomains` or `preload`, and there is no setting for
either.

### Keeping a visitor on one machine

Only offered where an environment answers from more than one web machine. See
the note on the form: it is for an application that keeps sessions on local disk
and cannot move them, and it costs you some of what the second machine was for.

## Taking a site offline

**Show the offline page instead of the site**, on the Routing tab. Visitors
get a **503** with a `Retry-After`, which tells search engines the outage is
temporary rather than the page gone. You can add a one-line note for visitors.

**The site keeps running behind it.** Deploys, the shell and restores all still
work, so this is the switch for work you would rather nobody watched — a large
migration, a restore, a content freeze.

There is **no bypass**: no address, cookie or hostname that sees the site while
it is offline. Check your work on staging, or turn the page off again to look.

It needs the Admin role. [Deploys](deployments.md) do not use it: a deploy
never takes your site offline on its own.

## Your own pages

On the project's Edge tab, under **Pages the edge serves**: what a visitor
sees when the edge answers instead of your site — while it is offline or being
deployed (503), when a request is blocked (403) or refused as a probe (404),
and when your application does not answer (502) or answers too slowly (504).
Leave one empty and visitors get the platform's page, which is branded Vallic.

Each is complete HTML, served as written. It must not load anything from the
site it stands in for — that is what is unreachable — so inline the styles and
embed any image. A page is sent to every machine the project runs on, so each
has a size limit, and the form says which is too large. The form lists the
placeholders each page can use, such as the time and the region, and links to
the platform's own page for comparison.

## When your application is not answering

If your application returns a **502**, **503** or **504**, or does not answer at
all, visitors get the platform's page for it instead of your application's
response — or [your own page](#your-own-pages) for it. That includes a 503 your
application sends deliberately: its own maintenance page is replaced.

## When a change takes effect

Saving does not restart anything. The proxy's configuration is rebuilt from
what is saved and sent to the machine, and the proxy reloads it without
dropping connections.

Saving sends it: every machine the environment runs on is asked to rebuild its
configuration, and picks that up within about a minute. That applies to
everything on this page: the project's Edge tab, an environment's Routing tab,
and taking a site offline.

## Who can do what

| | Role needed |
|---|---|
| The project's Edge tab — blocks, rate limit, compression, HSTS, probe rules, pages | Admin |
| An environment's own — allowed addresses, password, one machine per visitor | Developer — Owner on a protected environment |
| Taking the site offline | Admin |

See [Teams](teams.md).

## Next

- [Domains](domains.md) — the hostnames these rules apply to
- [CDN](cdn.md) — caching and blocking by country, further out

Source: https://docs.vallic.com/edge.md

---

# Deployments

> Connecting your repository, deploying on push, what a deploy does in what order, and going back to an earlier release.

A deploy has two halves. A **build** turns one commit into a **release**: a
packed copy of your application, made once. A **deployment** puts a release on
an environment. Keeping them apart is what makes staging and production run
the same bytes rather than two builds of the same commit, and what makes going
back a matter of deploying an older release rather than building one again.

## Connecting your repository

A repository host is connected once, on the team, and every project on the
team can then use it. It needs the Admin role.

| Host | How it connects | Self-hosted |
|---|---|---|
| GitHub | Install the Vallic GitHub App, from the team's settings, on the repositories you choose | No — github.com only |
| GitLab | An access token with read access, and the address of the instance | Yes |
| Gitea | The same | Yes |

**GitHub** needs nothing else: the App receives your pushes by itself.

**GitLab and Gitea** need a webhook adding on the host. When you save the
connection, the console shows the webhook address and its secret **once**.
Add a push webhook with both, because without it pushes never reach us. The
token is checked against the host before it is kept, and is stored encrypted
and never shown again.

Then attach a repository to the project. **A project's repository cannot be
changed afterwards** — its releases, its history and its branches all belong to
that repository. If the code has moved, reconnect (for a renamed repository or
a reinstalled App, which is safe — pushes are matched by the host's own
repository id, not its name) or create a project for the new one.

## Which branch, and deploying on push

Each environment tracks one branch, chosen under **Source** on the environment.
Two environments of one project cannot track the same branch: they would be
two copies of one site, both built from every push.

**Deploy on push** is off until you turn it on — production included. When it
is on, a push to the tracked branch builds that commit and deploys it to every
environment that tracks the branch and has it switched on.

When it is off, **a push does nothing**, not even a build. You deploy when you
decide to, from the console.

- **Only branches.** Pushing a tag deploys nothing.
- **The same commit twice is not two deploys** on GitLab and Gitea: a push of
  a commit already built from that branch is skipped. Use Redeploy.
- **Switching the branch does not move what is running.** The next push, or
  the next deploy you ask for, builds from the new one. Until then the old
  branch's release can still be redeployed, but not rolled back to an older
  one of that branch.
- **On GitHub, deleting a branch deletes the development environments built
  from it**, along with their backups — a feature branch that has been merged
  and deleted takes its environment with it. GitLab and Gitea do not do this.

## Deploying by hand

**Deploy** on the environment asks one question first:

- **Build the latest from *branch*** — shows the commit the branch points at
  and its message, builds it now and deploys it here when the build is done,
  whether or not Deploy on push is on.
- **Deploy an existing release** — the last ten releases built from this
  environment's branch, each with its number, commit, message and age. The one
  running now is marked; choosing it deploys it again. The last three this
  environment unpacked are marked **on the machine** and go live at once;
  older ones say **from storage** and are downloaded again first.

**Skip deploy steps** applies to either: a new build goes live without them
when it finishes, and only here — another environment watching the branch runs
its own. `vallic build --deploy --skip-steps` does the same.

**Each environment numbers its own releases.** Production's release 4 and
staging's release 12 can be the same build, or different branches entirely: an
environment counts the builds of the branch it runs and anything deployed to
it. The numbers on an environment's pages, in its notifications and in
`vallic release list <environment>` are that environment's; it is also what
`vallic deploy --release` takes. The API keeps the project's own number as
`number`, and gives every environment's under `numbers`.

**An environment only runs its own branch.** A release built from another
branch is not offered, and the API refuses it too. To get `main` onto a
development environment, merge `main` into that environment's branch and push;
to get a feature into production, merge it into production's branch. What an
environment runs is always what its branch says.

You cannot build an arbitrary commit from the console. Push it to a branch.

One deployment at a time per environment. While one is queued or running,
another is **refused**, not queued behind it — including one started by a
push. A push whose build finishes while the environment is busy is not
deployed there; deploy it by hand when the first is done.

Deploying needs the Developer role, on every environment.

## What a deploy does

The build runs first, on one of your project's own machines:

1. The repository is cloned at the exact commit.
2. The `build` steps from [`vallic.yaml`](configuration.md) run, and nothing
   else — nothing is guessed from your files.
3. The result is packed, checksummed and stored. That is the release.

A build has 25 minutes. Its log is under **Activity**.

Then, on the environment:

1. `vallic.yaml` is read at the deployed commit. A missing variable, an
   unknown service or anything else it cannot satisfy **refuses the deploy**
   before anything changes, listing every reason at once.
2. If the release asks for a service the environment does not run yet, or a
   different runtime version, that is set up first.
3. The release is downloaded, its checksum verified, and unpacked beside the
   one that is running. The directories that outlive a release — your
   framework's files directory, and any `mounts` — are linked into it.
4. **The switch.** The `current` link is moved to the new release in one
   step. There is no moment where half the files are old and half are new.
5. **PHP-FPM is reloaded gracefully.** Requests in flight finish on the old
   code; the next ones get the new. Workers and non-PHP applications are
   restarted.
6. The `deploy` steps run — **against the release that is already live**.
7. The `health` path is asked until it answers, if you named one.
8. Cron is written for the new release, and old releases are tidied away.

If the new release's stack will not start at step 5, the switch is undone and
the previous release keeps serving.

### Your site stays up

**The platform does not put your site into maintenance mode during a
deploy**, and does not take it offline. Visitors are served throughout —
by the old release up to the switch, and by the new one after it.

The consequence is step 6: your migrations run while the new code is already
answering requests. For most changes that is the right trade — a site that
never goes down for a deploy — but a migration the new code cannot run
without will see a few requests arrive before it has finished. If a change
needs the site quiet, turn on your framework's own maintenance mode as the
first deploy step and off as the last, or take the site offline yourself
from the [Routing tab](edge.md#taking-a-site-offline).

### When a deploy step fails

The deployment is marked **failed**, and by default **the new release stays
live**: its code is already serving, and the platform does not guess whether
going back is safer than staying.

With `on_failure: rollback` in `vallic.yaml`, the previous release is put back
instead. See [Configuration](configuration.md#deploy).

**Either way your database is not touched.** A migration that ran half-way has
changed the schema, and no release switch undoes that — which is what the
backup you take before a risky migration is for.

### The first deploy

An environment's first release skips the deploy steps when the environment has
a database. Nothing is installed in it yet, so a step that updates a schema —
`drush deploy`, `artisan migrate` — would fail and take the release with it.
Install the site or import a database once the release is serving; every
deploy after that runs the steps. Retrying a failed first deploy skips them
too.

Any deploy can skip them on request: tick **Skip deploy steps** in the Deploy
dialog, run `vallic deploy --skip-steps`, or send `{"skip_steps": true}` to the
API. Use it for a step that is failing, or before importing a database.

### More than one web machine

One machine deploys first and is the only one that runs the deploy steps and
cron, so a migration runs once rather than once per machine. The others then
follow **one at a time**, each taken out of rotation while it switches where
the load balancer allows it, so the site never has all its machines
switching at once. If the first machine fails, the rest are not deployed.

## Going back

Open **Deploy** and choose an earlier release of the environment's branch. It is deployed like any other:
the same artifact, the same switch, the deploy steps run again.

That makes it quick, because nothing is built. It also means **going back is
code only**. Nothing reverses a migration; if the release you are leaving
changed the schema, the older code meets the newer schema. Deploy steps that
are safe to run twice — `drush deploy`, `migrate` — are what keep that
survivable.

**Redeploy** deploys the running release again — to re-run deploy steps that
failed for a reason that has since gone, for example.

### How far back

Each machine keeps the live release and the two before it on disk. Older
releases are kept in storage: the five newest always, and others for up to 30
days, to at most ten — never counting away one that is deployed somewhere. A release older than that has expired
and can no longer be deployed — build it again from its commit.

## Following a deploy

**Activity** on the team lists every build and deployment, with a filter for
them. Each one shows its full log, including what your build and deploy steps
printed. Anyone on the team can read it.

**On GitHub**, each commit gets check runs: *Vallic / build* for the build,
and *Vallic / environment* for each deployment it went to, linking back to
the log. GitLab and Gitea get no commit statuses.

## Who can do what

| | Role needed |
|---|---|
| Read builds, deployments and their logs | Viewer |
| Deploy, redeploy, go back | Developer |
| Change an environment's branch or Deploy on push | Developer — Owner on a protected environment |
| Connect a host, attach a repository | Admin |

See [Teams](teams.md).

## Next

- [Configuration](configuration.md) — the `build`, `deploy` and `health` keys
- [Backups](backup-storage.md) — the copy to take before a risky migration

Source: https://docs.vallic.com/deployments.md

---

# Variables

> Configuration that differs between environments, where it is set, and which names you cannot use.

Variables are how an environment differs from its siblings without the code
differing. They are written into the application's environment when you apply
them, or with the next deploy.

## Two places to set them

**On the project**, where they apply to every environment. **On an
environment**, where they apply there and nowhere else.

An environment defining a name the project already defines replaces it, there
only. That is the entire rule — there is no third scope and no precedence
table.

The console says which is which. A variable listed as *inherited from project*
is not editable on the environment page: editing it there would silently create
an override, and the next person to change the project value would wonder why
one environment ignored them.

## Secrets

Mark a variable secret and its value is encrypted at rest and never shown
again — not in the list, not in the edit form, not in a backup you can read.
You can replace it; you cannot read it back.

Mark anything secret that would matter if it leaked: API keys, tokens, the mail
relay password. There is no cost to marking too much.

Anybody from Developer up can add and change ordinary variables. A secret is
replaced or removed by an Admin or Owner, and it cannot be turned back into a
plain variable: a value that has been secret should be rotated, not revealed.

### Pasting a .env

**Add variable → Bulk** takes a pasted `.env` and lists what it read, one row
per variable, each with its own **Secret** box. A name containing any of
`KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `PASSWD`, `PASS`, `PRIVATE`, `DSN`,
`CREDENTIAL` or `AUTH` arrives with the box already ticked — `STRIPE_API_KEY`,
`DATABASE_DSN`, `SMTP_PASS`. It is a match anywhere in the name, so
`MONKEY_MODE` and `PASSPORT_URL` are ticked too: untick a row before saving
when its value is safe to show, because once saved as a secret it stays one.

Adding a single variable ticks nothing for you; tick **Secret** yourself.

## Names the platform owns

Some names are set for you and cannot be redefined. Redefining them would
either break the environment or point it at another tenant's service, so the
form refuses them when you type them.

They are also the contract your code reads. Every value below is written into
the environment's `.env` — the container's environment — and is what your
`settings.php`, `config/database.php` or `wp-config.php` should read instead
of anything committed to the repository. A value marked *absent when* is only
there when the condition holds, so read it with a fallback rather than
assuming it.

#### Where the site is

| Name | What it is |
| --- | --- |
| `VALLIC_ENVIRONMENT` | The environment's name — `production`, `staging`, `pr-42`. |
| `VALLIC_ENVIRONMENT_TYPE` | What kind it is: `production`, `staging`, `development` or `preview`. The value to switch behaviour on — error verbosity, an environment indicator, whether to send real mail. |
| `PROJECT_MODE` | The same as `VALLIC_ENVIRONMENT_TYPE`, under the name older images read. |
| `VALLIC_SLUG` | The environment's slug, `{project}-{environment}`, unique across the platform. |
| `PROJECT_NAME` | The same as `VALLIC_SLUG`, under the name the images read. |
| `VALLIC_HOSTNAMES` | Every hostname the edge routes to this environment, comma-separated. Trust these Host headers and no others. Absent until something routes here. |
| `PROJECT_BASE_URL` | `https://` and the primary hostname — the one every other hostname redirects to. What to write into absolute links when no request says. Absent until something routes here. |
| `DRUSH_OPTIONS_URI` | Drupal projects only: `PROJECT_BASE_URL` under the name Drush reads, so cron and one-off commands write the same URLs a request would. |

#### Secrets

| Name | What it is |
| --- | --- |
| `VALLIC_ENTROPY` | 256 random bits, base64-encoded, generated once per environment and never changed. Derive the site's own secrets from it — Drupal's hash salt, WordPress's salts — rather than committing them. Arrives through the secret channel, so it is never in a task payload. |

#### Database

| Name | What it is |
| --- | --- |
| `DB_HOST` | The database service, reachable by this name from every container. Absent when the stack runs no database. |
| `DB_PORT` | Its port. |
| `DB_DRIVER` | `mysql` for MariaDB, `pgsql` for PostgreSQL. |
| `DB_NAME` | The database, created on the service's first start. |
| `DB_USER` | The application's user — not the superuser. |
| `DB_PASSWORD` | Its password. Generated once with the environment and never regenerated. Arrives through the secret channel. |
| `DB_ROOT_PASSWORD` | The superuser's password, on engines that have one. For a migration or a repair, not for the site. |
| `DATABASE_URL` | The same credentials as one connection string, which is what Doctrine reads and what most Node and Go drivers accept. Arrives through the secret channel, because it carries the password. |
| `DB_CONNECTION` | The same value as `DB_DRIVER`, under the name Laravel reads. |
| `DB_DATABASE` | The same value as `DB_NAME`, under the name Laravel reads. |
| `DB_USERNAME` | The same value as `DB_USER`, under the name Laravel reads. |

#### Services in the stack

| Name | What it is |
| --- | --- |
| `REDIS_HOST` | The cache service, whether it is Redis or Valkey — both speak the same protocol and the name is what the Drupal redis module reads. Absent when the stack runs neither. |
| `REDIS_PORT` | Its port, `6379`. |
| `VALKEY_HOST` | The Valkey service by its own name, when the stack runs Valkey. |
| `SOLR_HOST` | The Solr service, when the stack runs it. Port 8983. |
| `SOLR_USER` | The user Solr's login accepts, `solr`. Solr refuses a request without it. |
| `SOLR_PASSWORD` | Its password. Derived per environment and never regenerated. Arrives through the secret channel. |
| `VARNISH_HOST` | The HTTP cache in front of the application, when the stack runs it — where to send purge requests. A purge sent here needs no key. |
| `VARNISH_PURGE_KEY` | The key a purge needs when it comes in from outside, in an `X-VC-Purge-Key` header. Arrives through the secret channel. |
| `VARNISH_SECRET` | The secret for Varnish's admin interface (`varnishadm`, port 6082), for anything that drives the cache that way rather than by HTTP purge. Derived per environment. Arrives through the secret channel. |
| `SMTP_HOST` | The mail relay. Port 25 from inside the stack, unauthenticated; the relay is what talks to the outside world, through the provider set as its `RELAY_HOST` on port 587 — direct delivery on port 25 is blocked by most cloud providers. |
| `MEILISEARCH_HOST` | The Meilisearch service, when the stack runs it. Port 7700. |
| `RABBITMQ_HOST` | The RabbitMQ broker, when the stack runs it. |
| `RABBITMQ_PORT` | Its AMQP port, `5672`. |
| `RABBITMQ_USER` | The user to connect as, `app`. The image's `guest` is removed. |
| `RABBITMQ_PASSWORD` | Its password. Derived per environment and never regenerated. Arrives through the secret channel. |
| `RABBITMQ_URL` | The same as one `amqp://` URL, on the default virtual host. Arrives through the secret channel, because it carries the password. |
| `MEILI_MASTER_KEY` | Meilisearch's master key, which the service starts with and the application authenticates with. Derived per environment and never regenerated. Arrives through the secret channel. |

#### CDN

| Name | What it is |
| --- | --- |
| `VALLIC_CDN_PURGE_SOCKET` | The Unix socket to send a purge through. Present only when the project has bought a CDN. |
| `VALLIC_CDN_PURGE_URL` | The URL to POST a purge to, through that socket. Only its path matters. |
| `VALLIC_CDN_PURGE_TOKEN` | Says which environment is asking. It is not a CDN credential and cannot reach another environment's cache. Arrives through the secret channel. |

#### Files

| Name | What it is |
| --- | --- |
| `VALLIC_PUBLIC_DIR` | Absolute path, inside the container, of the directory the framework serves uploads from. It outlives every release. |
| `VALLIC_PRIVATE_DIR` | Absolute path of the directory that is never served — Drupal's private files, Laravel's `storage/app`. It outlives every release. |

#### Paths

| Name | What it is |
| --- | --- |
| `WEB_ROOT` | Where the live release is inside every container: `/var/www/html/current`. The directory above it holds every release kept for rollback. |
| `COMPOSER_ROOT` | The same directory — where `composer.json` is. |
| `DRUPAL_ROOT` | `/var/www/html/current/web`, the document root of a Drupal project. |
| `VALLIC_LOG_DIR` | `/var/log/app`, where an application writes log files it wants collected. Anything written here is shipped with the rest of the environment's logs and kept in the machine's own copy. It outlives every release. |

#### Container images

| Name | What it is |
| --- | --- |
| `MYSQL_DATABASE` | What the MariaDB image reads to create the database on first start: the same value as `DB_NAME`. |
| `MYSQL_USER` | The same value as `DB_USER`, for the MariaDB image. |
| `MYSQL_PASSWORD` | The same value as `DB_PASSWORD`, for the MariaDB image. |
| `MYSQL_ROOT_PASSWORD` | The same value as `DB_ROOT_PASSWORD`, for the MariaDB image. |
| `POSTGRES_DB` | The same value as `DB_NAME`, for the PostgreSQL image. |
| `POSTGRES_USER` | The same value as `DB_USER`, for the PostgreSQL image. |
| `POSTGRES_PASSWORD` | The same value as `DB_PASSWORD`, for the PostgreSQL image. |
| `RABBITMQ_DEFAULT_USER` | The same value as `RABBITMQ_USER`, for the RabbitMQ image. |
| `SOLR_CLOUD_PASSWORD` | The same value as `SOLR_PASSWORD`, for the Solr image: its login, and the digest it signs its ZooKeeper nodes with. |
| `RABBITMQ_DEFAULT_PASS` | The same value as `RABBITMQ_PASSWORD`, for the RabbitMQ image. |

Anything beginning `VALLIC_` or `VC_` is reserved wholesale, so that a variable
the platform adds next year cannot collide with one you added today.

## Who reads them

Your variables reach your application: the site's own container, its queue
worker and its [workers](configuration.md#cron-and-workers). They do not reach
the database, the cache or any other service beside it. Those read only the
platform's values above, so a variable you add cannot reconfigure them by
sharing a name with one of their settings. The one exception is a variable a
service needs from you. For example, OpenSMTPD reads `RELAY_PASSWORD` to log in
to your mail provider.

## What you can change

Every setting the services take, in one place. These are not project variables:
they configure the service itself, so they go under that service in
[`vallic.yaml`](configuration.md) rather than on the Variables page.
[Service settings](service-settings.md) says what each one does.

| Service | Variables |
| --- | --- |
| Go | `GOMEMLIMIT`, `GOMAXPROCS` |
| MariaDB | `MYSQL_MAX_ALLOWED_PACKET`, `MYSQL_INNODB_BUFFER_POOL_SIZE`, `MYSQL_MAX_CONNECTIONS`, `MYSQL_SLOW_QUERY_LOG`, `MYSQL_LONG_QUERY_TIME` |
| Meilisearch | `MEILI_MAX_INDEXING_MEMORY`, `MEILI_LOG_LEVEL` |
| MySQL 8 | `MYSQL_MAX_ALLOWED_PACKET`, `MYSQL_INNODB_BUFFER_POOL_SIZE`, `MYSQL_MAX_CONNECTIONS`, `MYSQL_SLOW_QUERY_LOG`, `MYSQL_LONG_QUERY_TIME` |
| Nginx | `NGINX_CLIENT_MAX_BODY_SIZE`, `NGINX_KEEPALIVE_TIMEOUT`, `NGINX_FASTCGI_READ_TIMEOUT`, `NGINX_GZIP_COMP_LEVEL`, `NGINX_STATIC_EXPIRES`, `NGINX_ERROR_LOG_LEVEL`, `NGINX_DRUPAL_NOT_FOUND_REGEX`, `NGINX_WP_NOT_FOUND_REGEX` |
| Node.js | `NODE_OPTIONS` |
| OpenSMTPD | `RELAY_HOST`, `RELAY_PORT`, `RELAY_PROTO`, `RELAY_USER`, `OPENSMTPD_MAX_MESSAGE_SIZE`, `OPENSMTPD_EXPIRE`, `OPENSMTPD_BOUNCE_WARN` |
| PHP-FPM | `PHP_MEMORY_LIMIT`, `PHP_MAX_EXECUTION_TIME`, `PHP_POST_MAX_SIZE`, `PHP_UPLOAD_MAX_FILESIZE`, `PHP_OPCACHE_MEMORY_CONSUMPTION`, `PHP_APCU_SHM_SIZE`, `PHP_FPM_PM_MAX_CHILDREN`, `PHP_DISPLAY_ERRORS`, `PHP_MAX_INPUT_VARS` |
| PostgreSQL | `POSTGRES_MAX_CONNECTIONS`, `POSTGRES_SHARED_BUFFERS`, `POSTGRES_WORK_MEM` |
| RabbitMQ | `RABBITMQ_VM_MEMORY_HIGH_WATERMARK` |
| Redis | `REDIS_MAXMEMORY_POLICY`, `REDIS_DATABASES` |
| Solr | `SOLR_HEAP`, `SOLR_MODULES` |
| Valkey | `VALKEY_MAXMEMORY_POLICY`, `VALKEY_DATABASES` |
| Vinyl Cache (Varnish) | `VARNISHD_PARAM_DEFAULT_TTL`, `VARNISH_BACKEND_GRACE`, `VARNISH_CACHE_PER_COUNTRY`, `VARNISH_MOBILE_SEPARATE_CASH`, `VARNISH_KEEP_ALL_PARAMS` |

Set them under the service in [`vallic.yaml`](configuration.md), not as
project variables — they configure the service itself rather than your
application, and each service page shows the shape.

Anything not listed here is either owned by the platform or decides where
a service connects, what it is, or whether it starts. Those are refused on
purpose: an application able to set them could break its own environment
in ways nothing here would catch.

### Wiring a site to them

Read the environment; commit nothing that differs between environments. For
Drupal there is a ready-made [`settings.vallic.php`](framework-drupal.md) that
does all of it — the database, the cache, the file paths, the hash salt, which
Host headers to trust — and is the same file on every project.

## When they take effect

Saving a variable does not restart anything. The running containers keep the
environment they were started with, so a change is something you prepare and
then release, not something that happens under a live site the moment you hit
save. Until it is released, the Variables page says **Not applied** and names
the environments still running on the old values.

It is released in one of two ways:

- **Apply variables**, on that notice, or `vallic var apply` from the command
  line. The site and its workers restart with the new values, which takes a
  few seconds; the database and other services keep running. From the
  project's page, or with `--scope project`, it applies to every environment
  still behind, and names them.
- **The next deploy.** A deploy sends any changed variables before the new
  code goes live, so the release starts with them.

Change several at once and apply once: every apply is a restart.

## Next

[Storage](storage.md) covers disks, which is the other thing that differs
between environments.

Source: https://docs.vallic.com/variables.md

---

# Machines

> The two kinds of machine, which way each can be resized, and what it costs in downtime.

A machine can be moved to another type at any time, from **Resize** on the
machines page — to a bigger one always, and to a smaller one on a Cloud Native
machine, whose disk is not part of its type.

## Two kinds of machine

In short:

- **Regular** — the disk comes with the machine. Simple, and the fastest disk
  there is. It can only get bigger: more room means moving to a bigger machine.
- **Cloud Native** — the disk is separate from the machine. Make the machine
  bigger or smaller whenever you like, and grow the disk on its own.

When you configure a project you choose a **machine type** beside the
datacentre, and it decides how the disk works. Not every datacentre sells both;
where only one is offered there is nothing to choose and the question is not
asked.

| | Regular | Cloud Native |
|---|---|---|
| The disk | comes with the machine | a volume of its own |
| Where it lives | on the machine, local storage | on the provider's network |
| Choosing the size | whatever the type includes | yours, from 10 GB up |
| Growing it | move to a larger type — see [It is downtime](#it-is-downtime) below | grow the volume, machine unchanged |
| Best for | most sites | files that grow unpredictably |

**Regular is the default and is what most sites want.** Cores, memory and disk
arrive in one plan at one price, on the fastest disk there is — a device
physically in the machine. The catch is that the disk is part of the plan: more
room means a bigger machine, and a bigger machine means the stop-change-start
below.

**Cloud Native separates the two.** The machine is cores and memory; the disk is
a volume attached to it. That is the one thing a regular plan cannot do, and it
changes two things that matter:

- **Growing storage stops being a resize.** You enlarge the volume and the
  machine stays the type it is. It still pauses for a moment — a filesystem
  cannot be extended while the kernel is running on it — but you are not moving
  to a different machine and not repricing everything else. The resize dialog
  shows each size's monthly price and the difference from now, and the disk's
  line on the bill moves to the new size from the day it grew.
- **You size the disk to the site rather than to the machine.** A machine with
  a lot of memory and modest files stops forcing you to buy a disk to match.

**Nothing comes with it, and that is the point.** You are buying cores and
memory; the disk is a purchase of its own, from 10 GB upwards in tens to 100
and then in fifties. It appears as its own line on the bill, and the rate falls
as the disk grows — the first ten gigabytes are the dearest, because a machine
keeps a fixed amount of any disk whatever its size.

**The size you choose is the size your site gets.** The volume underneath it is
larger — a machine keeps room for the operating system, the container images,
the build cache, the journal, the releases kept for rollback and the nightly
database dumps held before they go offsite — but that is ours to work out, not
yours to subtract. Ten gigabytes bought is ten gigabytes you can fill.

The trade is real and worth knowing before you pick it: **the storage is on the
provider's network, not in the machine.** The included volume is built on the
fastest network tier the provider sells rather than the cheapest, which is what
keeps that difference off the pages your visitors wait for.

## Shared or reserved CPU

Sizes are grouped under two headings.

**Shared CPU** is shared with other workloads and costs less. A busy neighbour
on the same hardware can slow it down for a moment — fine for most sites, and
for every staging copy.

**Reserved CPU** is set aside for this machine alone. Choose it for a busy shop,
an API with steady traffic, or anything you would notice slowing down.

Reserved is not a promise of a speed: it says the cores are yours, not how fast
the processor under them is.

## Which way a machine can move

**A Cloud Native machine moves both ways.** Its disk is a volume of its own, so
changing the cores and memory never touches it, and a machine sized for a launch
that did not happen can be made smaller again. You are shown every type the
provider sells in that region, with its CPU, memory and monthly price.

**A regular machine only goes up.** Its disk comes with the type and grows with
it, and a disk is never made smaller — so once a machine has moved up, the sizes
below it are gone for good. Rather than let you take that step without knowing,
the list only offers the size it is on and larger, and says why.

That is the trade between the two kinds, and it is most of the reason to pick
Cloud Native: not the disk itself, but being able to change your mind.

## It is downtime

This is the part worth reading twice, because it is the opposite of
[growing a disk](storage.md).

Moving a **Regular** machine, and shrinking any machine, is done with the
machine off. So the sequence is:

1. The machine **stops**.
2. Its type changes.
3. It **starts again**.

Everything on that machine is offline for the few minutes this takes. On a
single-machine environment that is the whole site. Plan it like a deploy, not
like a settings change.

**Growing a Cloud Native machine** is the exception, where the provider allows
it: the cores and memory are added while it runs, with nothing offline. Not
when its disk is being grown in the same step — the filesystem cannot be
extended under a running kernel. The resize dialog says which of the two is
about to happen before you confirm.

**A machine resizes with its role.** Every machine doing the same job — the
web servers behind a balancer, say — is kept at one size, because traffic is
shared between them evenly and the smallest would be the ceiling. Resizing
one resizes the others, one at a time, and the dialog says so first.

If the provider refuses the new type, the machine is started again at the size
it was — a refused resize is not left as a machine switched off.

## Scaling for a while

A Cloud Native production machine can also be made bigger for a while
without changing what it was bought at: by autoscale when it is busy, or by
an upscale for seven days. See [Scaling](scaling.md).

## The disk on a regular machine

*This section is about regular machines. On a Cloud Native machine the disk is a
volume of its own and is resized separately — see above.*

The disk is part of the type here, so it grows when the type does. That is the
provider's behaviour and not a choice this platform makes: a larger type comes
with a larger disk, and the data already on it is why nothing ever shrinks it
back.

Which is why the list stops at the size the machine is on. Once it has grown,
the smaller types have less disk than the machine already uses, and the provider
would refuse the move after having stopped the machine for it.

If you need more room without changing the machine, [buy a volume](storage.md).
It grows without downtime and without taking anything away.

## Adding a machine for background work

On a dedicated plan, on either shape, you can add a **worker**: a machine
that runs your schedule and any long-running processes instead of the web
machine doing it alongside serving requests.

Worth it when background work and visitors are competing for the same CPU. The
symptom is a slow *site* rather than a slow job, which is what makes it easy to
miss — see [Shapes](shapes.md).

It is a machine like the others: you pick its size, and it is charged like one.

## What you cannot change

The architecture. An arm machine cannot become an x86 one, because the disk
image on it will not boot there — so only types of the same architecture are
offered.

## Next

[Storage](storage.md) covers disks, which is the other half of making a machine
bigger, and [Upgrades](upgrade.md) covers giving
one service a machine of its own.

Source: https://docs.vallic.com/machines.md

---

# Scaling

> Three ways to give a machine more — resize it for good, let autoscale move it when it is busy, or upscale it for seven days — and how each is billed.

A machine is bought at a size, and that size is what the contract carries.
There are three ways to give it more:

| | For | Billed |
| --- | --- | --- |
| **Resize** | a size that should stay | the new size, at its ordinary price, from the moment it starts |
| **Autoscale** (beta) | traffic you cannot predict | by the day, only while it is bigger |
| **Upscale for seven days** | a week you know will be busy | by the day, for the week |

Autoscale and upscales are for Cloud Native production machines — the ones
whose disk is not part of their size, see [Machines](machines.md) — on a
dedicated plan, and are on the **Autoscale** card of the project's Commercial
terms. A day at a bigger size costs more than the same size bought for good:
paying for a size you are not committed to is the premium, and the
[price list](/cloud/pricing) has it.

## Resize for good

From **Resize** on the machine, on the environment's Resources tab or the
machines page. Any machine, any environment:

- **Bigger** on every machine; **smaller** only on a Cloud Native one, whose
  disk is not part of its type — see
  [which way a machine can move](machines.md#which-way-a-machine-can-move).
- **The contract moves with it.** The new size is billed from the moment the
  machine starts on it, for the rest of the period, at its ordinary price;
  a smaller one is credited the same way.
- **Machines doing the same job move together** — the web servers behind a
  balancer, one at a time.
- **It is downtime** for a few minutes on most machines; growing a Cloud
  Native machine is often done while it runs. The resize dialog says which
  before you confirm — see [it is downtime](machines.md#it-is-downtime).

**Keeping the size autoscale or an upscale chose.** If a machine is bigger
than it was bought at just now and you want it to stay that size, open
**Resize** and choose the size it is on. Nothing is resized: the size goes on
the contract at its ordinary price, and the upscale ends. Choosing any other
size resizes it and ends the upscale too. Either way, that day is billed as
an autoscaled day.

## Autoscale

**Autoscale is in beta.** It works and is billed as described here, and it
is still being proven — so, like every beta feature, it is not covered by the
[uptime commitment](projects.md#support-and-uptime).

Switch it on in the configurator, under **Configure project**, or later on the
**Autoscale** card, and choose for each machine the largest size it may go to.

- **Up:** when its CPU stays above 80% for 15 minutes, it is moved one size
  up — the next size with more cores — as far as the size you chose.
- **Down:** after at least a day on the bigger size, once its CPU has stayed
  below 40% for an hour, it comes back one size, never below the size you
  bought. A database steps down in the project's maintenance window.
- **Which machines:** production machines only, and only those that serve a
  page — the all-in-one machine, the web servers, the database and the load
  balancer. Several web servers resize one after another, so behind a load
  balancer the site stays up.

Switching it on costs nothing; only the time a machine spends bigger is
billed.

## Upscale for seven days

For a launch, a campaign, a week you know will be busy: choose a bigger size
for a production machine on the same card, and it is moved there at once and
put back to the size you bought seven days later — a database in the
project's maintenance window. Choose no upscale to end one early, and it goes
back straight away.

While an upscale lasts, autoscale, if it is on, may take the machine higher
but never below the upscaled size.

## Autoscale and upscales, either way

- **Only CPU and memory change.** The disk stays the size it is.
- **Downtime.** Scaling down restarts the machine, for about a minute.
  Scaling up does on some machines and not on others, and where it does, it
  is about a minute too. [Support](support.md) can tell you which yours are.
- **You are told** each time a machine is moved, when an upscale ends, and
  when a busy machine wanted to go past the size you allowed.
- **Billed by the day, on your next invoice.** Each day, or part of a day, a
  machine spends above the size you bought is charged at a premium over that
  size's daily price, in place of the size you bought; the
  [price list](/cloud/pricing) has the rate.
- **Not on the shared plan**, and not during a free trial.

## Next

- [Machines](machines.md) — the two kinds, and which way each can move
- [Monitoring](monitoring.md) — what the machines are doing

Source: https://docs.vallic.com/scaling.md

---

# Upgrades

> An environment's Resources tab — giving the database, cache or search a machine of its own after a site is built, adding a service or an extra machine, and what each costs.

A site usually starts with everything on one machine. When one service starts
competing with the rest — a search index eating the memory PHP wants, a
database that needs the disk to itself — it can be moved onto a machine of its
own without rebuilding the environment. That is the environment's **Resources**
tab.

It is also where a built environment gains what it does not run yet: a service,
or an [extra machine](extras.md). It moves and adds; it does not change a
[shape](shapes.md) in any other way.

## What it does

The tab shows the environment's topology first, then its machines, then what
it runs and where each service came from. Anybody who can see the environment
sees that much; the changes below it are for Admins and above.

In production, for each service that can move there is a choice:

- **On the web machine**, or
- **A machine of its own**, with a size for it — from the same provider and
  region as the rest of the environment. The monthly price is on each size.

You are shown a summary of what will move before anything happens. The new
machine is built first; the service follows.

**One change at a time.** While a machine of the environment is still being
built, resized or removed, or a database is still moving, the tab says so and
offers nothing to change. It comes back once the last change has finished —
progress is under **Activity**.

## What can move

| Service | Onto its own machine | Back again |
|---|---|---|
| Cache, search, queue | Yes | Yes — the emptied machine is removed |
| The database | Only while the environment is still **one machine** | No |
| Vinyl Cache (Varnish) | No | — |
| The web tier | No | — |

**Move the database first.** It is only offered while everything is still on a
single machine. Once the cache or search has moved off, the database option
disappears, so if you want both, do the database before anything else.

Varnish is not offered because it becomes the front door: moving it would move
where your DNS points, before the machine it points at existed.

Each service gets its own machine. Two moved services cannot share one.

### Where it is not offered

- **Staging, development and preview environments.** Their services are where
  `vallic.yaml` puts them, and the one change the tab offers there is an
  [extra machine](extras.md). Give them more room by [resizing](machines.md)
  their machine instead.
- **The dedicated shape**, where every service already has its own machine.
- **During a [free trial](billing.md#free-trial)**, nothing moves onto a
  machine of its own until the first payment has gone through. Moving a
  service back onto the web machine still works.
- **A shared plan**, which has no machines of its own to draw on. Moving to a
  dedicated plan is a migration we do with you — ask support. A service can
  still be added to the machine it shares.

The tab does not add web machines or workers, and does not change the plan.

## Adding a service

In production, **Add a service** offers what the environment does not run yet.
Anywhere else it is **Add an extra machine**, and offers only that — a service
for staging or development is named in `vallic.yaml`, and the next deploy
starts it.

What can be added:

- **A service** from the catalogue — a cache, a search engine, a queue — that
  the environment has no member of its kind for. One cache, one search engine:
  an environment with Redis is not offered Valkey, because that is a swap.
- **An extra machine**, by its slot name (`pioneer`, `voyager`, …), for the
  containers your repository describes in `.vallic/extra/<slot>.yml`. See
  [Extra machines](extras.md).

Where it runs depends on the environment:

| Environment | A service | An extra |
|---|---|---|
| Production, flexible shape | A machine of its own | A machine of its own |
| Production, dedicated shape | A machine of its own | A machine of its own |
| Staging, development and previews | Through `vallic.yaml` | A machine of its own, at full price |
| A shared plan | Through `vallic.yaml` | Not offered |

A machine of its own is built in the environment's provider and region, at the
size you choose, and added to the subscription from the day it is built. You
are shown what will happen — the machine, its size, what it restarts — before
anything is bought.

**The first machine beside one that ran alone restarts it**, on a provider that
can only connect a running machine to a private network after stopping it
(UpCloud). On staging and development that machine is the one they all share,
so every environment on it is down for a minute or two. Hetzner connects it
without a restart.

A service on the machine that runs the site is not added here: name it in
`vallic.yaml`, and the next deploy starts it. This is the way to give a service
a machine of its own instead.

Never offered: the database (swapping it is a migration — ask support), Varnish
(it becomes the front door), and the web tier. Removing a service is not here
either.

## What each move costs in downtime

**Cache, search or queue:** the service stops while it moves, and **its data is
not copied** — it is rebuilt. A cache refills on its own. A search index has to
be re-indexed by your application. Do not count on jobs still sitting in a queue
surviving the move — let it drain first.

**The database:** **your site shows the offline page for the whole copy.** The
new machine takes a dump of the database from the old one and loads it; nothing
can safely write to the database while that happens. A small database is
minutes; a large one can be the better part of an hour. Plan it for a quiet
time.

The site's address does not change. Nothing you have in DNS needs editing.

## What your application needs

**Nothing, if it reads its connection details from the environment.** The
platform rewrites `DB_HOST`, `REDIS_HOST`, `SOLR_HOST` and the rest to point at
the new machine, over the private network. The credentials and the database name
stay the same.

**An application that has `localhost` or `127.0.0.1` written into its
configuration will lose its database** the moment it moves. Check before you
start — see [Variables](variables.md) and your framework's page.

## The old copy of the database

When the database moves, the copy on the web machine is kept for **72 hours**,
and then removed. It is not served from — it is there in case something about
the move needs looking at. Pointing the site back at it is a support request,
not a button.

You can remove it sooner from the **Storage** tab, and if you set the disk split
yourself, that tab is also where to reclaim the room it leaves.

## Cost

A new machine — moved to or added — is added to the project's subscription at
the price shown beside its size, from when it is built. A machine removed by moving a service back
comes off it. See [Billing](billing.md) for how part-months are charged.

## Who can do it

The **Admin** role, once the project has been built. Support can do it for you
too, if you ask. See [Teams](teams.md).

Progress is under **Activity**.

## Next

- [Machines](machines.md) — resizing, instead of moving
- [Extra machines](extras.md) — what runs on an extra
- [Shapes](shapes.md) — what the layouts are and what each asks of your application

Source: https://docs.vallic.com/upgrade.md

---

# Storage

> Where your files and database live, how to buy more room, and what disk usage counts.

Every environment starts on its machine's own disk. When that is not enough,
you buy a **volume** — a separate disk the provider attaches to the machine —
and move your files onto it.

## Two halves of one disk

Your environment's disk is in two parts, and which part filled up decides what
you do about it.

**Your files.** Uploads, private files, and any directories your `vallic.yaml`
asked for. This half can move onto a volume, and growing it is a purchase.

They are stored under names we choose rather than your framework's: `public`
for whatever your application serves, `private` for what it keeps behind an
access check, and `mounts/` for anything your `vallic.yaml` declared. Your code
still sees its own paths — a Drupal site still writes to
`web/sites/default/files` — but on the disk, in a backup, and in the figures
below, it is `public`.

**The servers' own data.** The database's files, a search index, a message
broker's queues. This half stays on the main disk, and growing it means
[a bigger machine](machines.md).

How much of it you may keep is set by your plan's **memory**, not by how much
disk happens to be free. A database is fast when the rows it keeps asking for
are already in memory; one much larger than the machine's memory is reading
from disk for a growing share of its queries, and it gets slower the bigger it
grows. So a 4 GB machine allows 20 GB of server data, an 8 GB machine 50 GB,
and so on. The console shows where you are against it.

Buying storage does not raise that number. The database always stays on the
machine, so a database that has outgrown its machine needs a bigger machine or
[one of its own](shapes.md).

### Dividing it between services

If your site runs more than one thing that keeps data — a database and a search
index, say — you can give each a share of the allowance. The storage page then
reports each against its own share instead of lumping them together, so when
something grows you can see which one it was.

That matters because the answers differ. A database that has outgrown its
share needs a bigger machine. A search index that has outgrown its share can
often be reindexed, or given a disk of its own. A single total cannot tell you
which situation you are in.

The shares have to add up to no more than the allowance — budgets that each
look healthy while the disk fills would be worse than having none. You can
leave some unallocated, and you can leave a service out entirely, in which case
it is measured against the whole allowance as before.

**These are budgets, not limits.** Nothing stops a service growing past its
share; what a share buys you is being told which one did.

**The database always stays on the machine.** Everything else that fills a
disk can be moved onto one of its own.

## What can have its own disk

Each of these can be given a disk of its own, from the environment's Storage
tab, one row at a time:

- **Public files** — what the site serves
- **Private files**, and with them your declared mounts
- **A search index**, if it has outgrown the machine and you accept that
  searches get slower
- **A queue**

The database always stays on the machine. Services that keep nothing worth
keeping — a cache, a proxy — are not offered a disk either, since they rebuild
themselves from nothing.

## The machine's own disk

On a **Regular** machine the root disk comes with the machine type. It is local
storage, it is the fastest disk there is, and it only changes by moving to a
different type — which is downtime. See [Machines](machines.md).

On a **Cloud Native** machine the root disk is a volume of its own. You pick the
size when you configure the project — from 10 GB up, priced as its own line —
and grow it later without moving the machine. It pauses briefly while the
filesystem is extended; you are not changing machine type and not repricing
anything else.

The size you pick is the size you get. The volume underneath is larger, because
the machine keeps room for the OS, the images, the journal, the releases kept
for rollback and the database dumps held before they go offsite — but that is
the platform's arithmetic, not a subtraction from what you bought.

That is the whole difference between the two, and it is chosen beside the
datacentre when the project is set up.

## Buying more room

A disk is priced by the 5 GB block, and the rate per block falls as the disk grows. Sizes run from 10 GB: fives to 50 GB, tens to 100 GB, then fifties to 1 TB. On the
Storage tab, choose **Own disk…** on the bucket's row and pick a size. The
first size offered is the smallest that holds what is budgeted for that bucket,
because a disk has to be at least as large as what it is about to take over.
The disk is made straight away and shows under Disks while
the provider attaches it; from then on it is on your monthly bill by the block.

The disk arrives empty. Moving what is there onto it is a separate step — see
below — because the site stops while the copy runs.

A disk that filled up is the one storage problem with an answer that does not
touch your files. Choose **Grow** on the volume, pick a bigger size, and the
disk grows underneath the running site — nothing unmounts, nothing restarts,
and no files move.

It happens in two parts, a few minutes apart:

1. The disk itself grows at the provider, and your new size is what you are
   billed from then on.
2. The filesystem on it grows to match, on the machine's next reconcile. Until
   that happens `df` still reports the old figure.

This is the opposite of [resizing the machine itself](machines.md), which does
require stopping it.

**Disks only grow.** A disk cannot be shrunk — the filesystem on it does not
know it is about to lose the blocks its files are on — so a size you pick is a
floor for the rest of that disk's life.

## Moving data between disks

Buying a disk does **not** move the data. The two are separate on purpose:
remounting under a running site would make every file vanish from the site's
point of view without moving a byte, and it looks exactly like the uploads were
deleted.

Moving is its own action, per bucket: **Move data…** on the row once the disk
is ready. It stops the stack, copies that bucket's directory, verifies the
copy, and only then mounts the disk in its place. Nothing is deleted by the
move — the old copy stays where it was, so a move that went somewhere
unexpected is a mount away from being undone rather than a restore from
backup. Freeing that space is a separate, confirmed step under **Left behind**.

## When an environment is deleted

Its disks go with it. A disk on a machine that goes too is removed with the
machine; one on a machine that stays — one that also carries your other
environments — is unmounted there and destroyed. Nothing is kept for a grace
period and billing stops at deletion, so anything you need from the files
belongs in a backup or a download before you delete.

## What disk usage counts

The figures shown for an environment cover everything that is actually on
disk, and they are reported apart rather than as one total:

- **public files** — uploads, anything served to visitors
- **private files** — anything behind an access check
- **the database**
- **releases** — your code, once per release kept for rollback
- **application logs** — whatever your code writes to `/var/log/app`

The database surprises people. A database is a file on a disk like anything
else, and an environment whose usage looked healthy while its database quietly
filled the machine is the outage these numbers exist to prevent.

**Application logs are the one figure with a limit rather than a budget**, and
they are not counted towards the storage you are assigned. The limit is a
tenth of the storage the environment is assigned, between 100 MB and 20 GB;
past it the oldest files are removed, and never the one being written. See
[Logs](logs.md).

They are kept apart because a single total would not tell you what to do. Your
files growing and your database growing have different answers — buy storage,
or take a bigger machine — and only the separate figures say which one you are
looking at.

## Shared storage

On the [Dedicated shape](shapes.md#dedicated), with two or more web machines,
files cannot be local — an upload landing on one web machine does not exist on
the other. So the shared directory is served from
one machine to the others over the **private network**, never the public one,
and the machines write as their own environment's user rather than as root.

This is set up for you. What it does not do is change your application: if the
code writes to a local path instead of the shared directory, shared storage
cannot help it. See [Shapes](shapes.md).

## Next

Back to [Start here](start.md), or [Software stacks](stacks.md) for what runs
on top of all this.

[Backups](backup-storage.md) covers what is copied off the machine, and [Logs](logs.md) what your containers write.

Source: https://docs.vallic.com/storage.md

---

# CDN

> Serving your site from the edge, and how to clear the cache from your own code.

A CDN puts copies of your site in datacentres near your visitors, so a page
loads from the same continent rather than from the machine it is built on. The
platform runs it for you: you buy an amount, and the pull zone, your domains
and their certificates are all set up for you.

## Buying it

In the configurator when you set a project up, or afterwards under **Commercial
terms** on the project's configuration page, under **Edge**. It is bought by
the **terabyte a month**, minimum one.

It is a **commitment, not a meter**. You pay for what you bought whether or not
you use it, which is what lets us sell it without metering every byte. The CDN
tab shows what you have actually served this month, so you can tell whether the
number is the right one before the next invoice rather than after it.

Raising the amount takes effect at once, charged for the rest of the period.
Lowering it, or setting it to zero to give it up, is recorded and takes over
at your next invoice — you keep what you bought until the period it was bought
for ends, and it is not credited. Once zero applies the pull zone is removed,
and your domains stop being served from the edge the moment DNS catches up.
See [Billing](billing.md#changing-something-mid-term).

## Pointing a domain at it

The CDN is for **your own domains**. An environment's `…vallic.cloud` hostname
is deliberately never put behind it — it stays a direct route to the machine,
which is what gives you a way in that does not depend on the cache, and what
lets the pull zone read from it without reading from itself.

So the order is:

1. Add your domain under **Routing** and prove you control it.
2. Point it at the address the CDN tab shows. Copy it from there rather than
   guessing at its shape — it is the one value in this that is not yours.
3. Wait. A certificate is issued once the name resolves to the edge, which
   cannot happen before you publish that record.

The console does the rest: attaching the hostname to the pull zone, asking for
the certificate, and telling you when it is ready. Until the certificate
arrives the name will not serve over HTTPS — that is the CDN waiting for your
DNS, not something to fix here.

An **apex** domain cannot hold a CNAME. If your DNS provider offers ALIAS or
ANAME, point that at the same name; if it does not, use a subdomain such as
`www`. Either way, remove the records it had before: an old **A** or **AAAA**
left beside the new one sends some visitors past the CDN to your old address,
and the certificate cannot be issued while the CDN's own check lands there.

## Clearing the cache from your code

A cache is only useful if you can drop it when you publish. Your application
can, without any credential:

```
VALLIC_CDN_PURGE_SOCKET=/run/vallic-cdn/purge.sock
VALLIC_CDN_PURGE_URL=http://localhost/purge
VALLIC_CDN_PURGE_TOKEN=…
```

They arrive in the environment like every other [variable](variables.md), and
are there only when the CDN is on. Send the request through the socket; the
URL supplies only the path.

### Drupal

Install [Vallic Purge](https://www.drupal.org/project/vallic_purge). It plugs
into the [Purge](https://www.drupal.org/project/purge) module, reads both
variables itself, and clears exactly the pages a change affects the moment
it is saved. There is nothing to configure:

```
composer require drupal/vallic_purge
drush en vallic_purge purge_queuer_coretags purge_processor_lateruntime purge_processor_cron
drush p:purger-add --if-not-exists vallic
```

Export your configuration afterwards, so every environment gets the same
purger on its next deploy.

Then set **Browser and proxy cache maximum age** under *Configuration →
Development → Performance*. Until Drupal says a page may be cached, the CDN
keeps no copy of it, so there is nothing to clear. Pages for signed-in
visitors are sent as `private` and are never cached, whatever this is set to.

Keep the module enabled everywhere. On your laptop, in CI, or on an
environment without the CDN, it sends nothing and adds nothing to your
responses. The status report shows which of those it is.

### Anything else

Send a POST to that address with the token as a bearer:

```json
{"scope": "all"}
{"scope": "url", "values": ["https://www.example.com/news"]}
{"scope": "tag", "values": ["* node:42 *"]}
```

- **all** empties the whole cache. The blunt instrument, and the one to reach
  for after a deploy that changed templates.
- **url** drops individual addresses. Only hostnames this environment answers
  on are accepted; anything else is ignored rather than failing the batch, so
  one mistyped address does not stop the rest.
- **tag** drops every response whose tags match. The precise one, and the one
  worth wiring into a CMS that already knows which pages a change affects.

You never name the pull zone. The request is answered on your own machine, and
which environment is asking is decided by the token — which is why the token
should be treated like any other secret in the environment, and why a CDN
credential never appears in your container.

### Tagging a response

A response is tagged by a `CDN-Tag` header. The CDN keeps that header as **one
string**, not a list, and matches each purge value against the whole string,
with `*` standing for anything. So a page sent with

```
CDN-Tag: : node:42 node_list :
```

is cleared by `* node:42 *`, but not by `node:42`, which only matches a
header that is exactly `node:42`. Write the tags separated by spaces, with a
character at each end as above so the first and last tags have a space on
both sides too, and purge with `* tag *`.

Keep the header short. A busy page can carry hundreds of tags, and headers
that run to many kilobytes can be refused on the way out, which fails the
page instead of caching it. Shorten long tags to a hash of a few characters
and purge by the same hash. The Drupal module writes each tag as eight.

### After a deploy

A deploy does not clear the CDN. Pages your site clears by tag stay fresh as
content changes, but a release that changes templates, stylesheets or scripts
leaves the old pages at the edge until they expire. To start a release from an
empty cache, clear it in the last deploy step.

For Drupal with Vallic Purge:

```yaml
deploy:
  steps:
    - 'drush deploy'
    - 'drush cache:rebuild-external -y'
```

`cache:rebuild-external` goes through Purge's Drush processor, which has to
be added once, like the purger: run `drush p:processor-add drush_purge_invalidate`
and export your configuration. Without it the step fails, and with
`on_failure: rollback` so does the release.

For anything else:

```yaml
deploy:
  steps:
    - '[ -z "$VALLIC_CDN_PURGE_URL" ] || curl -fsS --unix-socket "$VALLIC_CDN_PURGE_SOCKET" -X POST "$VALLIC_CDN_PURGE_URL" -H "Authorization: Bearer $VALLIC_CDN_PURGE_TOKEN" -d "{\"scope\":\"all\"}"'
```

The test at the front skips the call where the CDN is off, so the same
manifest deploys staging too.

Emptying the cache means every page misses at once, so your machine is at
its busiest in the minutes after a release. A site that already clears by tag
often needs this only when the look of the site changed. You can also do it
by hand: **Clear the CDN cache** on the environment's CDN tab, for anybody who
can deploy.

## Who is really asking

Behind a CDN every request reaches your application from the CDN, not from
your visitor — so the address your framework reports is one of a few hundred
edge servers. Two headers carry the truth instead, on every request:

```
X-Vallic-Ip: 203.0.113.9
X-Vallic-Country: DE
```

Read `X-Vallic-Ip` wherever you would have read the client address: rate
limiting, fraud checks, anything that logs who did what. `X-Vallic-Country` is
the visitor's country as a two-letter code, for a price list or a language
default. Only trust them on requests that came through the CDN — somebody
reaching your platform hostname directly can send any header they like.

## What is cached, and what never is

A page your application marks `private`, `no-cache` or `no-store` is **never
cached**, whatever else is configured. That is how a signed-in page stays
somebody's own: every framework already sends one of those on a response that
belongs to a person.

A request from a **signed-in visitor** is answered by your site, never from the
CDN, and reaches their browser as `private, no-cache` — so an editor never sees
the copy of a page that was cached for everybody else. Signed-in means the request carries your
application's session cookie, under its framework's default name:

| Application | Session cookies |
|---|---|
| Drupal | `SESS…`, `SSESS…` |
| WordPress | `wordpress_logged_in_…`, `wp-postpass_…` |
| Laravel | `laravel_session` |
| PHP (Symfony and others) | `PHPSESSID` |
| Node.js | `connect.sid` (Express) |
| Go | none — name yours under **Session cookies** |

A session under another name goes under **Session cookies** on the CDN tab,
below.

Beyond that, these settings on the environment's **CDN** tab are yours:

- **Dynamic cache** lets the CDN decide what is cacheable from your own
  response headers rather than by file type. For a site that will not be
  purging by URL or cache tag. Leave it off if you have tuned your cache
  headers yourself.
- **Browser cache time for pages** and **for static files**, with Dynamic
  cache off: how long visitors' browsers keep a page — 5, 15 or 60 minutes,
  or a day — and your stylesheets, scripts, images and fonts — 1, 7 or 30
  days — before asking again. Told apart by the `Content-Type` your site
  sends. Left at *As your site says*, your own `Cache-Control` decides, and
  a long one means a browser goes on showing an old copy after you purge the
  CDN. Keep the page time short on a site people sign in to: a page somebody
  saw signed out stays in their browser, signed out, for that long after they
  sign in. Signed-in visitors are always told `private, no-cache`, and
  anything your site marks `private`, `no-cache` or `no-store` keeps that. These
  change only what browsers are told; the CDN still keeps a response as long
  as your site says, so your site is not asked more often. A time is set
  rather than capped — a response you mark for one minute is kept for the
  chosen time too.
- **Vary the cache by** WebP or AVIF support, mobile against desktop, country,
  region, hostname or cookie. Each one keeps a separate copy of every page
  per value, and two of them multiply — so tick only what your site genuinely
  serves differently. Cookie takes the names of the cookies that matter — a
  language or currency choice — and only those make a copy; every other
  cookie is ignored. A session cookie cannot be one of them, since that would
  be a copy per visitor — signed-in visitors skip the cache instead.
- **Request headers to vary by**, one name per line — `Accept-Language`, say —
  for a site that answers differently by a header. `Cookie` and `User-Agent`
  cannot be used: both would be close to a copy per visitor.
- **Blocked countries** are refused at the edge and never reach your site.
- **Session cookies**, one name per line — or the start of one, for a cookie
  with a varying suffix — for a session your application names differently
  from its framework's default. A visitor carrying one is answered by your
  site, never from the CDN, and told `private, no-cache`. Up to five; names only, so letters, digits, dots, dashes
  and underscores.
- **Never cache these paths** — `/admin/*`, `/cart` — for anything that should
  always come from your application even though it does not say so itself.
  Browsers are told the same: these paths reach them as `private, no-cache`,
  whatever your site sent. Up to twenty.

They are read from the CDN and written straight back to it. Nothing is kept
separately, so the page always shows what the edge is actually doing.

Some things are decided for every site and are not switches: the query
string is always part of the cache key, so `?page=2` is never served the
first page; a stale copy is served while your site is slow or down rather
than an error; an error response is never stored, so the edge stops serving
it the moment your site answers again; and redirects are passed to the browser rather than followed at the
edge — so your canonical URLs stay enforced.

## How it is bought

By the terabyte a month, committed rather than metered — you pay for what you
bought whether or not you use it, and nothing here counts bytes. A CDN bought
by the hundred gigabytes before it was sold by the terabyte keeps its amount
and its price until you change it; a new amount is rounded up to whole
terabytes.

On one of two networks. The **Volume network** is the one selected to begin
with: it reaches 10 locations across Europe, Asia and North and South America,
costs a fraction as much, and is enough for most sites. The **Standard
network** reaches 119 locations worldwide, and is worth its price when your
visitors are spread out; anybody far from a Volume location gets a slower
first byte. You can move between them
later; the pull zone stays and no hostname is lost. Moving to the Standard
network happens at once; moving to the Volume network is a reduction, so like
any other it takes over at your next invoice.

A project that was already behind the CDN before there was a choice is on the
standard network and stays there until you change it.

The Volume network is one price everywhere. The Standard network is priced by
**the datacentre you chose**, because that is what decides where the bytes
come out: Europe and North America is the cheapest, then Asia and Oceania,
then South America, then the Middle East and Africa. Change the datacentre in
the configurator and the figure moves with it.

The figure itself is quoted where it is agreed, not here: the configurator and
the Commercial terms card both show what will actually be invoiced.

## Next

- [Domains](domains.md) — adding a domain and proving it
- [Variables](variables.md) — where the purge settings arrive

Source: https://docs.vallic.com/cdn.md

---

# Logs

> Where your application's output goes, how long it is kept, and how to send a copy somewhere else.

Everything your containers write — the application, the web server, the
database — is collected on the machine they run on. You do not have to
configure anything for that to happen.

## Logging from your own code

A container has no syslog daemon. Drupal's core **syslog** module sends its
lines to a socket nothing is listening on, so a site configured that way logs
to nowhere at all.

Write files instead. Every application container has a directory at
**`/var/log/app`**, and the same path is in the environment as
`VALLIC_LOG_DIR`. Anything you write there is collected with the rest of your
logs: kept on the machine, and forwarded if you have set up a destination.

For Drupal, the **Monolog** module with a rotating file handler:

```php
// settings.php — or the monolog settings the module reads.
$settings['monolog.channel_handlers']['default'] = ['rotating_file'];
$settings['monolog.handlers']['rotating_file'] = [
  'type' => 'rotating_file',
  'path' => ($_ENV['VALLIC_LOG_DIR'] ?? '/var/log/app') . '/drupal.log',
  'max_files' => 7,
];
```

For Laravel, point the `daily` channel at the same directory — there is a
worked example on the [Laravel](framework-laravel.md) page, along with the
reason it matters there in particular: Laravel's default log path is inside
the release, so it starts again on every deploy.

You can read and clear the directory yourself from the environment shell — it
is `/var/log/app` in there, like everywhere else in the stack.

**There is a limit: a tenth of the storage your environment is assigned,
between 100 MB and 20 GB.** Past it we remove the oldest files first, and
never the one being written.

**It does not count towards the storage you are assigned.** The room your logs
take is ours to manage, not something subtracted from the disk you bought.

Set your logger to rotate and you will never meet the limit; if you need to
keep more than that, forward a copy somewhere that keeps it.

## What is kept, and for how long

**Seven days, on the machine.** A container that has been restarted has not
taken its history with it, and you do not need a third-party account to read
it. Older than that is removed, because a busy site would otherwise fill the
disk its own stack is running on.

If you need longer than a week, send a copy somewhere that keeps it — below.

### The access log

The web server in front of your site always writes an access log, because the
platform counts requests and measures response times from it. By default it
holds only what that counting needs — which site, which path, the status, how
long it took, when. No visitor address, no query string, no user agent: nothing
that is somebody's personal data. The full line, addresses and all, is written
only when full access logging is switched on for the project, and never on a
machine shared with other teams, because one customer cannot consent for
another's visitors. Turning it on makes you the controller of that data.

## Sending a copy somewhere else

Forwarding is an add-on per project, included at no cost. It appears on the
project's invoice, at nothing, as soon as the project sends its logs
somewhere.

Two steps, because the credentials belong to your team rather than to one
project.

1. **Add the collector once, under your team's settings**, in
   **Integrations**. Datadog, New Relic, Logz.io, Axiom, Grafana Loki, Splunk,
   an OpenTelemetry collector, a plain syslog endpoint, or an S3-compatible
   bucket to archive them in. You give it a name, whatever that service needs
   to identify your account, and the token. The token is encrypted and never
   shown again.
2. **Choose it as the destination on the project's configuration page.** Any
   project on the team can point at the same integration, and rotating the
   token in one place fixes all of them. Both steps take the Admin role or
   above.

Forwarding is not offered on the entry line of machines. The logs are still
kept on the machine either way; a larger machine adds sending them somewhere of
your own.

**A plain syslog endpoint is still a destination.** Host, port, protocol, no
token. TCP unless the endpoint insists otherwise: UDP drops messages silently
under exactly the load that produces the logs you want to read.

This is **additive**. It does not stop the machine keeping its own copy, and it
does not replace anything — it is a second destination. Shipping is done by
[Vector](https://vector.dev) on the machine.

### What is sent, and what is not

Only your project's containers. Never another customer's, and never the
machine's own output — the machines are shared, and a log stream that carried
your neighbours' traffic would be somebody else's data arriving in your
account.

If the endpoint is unreachable, messages queue on disk and are delivered when
it comes back rather than being dropped. The logs worth having off the machine
are usually the ones produced while something was wrong, which is exactly when
a destination is most likely down.

## Changes take effect on the next reconcile

Saving a destination sends it to every machine the project runs on, which
picks it up within about a minute.

## Next

[Backups](backup-storage.md) covers the other thing that leaves the machine,
[Monitoring](monitoring.md) covers metrics and availability, and
[Shell access](shell.md) covers reading what is on the machine yourself.

Source: https://docs.vallic.com/logs.md

---

# The Vallic CLI

> Installing the command line, and forty commands for the things people do every week — deploying, shells, files, databases, backups, variables and domains.

The `vallic` command does from a terminal what the console does in a browser,
and a few things only a terminal can: a shell in a container, a database
streamed in or out, a local port onto a service.

## Installing it

```
curl -fsSL https://raw.githubusercontent.com/Vallic/vallic-cli/main/installer.sh | bash
vallic login
```

`vallic login` opens a browser to sign you in. In CI, where there is no
browser, sign in with a token from your account instead: `vallic login
--token`. A token deploys and follows deploys and nothing else — see
[What a token can do](api.md#what-a-token-can-do). Shell and file commands also need an SSH key on your
account — see [Shell access](shell.md#before-you-start).

`vallic self-update` replaces the binary with the version the platform
publishes.

## Which project and environment

Inside a checkout of your project, the CLI works out the project from the git
remote and the environment from the branch, so most commands need neither.
Elsewhere, name the environment and add `--project` with the project's machine
name:

```
vallic ssh staging --project acme-site
```

A branch that matches no environment is refused rather than sent to
production.

## Examples

### Getting set up

```
# Who the stored credential belongs to
vallic whoami

# Record which project this checkout is, once, outside a clone of its repository
vallic link acme-site

# What this checkout points at: project, environment, branch, last deploy
vallic status

# Check vallic.yaml before committing it
vallic validate --against production
```

### Environments

```
# Every environment of the project, and one in full
vallic env list
vallic env info staging

# A development environment from a branch, deployed on every push
vallic env create feature/checkout --type development --auto-deploy

# Point staging at another branch
vallic env source staging --branch release/2.0

# Its address, or open it in the browser
vallic url staging --open
```

### Deploying

```
# Deploy what staging's branch points at, and wait for the result
vallic deploy staging --wait

# Build and deploy in one go
vallic build staging --deploy

# The last five builds, and deploy an earlier one
vallic release list production --limit 5
vallic deploy production --release 41 --wait

# Deploy what is already live again, or put the previous release back
vallic redeploy production --wait
vallic rollback production --wait
```

### What the platform has been doing

```
# Recent deploys and builds, and one task's output as it runs
vallic activity list production --type deploy,build
vallic activity log 1234 --follow
```

### A shell and commands

```
# A shell in the site's container
vallic ssh staging

# One command, then back
vallic ssh staging -- ls -la /mnt/files/public

# drush, in a Drupal project
vallic drush staging -- cr
vallic drush production -- updb -y
```

An environment on several machines is reached through its front, which
carries the login on to the machine you name; without `--machine` it goes
where the site runs. `--container` opens a worker's container instead of the
site's:

```
vallic ssh production --machine web-2
vallic ssh production --container worker-queue-1 -- php artisan queue:failed
```

### Files

```
# Public files up, and back down
vallic mount upload staging ./files
vallic mount download production ./files-from-production

# Private files, and a mount vallic.yaml declares
vallic mount upload staging ./private --area private
vallic mount upload staging ./exports --area mounts/exports

# Every area an environment has, by the name --area takes
vallic mount list staging
```

### Databases

```
# A copy of production's database, then onto staging
vallic db export production > production.sql
vallic db import staging < production.sql

# An interactive prompt
vallic sql staging

# A local port onto the database, for a desktop client
vallic tunnel staging db --port 3307
```

### Backups

```
# The database backups production has, and take one now
vallic backup list production --kind database
vallic backup create production --kind database

# A copy of one to keep, and put the newest back over the environment
vallic backup download 42 production --wait
vallic backup restore latest staging --kind database
```

`backup restore` overwrites the environment with its own backup, and only that
environment's: a backup goes back where it was taken and nowhere else. On a
protected environment an owner has to confirm it in a browser.

### Variables

```
# What is in effect for staging, including the project's
vallic var list staging

# Set a secret, read a value, and restart the site so it takes effect
vallic var set PAYMENT_KEY sk_live_example production --secret
vallic var get API_URL staging
vallic var apply production
```

### Domains

```
# Claim a hostname, then check the DNS records it printed
vallic domain add shop.example.com production
vallic domain verify shop.example.com production
```

Every command has `--help`, with more examples than these.

## Next

- [Shell access](shell.md) — keys, plain SSH, and what the shell can reach
- [Variables](variables.md) — scopes, secrets and when they take effect

Source: https://docs.vallic.com/vallic-cli.md

---

# Shell access

> Logging in to an environment over SSH, copying files and databases in and out, and what the shell can and cannot reach.

Every environment can be reached over SSH, into the container your site runs
in. It is how you run `drush`, look at a file your application wrote, or move a
database from somewhere else.

## Before you start

**Add an SSH key to your account** — **Your keys** on the environment's Shell
access card, or **SSH keys** on your account. Keys belong to you, not to a team: one
key reaches every environment you are allowed a shell on, across all your
teams, and removing it removes it from all of them.

| Accepted | Refused |
|---|---|
| Ed25519 | DSA |
| ECDSA (256, 384 or 521 bits) | RSA under 2048 bits |
| RSA of 2048 bits or more | Hardware security keys (`sk-` types) |

Ed25519 is the one to generate if you are making a new key. A key can only be
on one account.

A new key reaches the machines within a minute or so of saving it.

## With the Vallic CLI

The **Shell access** card shows these commands first. The CLI works out the
host, the port and the account for you, and gets the quoting right. Forty more,
from deploying to domains, are on [The Vallic CLI](vallic-cli.md):

```
curl -fsSL https://raw.githubusercontent.com/Vallic/vallic-cli/main/installer.sh | bash
vallic login
```

| | |
|---|---|
| `vallic ssh staging` | A shell. `vallic ssh staging -- drush cr` runs one command |
| `vallic mount upload staging ./files` | Copies a local directory into the public files directory |
| `vallic mount download staging ./files` | And back down |
| `vallic mount upload staging ./private --area private` | The same for private files, or a mount: `--area mounts/<name>` |
| `vallic mount list staging` | Every area the environment has, by the name `--area` takes |
| `vallic db import staging < dump.sql` | Replaces the database with a dump |
| `vallic db export staging > dump.sql` | Takes a copy of it |
| `vallic sql staging` | An interactive database prompt |
| `vallic tunnel staging db` | A local port onto the database, for a desktop client — see [Tunnels](#tunnels-to-your-services) |

The card adds `--project` with your project's machine name —
`vallic ssh production --project acme-webshop` — so its commands work from
any directory. Inside your project's checkout it can be left off: the project
comes from the git remote, and leaving the environment's name off as well
picks the one tracking your current branch.
The installer checks the download against a checksum the platform publishes,
and installs nothing if it does not match. The CLI uses the same keys and the
same access as plain SSH below; it adds nothing a key would not reach.

## Logging in

Plain SSH works without the CLI. The card shows the command with everything
filled in, under **Plain SSH**:

```
ssh -p 2417 vc-t-acme@production.gwfdxo99.vallic.cloud
```

- **The host** is the environment's platform hostname — never your own domain,
  which may be behind a CDN that does not carry SSH.
- **The port** belongs to the project. Every environment of the project uses
  the same one, and it does not change. It is not 22, so remember `-p`, and
  `-e 'ssh -p …'` for rsync.
- **The user** is your team's account on the machine. It is the same for
  everyone on the team: which person you are is decided by your key.
- **Staging and development add the environment's name** after the host —
  `ssh -t -p 2417 vc-t-acme@staging.gwfdxo99.vallic.cloud staging`. They share
  one machine and the project's port, so the port alone does not say which you
  meant; the name does, and is taken off before anything runs. Keep the `-t`:
  ssh treats the name as a command, and gives a command no terminal unless it
  is asked to, so without it the shell opens and shows nothing. For a command,
  put it after the name: `… staging db-export`. For rsync, pass it as
  `--rsync-path='staging rsync'`. Without a name where one is needed, the login
  is refused with the choices rather than guessed. Production needs none.

If you have several keys loaded, SSH may offer the wrong ones first and be
turned away before it reaches the right one. Name it:

```
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 -p 2417 vc-t-acme@production.gwfdxo99.vallic.cloud
```

## Where you land

**Inside the site's application container, as the user the site runs as** —
not on the machine. Your code, your files and your database are all within
reach; the host, other containers' internals and other customers are not.

| Path | What it is |
|---|---|
| `/var/www/html/current` | The live release. **Read-only** |
| `/mnt/files/public` | Your application's files. Writable, and what survives a release |
| `/var/log/app` | Your application's own log files — see [Logs](logs.md) |

The release is read-only because it is the artifact that was built and
deployed. A fix edited in place would be gone on the next deploy and would
never have been in git; make it in the repository.

**`drush` is your project's own**, from its `vendor/bin`, so it is always the
version your site was built with. The same goes for anything else your build
puts in `vendor/bin` or `bin`. **WP-CLI is not provided** — add it to your
build if your WordPress site needs it.

If the environment has never been deployed, there is nothing to log in to yet,
and the shell says so.

## Several machines, and workers

An environment on more than one machine — a dedicated one, or one with a
service on a machine of its own — answers at one address: its front. The login
is carried on from there to the machine you name, over the environment's
private network, and you log in there with the same key. The **Shell access**
card lists a line per machine; by hand it is ssh's `ProxyCommand`:

```
ssh -t -o ProxyCommand='ssh -p 2417 vc-t-acme@production.gwfdxo99.vallic.cloud jump web-2' -p 2417 vc-t-acme@web-2.production.gwfdxo99.vallic.cloud
```

Where the front runs no site of its own — a load balancer, or Varnish on its
own machine — the card's plain login already goes on to the first web server.

A **worker** runs in a container of its own, named `worker-<name>-<n>` after
the worker in `vallic.yaml` — `worker-queue-1`. Put its name after `@` to open
it instead of the site's: `ssh -t … @worker-queue-1`, on the worker machine
where the environment has one. The CLI takes the short name and finds the
machine: `vallic ssh production --container queue-1`. The workers are listed on
the card, and asking for one that is not on that machine lists what is.

## Copying files

Each environment keeps three directories, separate from each other, and all
of them outlive every deploy:

| Directory | What it holds |
|---|---|
| `/mnt/files/public` | Files the web serves directly — Drupal's `sites/default/files` |
| `/mnt/files/private` | Files served only through your application's access checks |
| `/mnt/files/mounts/<name>` | The extra directories your `vallic.yaml` lists under `mounts` |

`vallic mount upload` and `vallic mount download` reach every one of them:
public by default, the others with `--area private` or `--area mounts/<name>`.
Without the CLI, `rsync` straight into the directory:

```
rsync -avz -e 'ssh -p 2417' ./files/ vc-t-acme@production.gwfdxo99.vallic.cloud:/mnt/files/public/
rsync -avz -e 'ssh -p 2417' ./private/ vc-t-acme@production.gwfdxo99.vallic.cloud:/mnt/files/private/
```

Never copy private files into public: anything there can be fetched by its
URL. The other way round takes a copy out. `scp` works too, with **`scp -O`**
on a recent OpenSSH, which otherwise speaks SFTP by default. **SFTP itself is
not available**, so a graphical SFTP client will not connect.

## Databases

Three commands, run in the database container rather than your application's,
so the right client is always there:

| Command | Does |
|---|---|
| `db-export` | A dump of the environment's database, to standard output |
| `db-import` | Loads a dump from standard input |
| `db-cli` | An interactive `mysql` or `psql` prompt |

Used from your own computer, over SSH:

```
ssh -p 2417 vc-t-acme@production.gwfdxo99.vallic.cloud db-export > dump.sql
ssh -p 2417 vc-t-acme@production.gwfdxo99.vallic.cloud db-import < dump.sql
```

`db-import` loads over what is there. On production that is your live site;
take a [backup](backup-storage.md) first. To bring another environment's data into
staging, **Copy from another environment** under Backups is usually the better
route, because it runs your sanitisation commands.

## Tunnels to your services

`vallic tunnel` opens a port on your own computer that reaches one of your
environment's services, so a desktop client can connect to it: a database tool,
a Redis or Solr browser, `curl` against Varnish.

```
vallic tunnel staging db        # whichever database the stack runs, on 127.0.0.1:3306
vallic tunnel staging redis     # 127.0.0.1:6379
vallic tunnel staging solr      # http://127.0.0.1:8983/solr
vallic tunnel production db --port 13306
```

| Service | Name | Local port |
|---|---|---|
| The database | `db` (or `mariadb`, `mysql`, `postgres`) | 3306, or 5432 for PostgreSQL |
| Redis or Valkey | `redis`, `valkey` | 6379 |
| Memcached | `memcached` | 11211 |
| Solr | `solr` | 8983 |
| Meilisearch | `meilisearch` | 7700 |
| RabbitMQ | `rabbitmq` | 5672 |
| Vinyl Cache (Varnish) | `varnish` | 6081 |

`--port` chooses another local port, for when the usual one is taken or you
have tunnels to two environments open at once. Ctrl-C closes the tunnel.

The credentials are your environment's own. For the database:

```
vallic ssh staging -- printenv DB_NAME DB_USER DB_PASSWORD
```

A tunnel reaches only the service you name, of the environment you name, on its
own port: each connection is one SSH session that the machine hands to that
service. It works where the service runs on the same machine as your site; a
database on a machine of its own is reached with `vallic sql` and `vallic db`.
Who may open one is who may open a shell — below.

## What is not allowed

**No SSH port or agent forwarding.** `ssh -L` and `ssh -A` are refused: an
address and port of your own choosing would be a way past everything on this
page that decides who reaches what. `vallic tunnel` is the way to reach a
service from your computer.

A connection that stops answering is dropped after about three minutes. Five
failed logins from one address within ten minutes blocks that address for an
hour.

## Who can use it

**Developer and above**, on every environment, production included. The shell
is part of operating a site; the protection on production guards reshaping it,
not working on it. Viewers get no card and no access.

Access follows your role: a person narrowed to certain projects reaches only
those, and a suspended team loses shell access along with everything else
above Viewer.

Every login is recorded on our side — who, from which address, to which
environment, and for how long. What is typed inside a session is not. The
record is not shown in the console.

## Next

- [Logs](logs.md) — reading what your application wrote
- [Backups](backup-storage.md) — before you import anything over production

Source: https://docs.vallic.com/shell.md

---

# Backups

> What is copied, where it goes, how long it is kept, and what you have to do to get one back.

Production environments are backed up without you doing anything. Two things
are copied: the **database**, and the **files** your application has written —
uploads, generated exports, anything under the directories that survive a
release.

Your code is not backed up, because it is in git and in a build artifact
already. Restoring a repository from us would be restoring it from a worse
copy.

## Where a copy goes

| Copy | Where |
|---|---|
| Near | object storage (Cloudflare R2) on the same continent as your machines |
| Cold | a different provider, in the EU: Scaleway, in Paris |
| On the machine | the database only, for the last two days |

**Two offsite copies, on different providers.** A backup that shares a fate
with the thing it protects is not a backup — and one that shares a *provider*
with it is only slightly better.

The near copy follows your machines. The cold copy recently moved from
Hetzner in Helsinki to Scaleway in Paris; until the new one holds enough
history, both are written, so nothing already kept is lost in the move.

The copy on the machine is the one that survives a dropped table or a bad
deploy, which are the restores people actually ask for; it is the database
only, because a copy of your files on the same disk as your files protects
against nothing. It keeps everything from the last 48 hours and never fewer
than the three most recent, and a restore reads from it first because nothing
has to leave the machine.

It shares the machine's disk with your database, so it gives way first: once
that disk is 90% full, or has less than 1 GB free, the copy on the machine is
not written — the backup says so as a failed step — and the database keeps the
room it needs to go on working. The offsite copies are written as usual. What
the copy on the machine takes is shown on the environment's **Storage** tab.

## Keeping a copy of your own

Under **Backups** on your team, you can point backups at storage you control
as well. Two steps, because the credentials belong to your team rather than to
one destination:

1. **Add the storage under Integrations**, once — S3-compatible object
   storage, an SFTP host or an rclone remote. On S3, give it the bucket
   backups should go in, and a destination appears on its own.
2. **Otherwise add a destination under Backups** and pick the integration. It
   asks for a name and nothing else: where it writes and what it signs with
   come from the integration, so rotating a key there fixes every backup that
   uses it.

**This is additive.** Connecting your own storage asks for a copy inside your
boundary; it does not stop us keeping ours. It is also how non-production
environments get backed up offsite, since we do not do that for you.

## When they run

Once a day, in a two-hour window at night: 03:00–05:00 in the country your team
is billed in, so a backup is not running while your visitors are. If your
visitors keep other hours, choose the hour and timezone yourself under
**Other → Backup / maintenance schedule** on the project's **Configuration**
page. One window covers every environment of the project, and scheduled
maintenance on your machines — upgrades that restart a service — is done in
the same window. A team with no billing address yet is backed up at
05:00–07:00 local to the continent its machines are on.

**The database can be copied every four hours instead.** That is an add-on on
production, available on either plan and either shape and bought
under Commercial terms. Files stay once a
day whatever the database does: uploads change slowly, and the data somebody
actually loses in an afternoon is in the database. The extra dumps are kept
for a week; beyond that the daily series is what reaches back.

If a backup is missed — the machine was rebuilding, something was down — it
runs as soon as it is noticed rather than waiting for tomorrow. A backup that
keeps failing is worth a [notification](notifications.md): nothing else
notices until somebody needs to restore.

## What is kept

| | Production | Staging | Development |
|---|---|---|---|
| Daily | 4 | 3 | 1 |

Deliberately shallow. The restores people ask for are from the last few days,
and a snapshot from a year ago restored today is a different site rather than
an older one.

**The same on either plan.** How far back a series reaches is not decided by
whether a project is shared or dedicated — it is bought for production, and
changed whenever you like under Commercial terms on the project's configuration
page. Thirty days instead of four keeps a snapshot for each of the last thirty
days, with a weekly and a monthly alongside, and it is the one that catches a
problem nobody noticed at the time — the mistake you find on your next
invoice rather than the same afternoon. The database can also be dumped every four hours
rather than nightly, bought the same way.

The staging and development counts apply where those environments have a
backup at all — on your own storage, or the database copy on their machine;
see [below](#non-production-environments).

**A backup you take by hand is kept for 30 days**, regardless of the counts
above. Somebody taking one is usually about to do something risky, and what it
protects against tends to surface days later — after four dailies would have
rolled it off.

## Non-production environments

**We do not back up staging or development offsite.** What we pay to store is
the data you cannot reproduce, and a staging environment is a deploy away from
existing again.

They keep the database copy on their own machine, which is what saves a botched
migration. If you want them kept properly, connect your own storage.

## Restoring

**Backups → Restore a snapshot** on the environment. It replaces what is there
now — anything written since the snapshot is lost — so it asks you to confirm.

Production can only be restored from **its own** history. Putting staging's
database into production is not a restore; it is replacing the live site with a
copy of somewhere your developers have been working.

## The encryption key

Every repository is encrypted, and **each one has a key of its own**. A
repository is one environment at one destination, so an environment writing to
three places has three repositories with three unrelated keys. That is
deliberate: a key decrypts the whole repository it belongs to, so sharing one
across environments would mean a staging machine holding something that could
read production.

The key is generated when the repository is first created, 256 bits from the
system's random source, and stored encrypted. It is handed to a machine only
for the job that needs it and never written to a task record.

**Losing it makes that repository unreadable** — by you, by us, by anyone.
Restic has no way to change a repository's password without rewriting the
repository, so it is the same key for the life of the backups.

### Taking it with you

Where the destination is **your own storage**, the key is yours to have: the
team's **Backups** page lists every repository on it, with **Show the key**
beside each. With your own credentials and the key you can restore anywhere,
with no involvement from us — `restic -r <repository> restore`. It takes an
Admin or the Owner rather than anybody who can restore, because one string
decrypts every snapshot in that repository, including ones taken before you
joined the team, and cannot be changed afterwards — somebody who leaves the
team keeps it.

On the **platform's own destinations** the key is not offered, and it would not
help: those buckets hold every customer's backups behind credentials that are
ours, so the key would unlock something you cannot reach. What works there is
an export — see below — or [bringing your own destination](#keeping-a-copy-of-your-own),
which is the recommended arrangement for exactly this reason.

## Taking a copy out

**Backups → Prepare a download** turns one snapshot into a plain copy — a
database dump or the files — and gives you a URL to it. That is the route out
that always works, on any destination: it is decrypted on the machine, so you
need no key and no credentials of ours.

What comes out is unencrypted and the URL needs no further authentication, so
it asks you to confirm before it is made. The link expires shortly after it is
ready; prepare it again for a fresh one.

## When a subscription ends

The machines are destroyed a day after the end of the period you paid for.
**The backups are kept for thirty days after the subscription ends**, so the
site can be brought back, and then deleted from our storage — the data
itself, not only the record of it.

A copy in your own storage is not ours to delete and stays where it is, with
its key still yours: that is the one copy that outlives the subscription for
as long as you want it to.

## Copying data between environments

**Backups → Copy from another environment.** This is how you get realistic data
into staging. It is not read from a backup: the receiving machine asks the
source for a fresh database dump and copies its files across directly, so what
arrives is what the source holds right now. It replaces rather than merges —
the database is rebuilt and the file directories mirrored, so nothing that was
there before survives beside it. A deploy follows.

The deploy is not optional and not cosmetic. The database that arrives belongs
to whatever code the *source* was running, and the environment receiving it is
running its own — so the update hooks and configuration import have to run for
the two to agree. Without that you have a database and a codebase that
disagree, which shows up as a fatal on the first page anyone opens.

**Nothing is scrubbed unless you say so.** See below.

On an environment whose database has a machine of its own, the copy runs in
parts, each on the machine holding it: the database on the database machine,
the files on the web machine, then your sanitisation commands where the
application runs. They appear as separate rows under Activity. A restore of the
database there does the same: the data goes back on the database machine, and
your sanitisation and the cache rebuild follow on the web machine.

Not available into production, for the same reason a cross-environment
restore is not.

## Resetting a database

**Backups → Reset database** empties the environment's database and starts it
again on the version `vallic.yaml` declares, with a backup taken first and the
newest backup restored after. It exists for one job — moving to a database
version that cannot read the old files — and
[Service upgrade](service-versions.md) is where to start if that is what you
are doing.
Only an Admin or the Owner can do it, and only by typing the environment's name.

## Sanitising what arrives

We do not scrub anything on our own. We cannot: we do not know which of your
tables hold people. Guessing would produce a result nobody could rely on —
scrubbed enough to look handled, not enough to be.

So you declare it, in `.vallic/commands/sanitization.yml`:

```yaml
commands:
  - 'drush sql:sanitize --yes'
  - 'drush sql:query "UPDATE orders SET customer_email = CONCAT(id, ''@example.test'')"'
  - 'drush sql:query "TRUNCATE payment_tokens"'
```

They run in your application container, in order, immediately after the
database arrives — from a copy or from a restore into that environment — and
before anyone can reach the environment. The first one to
fail stops the rest and fails the task — a half-scrubbed environment holding
real data while looking handled is worse than one that visibly did not finish.

**If the file is not there, nothing runs.** Production data lands in staging
exactly as it was, and every developer with access to that environment can read
it. That is worth deciding deliberately rather than discovering.

The file is read from the environment **receiving** the data, on the branch it
is running — so staging's copy governs what happens to staging.

**Never on production.** A restore into production is production being put
back from its own history, and that data belongs there — so the file is not
read and nothing runs, whatever it declares. Nothing can be copied into
production from another environment either, so there is no case where
production is scrubbed.

Other files can live in `.vallic/commands/` later; sanitisation is the first.

## Next

[Logs](logs.md) covers the other thing that leaves the machine, and
[Shell access](shell.md) covers importing a database dump by hand.

Source: https://docs.vallic.com/backup-storage.md

---

# Integrations

> Services your team already pays for, connected once and used by any project.

An integration is a service you already have, connected once on the team and
available to every project it owns. Credentials live on the team rather than on
a project, because most teams have one account with a log provider and one with
a storage provider, and re-entering the same key per project is how one of them
ends up wrong.

They are under your team's settings, and adding, changing or removing one takes
the Admin role or above.

## What can be connected

**Somewhere to send logs.** Datadog, New Relic, Logz.io, Axiom, Grafana Loki,
Splunk, an OpenTelemetry endpoint, or a plain syslog host and port for anything
not on that list. Shipped from the machine, alongside the copy the platform
keeps — connecting your own is additive and never replaces ours. See
[Logs](logs.md).

**Storage you control.** S3-compatible object storage, SFTP, or an rclone
remote, as somewhere your backups are copied to. An S3-compatible bucket can
also be where logs are archived, so one connection is both — that is why it is
one integration rather than two: it is one set of credentials, and splitting it
would mean entering the same key twice. SFTP and rclone take backups only.

**Somewhere the platform can tell you something.** Slack, Telegram, Discord,
PagerDuty, or a webhook for anything else. These are the channels
[Notifications](notifications.md) uses.

Email is deliberately not on this list. Your account *is* an email address, so
there is nothing to connect — it is simply a channel you can choose when
setting up a notification.

Log forwarding and backups to your own storage are not offered on the entry
line of machines; a larger machine adds both.

## Credentials

Given once, and not shown again. A key you paste is stored encrypted and never
rendered back into a form, so the console cannot leak what you handed it and
neither can a screenshot of it. Replacing one means entering the new value, not
editing the old.

Credentials never travel in a task. What a machine needs is handed to it when
it asks, over its own authenticated channel — so a key is not sitting in a
queue row waiting to be read.

## Removing one

An integration in use cannot be removed. The console lists what is using it —
which projects ship logs there, which backup destinations write there, which
notifications post there — and asks you to point those somewhere else first,
because removing it underneath them would show up later as a backup writing
nowhere. Nothing already written is deleted: a backup already copied to your
bucket is yours and stays there.

## Next

- [Logs](logs.md) — what is shipped, and what the platform keeps regardless
- [Backups](backup-storage.md) — pointing a copy at storage you control
- [Notifications](notifications.md) — the channels these feed

Source: https://docs.vallic.com/integrations.md

---

# Monitoring and metrics

> What is measured on your environments, where you can see it, and how availability is watched.

Two things are watched for you without any setting up: how hard each
environment is working, and whether production is answering. The first is on
the environment's **Metrics** tab. The second is what sends an
[availability notification](notifications.md).

## The Metrics tab

Every environment has one. It shows the last **15 minutes, hour, 24 hours or
7 days** — 24 hours to begin with.

### CPU, memory and disk

| Measured | How often | Kept |
|---|---|---|
| CPU and memory | Once a minute | 7 days |
| Disk | Every 15 minutes | 7 days |

Each line is drawn against **what the plan allows** — the top of the chart is
your allocation, not the machine's size. On a machine shared between
environments that is the question that matters: a line near the top is this
environment using what it was sold, whatever the rest of the machine is doing.

Each figure shows its peak and average over the window. Disk shows how much it
has changed, because a disk that is steadily filling is the one to act on
before it is full — a disk passing 80% is also a
[notification](notifications.md).

"The machine has gone quiet" under a reading means nothing has been reported
for a while. That is usually a machine that is down or being rebuilt, and is
worth a look at the environment's Activity.

Ninety days are kept: enough to answer "was it like this before the deploy on
Tuesday?" and to set a month beside the one before. For longer history, send
your [logs](logs.md) somewhere that keeps them.

### Traffic

Counted at the edge, from every request that reached the environment.

| Figure | What it is |
|---|---|
| Requests | Every request your application answered |
| Errors | Responses of **400 and above**, together — not split into 4xx and 5xx |
| p95 response | The time 95% of requests were answered within |
| Estimated bandwidth | Bytes in and out through the proxy |
| Most visited | The ten busiest paths over the last 24 hours. The platform's own uptime check, `/.well-known/vallic-health`, is counted in Requests but never listed here |

The lines are per minute over the last hour and per hour further back. The p95
is accurate to the width of the buckets it is counted in, which is plenty to
tell a slow site from a fast one and not enough to benchmark a change of a few
milliseconds.

**Errors count 404s.** A spike of them is usually a crawler or a scanner, not
your site breaking. The busiest paths list is the quickest way to tell which.

**What the edge refused is counted apart**, and in none of the figures above —
a scanner walking a word list is not your error rate. Under the paths, by why:

| Refused | Because |
|---|---|
| Probes for files no site serves | The [probe guard](edge.md#what-is-refused-for-everyone), what is [refused by default](edge.md#refused-by-default), or your own probe rules; the paths they asked for are listed as *Probes denied* |
| No user agent | A request that named no user agent, where the project [refuses those](edge.md#refused-by-default) |
| SQL injection | A scanner's SQL injection in the address, where the project [refuses it](edge.md#sql-injection) |
| Blocked addresses | An address on the project's block list |
| Blocked user agents | A user agent on the project's block list |
| Outside an allow list | An address an environment's allow list leaves out |
| Over the rate limit | More requests than the project's rate limit lets one visitor make (429) |
| Asked for the password | A request without the environment's password (401) — every reviewer's first visit is one |

A 429 or 401 your application gives itself is a request like any other.

Where a project **counts** SQL injection rather than refusing it, the card says
how many requests carried it. They reached your site and are in the figures
above; the number is there to tell you whether refusing them would turn
anybody real away.

**Who is asking** lists the clients that sent the most requests over the
window, refused or served: browsers, search engines, AI crawlers, SEO tools,
link previews, uptime monitors, scripts. The edge sorts each request's user
agent into one of these as it arrives and keeps only that — never the user
agent itself. A crawler that calls itself a browser is counted as a browser:
nothing at the edge can tell. To turn one away, add its name under **Blocked
user agents** on the [Edge tab](edge.md).

**Behind a CDN, this is what reached us**, not what your visitors asked for.
Requests the CDN answered from its cache are not in these numbers; the
[CDN tab](cdn.md) shows what it served.

## Is production answering

Every running **production** environment is requested once a minute, at its
canonical domain — your own, once you have made one canonical, and the platform
hostname before that. A redirect is an answer and is not followed.

- Any answer below 500 counts as up. A 404 on the home page is a problem, but
  not an outage.
- A check that fails is asked again a couple of seconds later, and only counts
  if that fails too — one dropped connection between us and your site is not
  your site failing.
- **Three failures in a row** — about three minutes — and the environment is
  counted as down, and an availability notification is sent, saying what the
  last check got: the status it answered, or that it did not answer and why.
- When it answers again, a second notification says so.

The three minutes are deliberate. A single slow answer during a deploy or a
restart is not something to wake anyone for, and alerting on it teaches people
to ignore the alert.

Staging and development environments are not watched this way.

This check is made from one place, as an early warning that reaches whoever
you chose under [Notifications](notifications.md). It is not the measurement
your uptime SLA is judged on.

## Availability for the SLA

Every project's production is committed to an uptime figure, and availability
is measured separately, from more than one location, against your own domain — the address your
visitors use — rather than against the machine. What counts is whether your
site was reachable the way your visitors reach it.

**Your own downtime is marked as yours.** While a deploy runs, and for a few
minutes after it finishes, your site is behind its maintenance page; so is it
while you have switched it offline. Both are sent to the monitor as your own
windows. They are recorded, but they do not count against the SLA and are not
credited. Only production is measured for credits — staging and development
carry no commitment.

The tiers, the figures and the credits are on [Projects](projects.md#support-and-uptime),
and the terms that bind them are in the SLA itself. The measurements are not
shown in the console today.

## What is not here

- **No application monitoring of our own.** Nothing inside your code is
  timed or traced by the platform — but you can send traces to a service you
  use. See below.
- **No custom checks or thresholds.** You cannot point the availability check
  at a different path, or change when it alerts.
- **No per-machine charts.** You see each environment against its allowance,
  not the machines underneath.

## Application monitoring with your own account

In the log forwarding card on the project's **Configuration** page, **Application monitoring**
lets you choose an agent from your team's [integrations](integrations.md). It
needs the Admin role, and a plan that includes log forwarding.

| Integration | What happens |
|---|---|
| **New Relic** | On a PHP stack, the New Relic extension is switched on in your application, cron and worker containers, with your licence key, distributed tracing on, and the environment's name as the application name. Nothing to install. |
| **OpenTelemetry (OTLP)** | The standard `OTEL_*` variables are set — service name, exporter, protocol `http/protobuf`, endpoint and your authentication header — and on PHP the OpenTelemetry extension is switched on. **Your application must include an OpenTelemetry SDK** to produce anything. |

Datadog is not offered as an agent; send to Datadog through OTLP instead.

Three things to know before relying on it:

- **New Relic is for PHP.** On a Node or Go application it sets variables
  nothing reads; use OpenTelemetry with your language's SDK there.
- **The OTLP endpoint is passed as written.** The same integration's endpoint
  is also where your logs go, and OpenTelemetry SDKs add `/v1/traces` to the
  endpoint they are given — so check where your traces actually arrive.
- **It is sent when you save**, to every machine the project runs on.

## Next

- [Notifications](notifications.md) — where the availability alert goes
- [Logs](logs.md) — the detail behind a spike in errors

Source: https://docs.vallic.com/monitoring.md

---

# Notifications

> What the platform will tell you about, where it tells you, and who gets it.

The platform watches a handful of things and says something when they change.
Configured per project, under **Notifications** on the project's configuration
page, by an Admin or Owner.

## What it will tell you about

| Subject | When |
|---|---|
| Deployments | A deploy failed — once per environment until the next deploy succeeds, which says it works again — or a release went live |
| Backups | A backup failed, or has not run when it should have |
| Availability | Production stopped answering its health check, and when it starts again |
| Storage | An environment passed 80% of its storage, or a disk did — one bought for an environment, or the machine's own on a machine that is only your team's — and again at 95% |
| Machines | A machine was built, resized or destroyed |
| Load | A machine has been busy long enough that it is the size rather than a spike |

Each is chosen separately, because they are not equally interesting to the same
person. A deploy failing matters to whoever pushed; a disk filling matters to
whoever pays.

Some of these are said **while there is still room to act** rather than when
they become a problem — storage at 80% and sustained load are both warnings
about where something is heading. Availability is the opposite: by the time it
is sent, the site is already not answering.

## Where it goes

Each subject gets one channel from those your team has connected under
[Integrations](integrations.md) — Slack, Telegram, Discord, PagerDuty, or a
webhook for anything else — and, separately, email on or off.

Email needs no setting up. Your account is an email address, so it is offered
without anything being connected first. Availability, storage, machines and
load start with email switched on, because all four can fire while nobody is
reading a chat channel; deployments and backups start with it off, because a
mail per deploy is how people learn to filter the sender.

A situation is said once. While a disk is still over 80% or production is still
down, nothing is repeated on the same channel; the all-clear follows when it
recovers, and only if the problem was announced in the first place.

## Who gets it

Everyone on the project from Developer upwards, except where the subject is
the owner's business rather than the team's — an overdue invoice goes to the
owner alone and does not appear in a deploy channel.

A person narrowed to particular projects is emailed about those and no others,
and not about anything that concerns the team as a whole rather than one
project. See [Teams](teams.md).

## The monthly traffic report

Separate from the notifications above, and off until somebody turns it on:
**Monthly traffic report** in the team settings. On the first of each month,
the team's owners and admins get one email with every project in it — what
each environment served against the month before, what the edge refused and
why, the SQL injection a project counts, the pages most visited, what
scanners went looking for, and who asked. Nobody else is sent it: a developer
brought in for one project does not get every project's numbers.

It is counted at the edge, so what your CDN answered from its cache is not in
it, as on the [Metrics tab](monitoring.md#traffic).

## Not a status page

These tell you about your own projects. If the platform itself is having a
problem, that is a different thing and is not something a notification about
your project would tell you.

## Next

- [Integrations](integrations.md) — connecting the channels
- [Logs](logs.md) — the detail behind what a notification only summarises
- [Monitoring](monitoring.md) — the metrics and checks an alert comes from

Source: https://docs.vallic.com/notifications.md

---

# Support

> Opening a ticket, what the priorities mean, and the tickets the platform opens for you.

Support is asked for with a ticket, from **Support** at the bottom of the
console's menu. Email does not open one.

## Opening a ticket

**Support → New ticket.** A ticket belongs to the team you are working in, and
anyone on that team can open one — Viewers included. Somebody who can see a
problem should be able to report it.

| Field | |
|---|---|
| Subject | Up to 200 characters |
| Project | Required — which of the team's projects this is about |
| Environment | Optional, and worth giving: it tells us which machine to look at |
| Activity log entry | Optional, once an environment is chosen: one of its 50 latest — the deploy that failed, the backup that did not run. Its log is the first thing we read |
| Priority | Below |
| Affected URL | Optional. Where to see the problem, if it can be seen |
| What happened | Plain text. What you did, what you expected, what you got |
| Attachments | Screenshots: PNG, JPEG, GIF or WebP, up to 2 MB each, five per ticket |

**A ticket cannot be edited once it is open.** Anything you want to add, add as
a reply: the conversation then says what was known when, which is what somebody
working on it needs.

## Priority

| Priority | Means |
|---|---|
| Low | A question, or something to look at when there is time |
| Normal | Something is wrong but the site is working |
| High | Part of the site is broken, or it is badly degraded |
| Urgent | The site is down or losing data |

The priority is yours to choose, and we do not change it. Choose honestly:
**Urgent** is the one the response times on your project's support level are
measured against — see [Projects](projects.md#support-and-uptime) — and an
urgent queue full of questions is slower for everyone in it.

## Replies

Replies are on the ticket, in the console. When we reply, everyone on the team
from **Developer** upwards is emailed, with a link back to it. **Replying to
that email does not reach us** — answer on the ticket.

A Viewer who opened a ticket is not emailed about replies, and should check the
ticket itself.

## Resolved and closed

Either side can mark a ticket **resolved** with their last reply. That closes
the conversation: no replies can be added afterwards. If the problem comes
back, open a new ticket and mention the old one.

**A ticket nobody has replied to for 14 days closes by itself**, with a note
saying so. That is not a judgement that the problem is solved — if it is not,
a new ticket reaches us the same way.

## Everyone on the team sees every ticket

The Support page lists the team's tickets, open and closed, whoever opened
them. A ticket is the team's business, and the person who opened it is not
always the person who is around when the answer arrives. Do not put anything
in one you would not want the rest of your team to read — a password, for
instance.

## Tickets we open

Some tickets are opened by the platform rather than by you, and show as from
**Vallic**. Today that is sustained load on an environment:

| Opened when | |
|---|---|
| CPU | The environment has used at least 60% of its machine's cores for an hour |
| Memory | The environment has used at least 90% of its plan's memory for 30 minutes |

Sustained means every reading in that time was over the line — a spike does
not count. Both are opened as **High**, and a
[Load notification](notifications.md) is sent alongside. The ticket resolves
itself once the environment has stayed back under the line for 15 minutes.

They are opened because an environment working that hard for that long is
usually the size rather than a bad afternoon, and the answer is often a
conversation — a bigger machine, a cache, a query that has started scanning a
table. Reply on it if you want one. They follow the same 14-day rule as
any other ticket, and a new one opens if the load is still there.

## Next

- [Projects](projects.md#support-and-uptime) — the support levels and their response times
- [Monitoring and metrics](monitoring.md) — the readings behind a load ticket

Source: https://docs.vallic.com/support.md

---

# Billing

> How a subscription is charged, when changes take effect, and what happens when an invoice goes unpaid.

Everything a project runs is on one subscription, billed to the team that owns
it. The team's **Billing** page lists the subscriptions, the invoices and the
card.

## The period

Monthly, quarterly or yearly, chosen when the project is set up.

| Period | Paid ahead | Notice to cancel |
|---|---|---|
| Monthly | — | 3 days |
| Quarterly | 5% off | 10 days |
| Yearly | 15% off | 45 days |

Paying ahead is money the platform can plan capacity on, which is what the
discount is for — and the notice period is the other side of the same trade. A
month is small enough that a few days is fair warning; a year is a commitment
planned around, and unwinding it takes longer.

**The period is fixed for the term.** It is what you agreed to and have been
invoiced under, so on a card subscription it changes at renewal rather than in
the middle. The Commercial terms cards show it and refuse it, rather than
leaving you to wonder where it went.

Premium support and the Advanced uptime SLA are sold on a quarterly or yearly
period, and the Premium uptime SLA on a yearly one. A project on a shorter
period is shown them and told why they are not available until the period
changes.

## Changing something mid-term

Everything else is changeable whenever you like: support, the uptime promise,
the environment count, extra bandwidth, the [CDN](cdn.md) and its network, the
number of domains, how many people are on a project, and production's backup
schedule. Resizing a machine or buying a disk counts too.

**Taking more is always immediate and always prorated.** You are charged for
the part of the period you actually use it, rather than for a whole month you
were halfway through. Raise the environment count on the 10th and you pay for
two-thirds of that month and can use it that afternoon.

**Giving something back is not the mirror image.** With one exception, a
reduction applies from your next invoice: you keep what you are giving up, and
keep being billed for it, until the period you bought it for ends.

| Change | Takes effect | Credited now? |
|---|---|---|
| **Resize** a machine, up or down | Immediately | Yes, both ways |
| Buy a disk, or grow one | Immediately | n/a |
| A disk you already have | Not removable | — |
| Add environments, traffic, CDN, domains, people | Immediately | n/a |
| Reduce environments, traffic, CDN, domains, people | Next invoice | No |
| Move the CDN to the cheaper network | Next invoice | No |
| Lower support or the uptime SLA | Next invoice | No |

Three of those need a reason.

**Resizing is the one thing credited both ways.** A machine holds nothing that
is not also somewhere else, so its size can move freely and the money moves
with it — down as well as up. That is only true while the disk stays put,
which is why not every machine can go down: see [Resizing a machine](#resizing-a-machine)
below.

**Everything else churns, so reductions wait.** What you buy is capacity for a
period — "two environments beyond production", "a terabyte of traffic", "four
people on this project". Feature-branch environments are made on Monday and
gone by Thursday; people join a project for a sprint. Crediting each change
would mean rewriting your subscription continuously for numbers that end the
week where they started. So you can lower any of them whenever you like, it is
recorded, and it takes over at your next invoice — and raising the number
again before then simply cancels the reduction.

This is also why **deleting an environment costs nothing and refunds nothing**.
What you bought is the slot; the environment is what fills it. Delete one and
the slot stays yours, ready for the next. Lowering the *count* is the separate
decision that changes the money.

**A disk cannot be given back.** Storage is grown, never shrunk — the data on
it is the reason it exists, and a disk that could be removed is a disk that
could take a database with it.

Adjustments appear on the **next** invoice rather than as a separate charge.

## Resizing a machine

Which way a machine can move depends on its disk — see
[Machines](machines.md) for the two kinds.

**A Cloud Native machine** has a disk of its own, so a resize changes CPU and
memory and leaves the disk alone. It goes both ways and is credited both ways,
so it is the one to reach for when a site is busier in December than in
February. Growing is done **without taking the site down** where the provider
allows it — the machine keeps serving while it gets bigger. Shrinking always
needs a restart: memory cannot be taken back from a running kernel, so the
machine is stopped, moved down and started again. You are told which of the
two is about to happen before you confirm.

**A Regular machine** has its disk as part of its type, so moving it up grows
the disk with it — and storage cannot be shrunk. That makes it a one-way door:
the list offers only the size it is on and larger, and the machine is always
stopped, moved and started again. It is charged prorated from the day you take
it, like everything else you add.

Growing the disk grows the filesystem on it too, so the room is there when the
machine comes back. Worth saying because it is the step people miss doing this
by hand: a disk that is bigger while the filesystem still reports the old size
looks exactly like nothing having happened.

Preview environments work differently. They do not have a machine each:
staging, development and every feature branch share one machine, and *that* is
what is resized. It is offered only the machines sold for this — the short
`VC-PRE-…` list — and resizing it gives every environment on it more at once.

That list is a range rather than one machine per size. Several machines carry
twenty environments; what separates them is how much each of the twenty gets,
which is why every option says both. The name is the count and then which of
that size's machines it is, cheapest first: `VC-PRE-E20-0` is the cheapest
machine carrying twenty, `VC-PRE-E20-1` the next one up, and so on. Pick further
down the list when the environments are rehearsing a large site, and at the top
when they are not. It can be moved to a smaller machine as well, but not one
that carries fewer environments than are running on it.

## Paying

By card, or by bank transfer where invoice terms have been granted — that is
not something you switch on yourself, because it is credit.

Card details are entered at the payment provider and never reach this platform.
**Manage at Stripe** on the billing page opens their portal, where the card,
the receipts and the past invoices live.

## Free trial

Paying by card, a project whose machines come to **€30 a month or less** before
tax starts with **three days free**. The configurator marks the machines that
qualify and says in its summary whether the whole order does; add-ons such as
support, uptime or extra environments are not counted towards the €30. A
promotion code can give a trial on other terms — longer, or on larger machines
— and says so when it is applied at checkout.

- **The card is taken at checkout** and charged when the trial ends. Cancel
  the project before then and nothing is charged: it runs until the trial
  ends, and is then deleted with its data and backups.
- **One trial per person, and per card.** Whoever orders, and every owner of
  the team, must not have had one before — from a code or not. A card or bank
  account that has already paid for a trial, under any account, ends the new
  one as soon as checkout completes: the site keeps running, and the first
  payment is taken straight away instead of after the free days.
- **The project runs as ordered until the first payment.** Nothing that raises
  the price can be changed during the trial: resizing a machine, growing a
  disk, moving a service onto a machine of its own, adding machines, or raising
  the commercial terms. Giving something back is allowed.
- **If the first payment does not arrive**, the site is taken offline a couple
  of hours after the trial ends and shows your offline page. Pay within three
  days — update the card in the billing portal — and it comes back on its own.
  After three days the project is cancelled, its machines are removed
  straight away and **its data and backups are deleted with them** — unlike an
  ordinary [cancellation](#cancelling), nothing is kept for thirty days.

Paying by invoice there is no trial: the terms are agreed with us instead.

## Tax

Prices in the console are shown and stored **net**; the public
[price list](/cloud/pricing) shows them with Croatian VAT included, because
that is the figure most of its readers will actually pay. Tax is worked out at checkout from the
billing address on your team, so it is the tax you owe rather than ours: within
Croatia and for EU consumers, 25%; for a business elsewhere in the EU with a
verified VAT id, reverse charged and labelled as such; outside the EU, zero.

The billing address belongs to the team, so two people buying for one team
cannot put two different companies on two invoices. Changing it applies to the
next invoice and never to one already raised.

## An invoice that goes unpaid

Nothing is destroyed, and nothing stops serving.

**Paying by invoice.** The team's owner is mailed a couple of days before an
invoice falls due, again when it passes its due date, and weekly after that.
**Three days past the due date the team is suspended.** After a week it also
opens a ticket on our side, because by then it wants a conversation rather
than another reminder. The suspension is lifted when the invoice is paid.

**Paying by card.** If the payment provider's retries fail and the
subscription ends unpaid, the team is suspended **seven days after the
subscription ended**. It is lifted once the team has a way of paying again — a
live subscription, or invoice terms we have granted.

Suspended means the same thing either way: everybody on the team is capped to
Viewer. The sites keep serving and the backups keep running; what stops is
changing things — deploys, provisioning and configuration.

## Cancelling

From the project, giving the notice its period asks for. The dialog quotes the
date before you agree to it. Cancelling with more notice than that ends the
subscription at the end of the current period; cancelling inside the notice
window ends it at the end of the next one.

**Your site keeps running until that date.** You have paid for the period, so
you keep what you paid for — cancelling on the second of the month does not
take the site away that afternoon. Billing stops on the date quoted, the
machines are destroyed a day after it, and the hostnames stop resolving
then.

Everything written down survives. The project, its environments, their domains
and the release history stay exactly as they are. The backups are kept for
thirty days after the subscription ends, so the site can be brought back, and
are then deleted from our storage — see [Backups](backup-storage.md#when-a-subscription-ends).
A copy in your own storage is yours and is not deleted.

## Worked examples

Assume a monthly subscription that renews on the 1st.

**Buying the project.** You configure production, add one extra environment
and a CDN commitment, and pay at checkout. The first invoice covers the whole
first period; the subscription then renews on the 1st of each month for the
same lines.

**Adding an environment on the 10th.** You raise the count from 1 to 2 in
Commercial terms. Two-thirds of a month is charged for the new slot, and it
appears on the 1st. You can set the environment up immediately.

**A feature branch, 12th to 15th.** You create an environment in the slot you
already have and delete it three days later. Nothing is charged and nothing is
credited — you were paying for the slot the whole time, and it is still yours.

**Resizing a Cloud Native production machine up on the 14th, down on the
20th.** Both are prorated, because a resize leaves the disk alone. The invoice
on the 1st shows the charge for six days at the larger size and the credit for
the rest of the month at the smaller one.

**Moving a Regular production machine up on the 16th.** The new size comes
with a bigger disk, so half a month is charged for the difference and there is
no way back below that size afterwards.

**Removing an environment on the 20th.** You delete the environment — its
machine is destroyed there and then — and lower the count from 2 to 1. The
count stays at 2 until the 1st, when the lower price takes over. The rest of
that month is not credited: it is what you bought. Change your mind on the
27th and raising it back to 2 cancels the reduction entirely.

**Dropping support from Premium to Standard on the 18th.** Recorded, and
applied on the 1st. You keep Premium response times for the rest of the month
you have paid for.

**Buying a 50 GB disk on the 22nd.** A third of a month is charged for it on
the 1st. It cannot be removed later, only grown.

**Cancelling on the 25th of a monthly plan.** Three days' notice, so it lands
at the end of the month you are in. Billing stops on the 1st, the site keeps
serving until then, and the machines go a day later. The backups are there for
thirty days after the 1st.

## Next

- [Plans](plans.md) — what is bought, and what each part is
- [Teams](teams.md) — who is billed, and how people are counted

Source: https://docs.vallic.com/billing.md

---

# API

> The API the console and the CLI call, the tokens CI deploys with, and what each can reach.

Everything the console does is an HTTP API under `/api/vc/v1`, and the console
and the CLI both call it. How you reach it depends on who is calling:

- **You, at your own machine:** `vallic login` signs the CLI in through your
  browser, and everything you can do in the console you can do from there.
- **A pipeline:** a personal access token, which reaches deploys and what a
  deploy needs — see [What a token can do](#what-a-token-can-do).

## Issuing a token

**Your account → Access tokens → Issue a token.** It asks three things:

| Field | Why |
|---|---|
| What is it for | Only you see it. It is how you tell which token to revoke when something is compromised |
| Team | The token reaches this team and nothing else, whatever else you are a member of |
| Expires | 7, 30 or 90 days |

**The token is shown once.** Only a digest of it is kept, so it cannot be shown
again or recovered — if you lose it, revoke it and issue another.

There is **no token that never expires**. A token lives in CI settings, in a
colleague's shell history, in a laptop that was sold; 90 days is the most any
of them stays good for after it was forgotten. You are emailed from 14 days
before one expires, and weekly after that.

**Revoke** is on the token's row. Revoked and expired tokens stay in the list,
so you can see what existed and when it was last used.

## What a token can do

**A token is for CI.** It reaches deploying, redeploying and rolling back,
building, following the task and its log, checking a `vallic.yaml`, and the
reads that find a project and environment (`/me`, `/teams`, `/projects`,
`/environments`, releases). Nothing else: variables, domains, backups,
environments, keys and tokens need you signed in — the console, or
`vallic login`. A token asked for anything else is refused with a 403,
`token_not_allowed`. A token cannot issue tokens.

**Within that, exactly what you can do in that team, and no more.** A token is
you, narrowed to one team: your role decides what it may touch, every rule that
applies to you in the console applies to it, and if your role changes, so does
the token. Leaving the team leaves the token with nothing.

There are no scopes. A token cannot be limited to reading, or to one project.
If a pipeline should only ever deploy, give it a token belonging to someone
whose role is only Developer — a person, or an account made for the purpose.

**A token never restores a backup.** A pipeline that could replace your live
database is a pipeline that could be made to. Restoring over a protected
production environment asks for your password again, signed in.

## Calling the API

```
curl https://console.vallic.com/api/vc/v1/environments \
  -H "Authorization: Bearer vcp_…"
```

JSON in and out. Every token starts `vcp_`, so one committed by mistake is
easy to search for. A request refused for what it sent is a **422** whose
`error.fields` names the reason per field.

Endpoints marked **token** take a personal access token; the rest need you
signed in. Signed in through a browser, a write also carries the session's
CSRF token in an `X-CSRF-Token` header, from `/session/token`; the CLI's
sign-in needs none.

Most changes come in two calls: a `GET` of what may be asked — the choices,
the values as they are now, or `blocked` with the reason there is nothing to
ask — and the `POST` that makes the change. Where machines are built or given
back, a `…/preview` says first what would happen and what it would cost, and
changes nothing. The console's dialogs are these calls, so anything they do
can be scripted the same way.

### You and your account

| | Endpoints under `/api/vc/v1` |
|---|---|
| You | `GET /me`, `GET /teams` (**token**) |
| Keys and tokens | `GET`, `POST` on `/me/ssh-keys` and `/me/tokens`; `DELETE /me/ssh-keys/{id}`, `/me/tokens/{id}` |
| Other browsers | `POST /users/{id}/sessions/end-others` signs out every browser but this one |
| Invitations to you | `POST /invitations/{id}/accept`, for the account whose confirmed address it was sent to |

### Teams

| | |
|---|---|
| The team | `GET /teams/new`, `POST /teams`; `GET`, `POST` on `/teams/{id}/settings`; `DELETE /teams/{id}` once nothing is left in it |
| People | `GET /teams/{id}/people`; `POST /teams/{id}/invitations`; `POST /invitations/{id}/renew`, `DELETE /invitations/{id}`; `PATCH`, `DELETE` on `/memberships/{id}` |
| Who it bills as | `GET`, `POST` on `/teams/{id}/billing`; `POST /billing/address-format` with `{country}` says how that country writes an address |
| Subscriptions | `GET`, `POST` on `/teams/{id}/subscriptions/{order}/cancel` |
| Integrations | `GET /teams/{id}/integrations/new`; `POST /teams/{id}/integrations/fields` with `{kind}` says what a service asks; `POST /teams/{id}/integrations`; `GET`, `POST`, `DELETE` on `/integrations/{id}` |
| GitLab and Gitea | `GET /teams/{id}/repository-hosts/new`, `POST /teams/{id}/repository-hosts`; `DELETE /repository-hosts/{id}` |
| Backup destinations | `GET /teams/{id}/backup-destinations/new`, `POST /teams/{id}/backup-destinations`; `GET`, `POST` on `/backup-destinations/{id}` |
| Notifications | `GET /teams/{id}/notifications` — what is waiting and what finished since you last looked, the bell's two counts and their first rows |
| What you have seen | `POST /teams/{id}/activity/seen`, `POST /teams/{id}/next-steps/hide` — yours alone |

### Projects

| | |
|---|---|
| Listing | `GET /projects` (**token**) |
| New | `GET /projects/new`, `POST /projects` |
| Settings | `GET`, `POST` on `/projects/{id}/settings`, `…/registries` (`DELETE …/registries/{host}` forgets one), `…/logs`, `…/notifications` and `…/backup-window` (`DELETE` goes back to the default) |
| Edge | `GET`, `PATCH` on `/projects/{id}/edge` — blocked addresses and user agents, rate limit, HSTS, compression and the pages the edge serves, in one save; nothing is kept if anything is refused |
| Commercial terms | `GET`, `POST` on `/projects/{id}/terms` |
| Repository | `GET`, `POST` on `/projects/{id}/repository`; `POST …/repository/reconnect` |
| Releases | `GET /projects/{id}/releases` (`?branch=` for one branch) (**token**) |
| Variables | `GET`, `POST` on `/projects/{id}/variables`; `DELETE …/variables/{name}`; `POST …/variables/import`, `…/variables/apply` |
| Cancelling, deleting | `GET`, `POST` on `/projects/{id}/cancel`; `DELETE /projects/{id}` once it has no environments |

### Environments

| | |
|---|---|
| Listing | `GET /environments`, `/environments/{id}` (**token**) |
| New, changed, deleted | `GET /projects/{id}/environments/choices`, `POST /projects/{id}/environments`; `PATCH /environments/{id}` (`git_ref`, `auto_deploy`), `DELETE /environments/{id}` |
| Deploying | `GET /environments/{id}/deploy` and `…/source` say what may be deployed and from where; `POST …/deploy`, `…/redeploy`, `…/rollback`, `…/build` (**token**) |
| Variables | `GET`, `POST` on `/environments/{id}/variables`; `GET`, `DELETE …/variables/{name}`; `POST …/variables/import`, `…/variables/apply`; `POST /variables/parse` reads a pasted `.env` back and saves nothing |
| Domains | `GET`, `POST` on `/environments/{id}/domains`; `PATCH`, `DELETE` on `/domains/{id}`; `POST /domains/{id}/verify` |
| CDN and edge | `GET`, `PATCH` on `/environments/{id}/cdn` and `…/edge` (the environment's own: allowed addresses, password, stickiness); `POST …/cdn/clear` |
| Maintenance | `POST /environments/{id}/maintenance` with `{maintenance, note?}` |
| Logs | `GET /environments/{id}/logs`: where the environment's request and error logs go — the project's destination by kind and name, whether it can send, how many days each machine keeps its own copy. Vallic Cloud does not keep the logs, so there is nothing to tail here |
| Resources | `GET /environments/{id}/resources?window=` `1h`, `24h`, `7d`, `30d` or `90d`: CPU, memory and disk, oldest first, averaged into at most 500 points; `step` says how many seconds each covers. Kept three months |
| Services | `GET /environments/{id}/services`; `GET …/services/choices`, `POST …/services/preview`, `POST …/services` |
| Shape and scale | `GET`, `POST` on `/environments/{id}/reshape` and `…/scale`; `POST …/reshape/preview`; `POST …/discard-old-database` |
| Storage | `GET`, `POST` on `/environments/{id}/storage/split`, `…/storage/allowance` and `…/storage/{disk}/disk`; `POST …/storage/{disk}/move`, `…/storage/discard`; `GET`, `POST` on `/volumes/{id}/grow` |
| Machines for it | `GET`, `POST` on `/environments/{id}/build-machines`; `POST …/build-machines/options` for one provider's sizes |
| Backups | `GET`, `POST` on `/environments/{id}/backups`; `POST …/backups/{snapshot}/download`, `…/restore`; `DELETE …/backups/{snapshot}`; `GET`, `POST` on `…/sync`; `POST …/reset-database`; `GET`, `POST` on `…/backup-key` |
| Checking a manifest | `POST /validate`, `POST /environments/{id}/validate` (**token**) |

### Machines, tasks and support

| | |
|---|---|
| Machines | `GET /servers`; `GET`, `POST` on `/servers/{id}/resize`; `POST /servers/{id}/reboot` |
| Following | `GET /tasks`, `/tasks/{id}`, `/tasks/{id}/log` (**token**); `POST /tasks/{id}/cancel` while nothing has started it |
| Activity | `GET /tasks` narrows with `team`, `project`, `environment`, `kind` (`deployments`, `backups`, `machines`, `configuration`, `maintenance`) and `type`, newest first, `limit` up to 100 and `before` for the next page; `GET /tasks/{id}` carries the task's `account` — what the console's dialog shows — and `?include=log` adds the end of a log you may read |
| Support | `GET /teams/{id}/tickets/new`; `POST /teams/{id}/tickets`, as `multipart/form-data` with screenshots as `attachments[]`, or JSON without; `POST /tickets/{id}/replies` with `{body, resolved}` |

A few things worth knowing before you script against them:

- **Deploys are refused, not queued**, while another is running on the same
  environment — you get a 409. Wait for the task and try again. See
  [Deployments](deployments.md).
- **Deploy, redeploy and rollback return a task.** Poll `/tasks/{id}` to learn
  how it ended, and read `/tasks/{id}/log` for what it printed.
- **The activity list is the console's.** The console's Activity page, its
  task dialog and its bell read `/tasks`, `/tasks/{id}` and
  `/teams/{id}/notifications` — what you script against is what you see
  there. Page with `next_before`: pass it back as `before`.
- **Poll with the ETag.** `/tasks` and `/teams/{id}/notifications` answer
  with an `ETag`. Send it back as `If-None-Match` and an answer that has not
  changed is a `304` with no body. It still counts towards
  [how often you can call it](#how-often-you-can-call-it).
- **`deploy` and `redeploy` take `{"skip_steps": true}`** to put the release
  live without running the deploy steps from `vallic.yaml`. See
  [the first deploy](deployments.md#the-first-deploy).
- **`rollback`** deploys the release that was running before the last
  successful deployment. It is code only; see
  [going back](deployments.md#going-back).
- **A domain past the project's allowance is refused** with a 409,
  `domain_allowance_spent`. See [how many domains](domains.md#how-many-domains).
- **Adding a domain takes the hostname and nothing else.** Making a domain
  canonical and declaring a CDN in front of it are done in the console. See
  [Domains](domains.md).
- **Verification is throttled** to twelve checks an hour per domain; past that
  you get a 429.
- **What cannot be taken back asks to be confirmed.** Cancelling a project or
  a subscription and copying another environment's data in want
  `"confirm": true` in the body; resetting a database wants the environment's
  name as `confirm`. Without it you get a 422 on `confirm` and nothing
  happens.
- **Some changes want a recent sign-in.** Restoring over a protected
  production environment, among others, answers a **403**
  `reauthentication_required` when your sign-in is older than it allows. Sign
  in again and repeat the request; the console sends you through it and
  back.
- **Revealing a backup key is its own `POST`.** `GET …/backup-key` says what
  the key opens; the key itself comes only from the `POST`, and every reveal is
  written in the activity log.

## How often you can call it

Two limits, and only one of them is about how busy you are.

**Six hundred requests a minute per token.** Past that you get a **429** and
should wait and repeat the request unchanged. It is a ceiling for a loop that
has lost its sleep, not a budget to plan against — a pipeline doing real work
will not come near it. The count is per token, not per person or per team, so a
runaway script cannot lock your colleagues out, and whoever has to revoke it
can still sign in.

If you are polling a task to see how a deploy ended, poll it every few seconds
rather than as fast as the loop will go. Nothing goes faster for being asked
more often.

**Repeatedly presenting a credential that does not work** gets your address
refused with a 429 for a while, whatever token you try next. A token is a
bearer credential — whoever holds the string is you — so guessing has to cost
something. Calling an endpoint with no `Authorization` header at all does not
count against this: that is a 401 and nothing more, so a forgotten header
cannot lock out an office that shares one address.

Presenting a token that worked clears the count, so an occasional typo in a
pipeline costs you nothing once the right one goes through.

## Next

- [Teams and people](teams.md) — the roles a token inherits
- [Variables](variables.md) — what a pipeline most often sets

Source: https://docs.vallic.com/api.md

---

# Software stacks

> Everything that can run on your machines, the versions on offer, and which parts you choose.

A **stack** is the set of services an environment runs. Some of it arrives with
your project type and some of it you pick.

## What arrives with the project type

You choose a project type — Drupal, Laravel, WordPress, PHP, Go or Node.js —
and the web bundle comes with it, whole:

- the **runtime** for that language: PHP-FPM, a Node.js server, or your Go
  binary
- **Nginx**, serving static files and passing requests to PHP — PHP projects
  only, because a Node or Go application is its own web server
- a **queue worker** container — Laravel only, where the framework has one
- **OpenSMTPD**, so the application can send mail

The **schedule** comes with it too, but not as a container. Your framework's
cron runs on the machine itself, against the application that is already
running there — see [`vallic.yaml`](configuration.md) for declaring your own
jobs.

These are not tick-boxes. A stack missing any of them is not a working site,
and asking somebody to tick five boxes that must all be ticked is asking them
to get it wrong. They run on the machines you already pay for, at no extra
charge.

Every PHP project — Drupal, Laravel, WordPress or plain PHP — runs on the same
[PHP-FPM image](stack-php.md). What differs between them is not the image but
what the platform sets around it: the Nginx configuration, the document root,
the default cron and the directories kept between releases.

## What you choose

For **Drupal, Laravel and WordPress** two are required, because there is no
sensible default:

- **A database.** MariaDB, MySQL or PostgreSQL. Some applications want one,
  some another, and guessing wrong is a migration. Choosing MySQL or
  PostgreSQL *replaces* MariaDB rather than adding to it — you never run two.
- **A cache.** Redis, Valkey or Memcached — one of them.

A **PHP, Go or Node.js** project needs neither, and gets either only by asking.

Your database, its user and its password are created for that environment
alone, and your application is handed them in its environment file. You never
set them and you never need to know them.

The rest are optional and cost a machine or a container each:

- **Search** — Solr or Meilisearch, one at a time
- **Queue** — RabbitMQ, when jobs need a real broker rather than the database
- **Proxy** — Vinyl Cache (Varnish), in front of the web servers
- **Build tools** — [Node.js for builds](stack-node.md), for a PHP site whose
  theme or assets need `npm`. It runs only during a build, never beside the
  site

## Everything, with versions

The version in **bold** is what you get if you do not choose one. This table is
built from the same catalogue the configurator reads, so it is never out of
date.

| Service | Kind | What it does | Versions |
| --- | --- | --- | --- |
| [Go](stack-golang.md) | Application | A compiled Go binary serving its own HTTP. Replaces PHP-FPM and nginx: it is the web server too. | **1.27.0**, 1.26.7, 1.26.6 |
| [Node.js](stack-nodejs.md) | Application | A Node application serving its own HTTP. Replaces PHP-FPM and nginx: it is the web server too. | **26.10**, 24.21, 22.23, 26.8, 24.20 |
| [PHP-FPM](stack-php.md) | Application | The platform's PHP-FPM image, for every PHP project: Drupal, WordPress, Laravel or anything else. | **8.5**, 8.4, 8.3, 8.2 |
| [MariaDB](stack-mariadb.md) | Database | The database. The same service on the web server or on a machine of its own. | **11.8**, 11.4, 10.11 |
| [MySQL 8](stack-mysql.md) | Database | Oracle's MySQL rather than MariaDB, where an application or a team is committed to it. | **8.0** |
| [PostgreSQL](stack-postgres.md) | Database | The other relational database. Chosen instead of MariaDB, never beside it. | **18.6**, 18.4, 18.3 |
| [Queue worker](stack-queue.md) | Application | Laravel's queue worker. The same container again, running the worker. | — |
| [Nginx](stack-nginx.md) | Application | Serves static files and passes PHP to FPM, behind the shared edge proxy. | **1.31**, 1.30, 1.29 |
| [Valkey](stack-valkey.md) | Cache | Cache and sessions. Not needed for several web servers, but faster for both. | **9.0**, 8.1, 8.0 |
| [Redis](stack-redis.md) | Cache | In-memory cache, for what expects Redis by name. Valkey is the fork most stacks now take. | **8.6**, 8.4, 8.2 |
| [Memcached](stack-memcached.md) | Cache | A simpler cache than Redis: keys and values, nothing else, and nothing kept when it restarts. | **1.6**, 1.5, 1.4 |
| [Meilisearch](stack-meilisearch.md) | Search | A lightning-fast search engine API, with AI-powered hybrid search for your sites and applications. | **v1.53.1**, v1.53.0, v1.52.3 |
| [RabbitMQ](stack-rabbitmq.md) | Background work | A message broker for work that outlives the request that asked for it. | **4.3.5**, 4.3.4, 4.3.3 |
| [Vinyl Cache (Varnish)](stack-vinyl.md) | Reverse proxy | Varnish. Caches whole responses in front of the site, so a hit never reaches it at all. | **8.0**, 6.0 |
| [Solr](stack-solr.md) | Search | Search. Needs zookeeper, which is added automatically. | **10.0**, 9.10, 9.9 |
| [ZooKeeper](stack-zookeeper.md) | Search | Coordination for Solr. Never selected on its own; Solr pulls it in. | **3.9** |
| [OpenSMTPD](stack-opensmtpd.md) | Application | Outbound mail relay for environments not using a hosted provider. | **7.8**, 7.6, 7.5 |
| [Node.js](stack-node.md) | Build tools | Node.js and npm for build steps, such as compiling a theme. Runs in the build, on no machine. | **26.10**, 24.21, 22.23, 26.8, 24.20 |

Each has its own page with the full version list, which of them are on their
way out, and the variables you may set on it. They are the pages under
**Software** in the sidebar.

## Changing a version

Versions are per environment, so a runtime or database upgrade is something you
try on staging first. Staging is an environment you add to a project, on either
plan — see [Environments](environments.md). Without one, the first environment
to run a new version is production.

Data on disk survives a container image changing — the database keeps its files
outside the container precisely so that upgrading the image is not a restore
from backup. Downgrading is a different matter: a database that has opened its
files with a newer version will usually refuse an older one, so treat a major
version change as one-way unless you have checked otherwise.

## Next

[Environments](environments.md) covers where these versions are set.

Source: https://docs.vallic.com/stacks.md

---

# 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

```yaml
version: 1
type: drupal

runtime:
  php: '8.4'
  memory_limit: 512M

services:
  - mariadb: '11.8'
  - valkey: '8'
  - solr:
      version: '9'
      environment:
        SOLR_HEAP: 1g

build:
  steps:
    - composer install --no-dev --optimize-autoloader
  cache:
    - vendor
    - web/core
    - web/modules/contrib
    - web/themes/contrib

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](service-versions.md) first.

```yaml
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](stacks.md): `mariadb`,
`mysql`, `postgres`, `valkey`, `redis`, `memcached`, `solr`, `meilisearch`,
`rabbitmq`, `vinyl` (Vinyl Cache, formerly 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](stacks.md).

**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 **Resources** 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.

```yaml
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:

```yaml
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](stack-node.md),
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.

That is the download cache. What a tool *installs* is not kept: `vendor/` is
unpacked again on every build unless it is listed, and for a PHP site it is the
directory most worth listing — the next `composer install` checks it against
`composer.lock` and touches only what changed:

```yaml
build:
  cache:
    - vendor
  steps:
    - composer install --no-dev --optimize-autoloader
```

A Drupal site adds the directories its installers write modules into — see
[Drupal](framework-drupal.md). `build.cache` is also for directories *your own*
build writes and would like back next time:

```yaml
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](deployments.md) covers the whole sequence around them.

```yaml
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](framework-drupal.md), [WordPress](framework-wordpress.md),
[Laravel](framework-laravel.md), [Symfony](framework-symfony.md), and
[Node and Go](framework-node.md).

`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.

```yaml
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](stack-php.md),
[Node.js](stack-nodejs.md) and [Go](stack-golang.md) 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. Leaving it out
gives you 512M, or your plan's maximum if that is lower. 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.

```yaml
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.

```yaml
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.

```yaml
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](shapes.md).

## 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.

```yaml
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](backup-storage.md#sanitising-what-arrives).

Four more files add rules to Varnish's cache policy, when your stack runs it:
`.vallic/varnish/recv.vcl`, `hash.vcl`, `backend-response.vcl` and
`deliver.vcl`. They take effect with the deploy that carries them. See
[Vinyl Cache (Varnish)](stack-vinyl.md).

## 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](variables.md). 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 |

Source: https://docs.vallic.com/configuration.md

---

# Service settings

> The settings each service takes from vallic.yaml, what they do, and what stays with the platform.

The services your site runs beside it — the database, the cache, Nginx, PHP
itself — each take a handful of settings: how much memory the database keeps
for its cache, the largest upload Nginx lets through, how long a PHP request may
run. You set them in [`vallic.yaml`](configuration.md), under the service they
belong to.

```yaml
runtime:
  php: '8.4'

services:
  - mariadb:
      version: '11.8'
      environment:
        MYSQL_MAX_ALLOWED_PACKET: 256M
        MYSQL_SLOW_QUERY_LOG: '1'
  - nginx:
      version: '1.31'
      environment:
        NGINX_CLIENT_MAX_BODY_SIZE: 64m
  - php:
      version: '8.4'
      environment:
        PHP_MAX_EXECUTION_TIME: '120'
        PHP_UPLOAD_MAX_FILESIZE: 64M
        PHP_POST_MAX_SIZE: 64M
```

Every entry under `services` needs a version, `php` included. Which PHP runs is
still `runtime.php` — see [Configuration](configuration.md#runtime) — so keep
the two the same.

These are not project variables. A project variable is something your
application reads; a service setting changes the service. They live in the
repository because they belong to the code: the upload limit your site was
built around is part of the site, not something to remember to set on each
new environment.

## The rules

**Only the settings listed below.** Each service takes a short list, and a
deploy that sets anything else refuses, naming the variable and what the
service does take. Nothing is accepted and quietly ignored, so a setting that
reaches a deploy is a setting that is in effect.

**Each value is checked.** A setting that takes a number takes a number, a size
takes a size, and a pattern that ends up in the service's own configuration
file takes only the characters a pattern needs. A value of the wrong shape
refuses the deploy, naming it. Everything is one line of text, at most 1,024
characters, and a `$` is a dollar sign: values are never expanded, so one
setting cannot read another, or anything else on the machine.

**They take effect on the next deploy.** A service whose settings changed is
restarted with them before your code lands. Delete a line and the next deploy
puts the default back.

**Background work gets the application's settings.** The queue worker, your
[workers](configuration.md#cron-and-workers) and your scheduled jobs are your
application run differently. A job given less time or memory than the site
would be the one that fails, so they run with whatever you set on `php`.

**Memory is sized from your machine.** The settings that decide how much memory
a service holds — the database's buffer pool, the cache's limit, Solr's heap,
Varnish's storage, PHP's APCu — are set from what you run on, not left at the
image's defaults. A service with a machine of its own takes most of it: a cache
alone on its machine gets 80% of it, the database 60%. One that shares a
machine with your site gets a share of your plan instead, so it can never grow
into the memory your site needs. Those of them listed below — the buffer pool,
PostgreSQL's shared buffers, Solr's heap, APCu — you can set lower. A higher
value is lowered to your share. The cache's limit and Varnish's storage are not
settings at all: they are only ever sized.

**Production shows no errors on the page.** On a production environment PHP
does not print errors, startup errors or failed assertions into the response;
they still go to [the logs](logs.md). Other environments behave as the image
does. Set `PHP_DISPLAY_ERRORS` if you want something else.

**PHP's memory limit is held to your plan.** `PHP_MEMORY_LIMIT` goes through
the same rule as `runtime.memory_limit`: anything above what your plan allows
is lowered to it. If the file sets both, `runtime.memory_limit` wins.

**Timeouts are a chain.** A slow request passes through Nginx and, if you run
it, Varnish before it reaches PHP, and whichever gives up first answers. Raise
`PHP_MAX_EXECUTION_TIME` and `NGINX_FASTCGI_READ_TIMEOUT` together. With Varnish
in front, a page still has 60 seconds to start answering, whatever the other
two say. Work that takes longer belongs in a [worker](configuration.md#cron-and-workers).

**Your code is never checked for changes while it runs.** PHP caches compiled
code and would normally check every file for changes on every request. Your
code does not change under a running site: a deploy switches to the new
release and reloads PHP, which empties the cache. So the check is off
everywhere, and there is no setting for it.

## Every setting, by service

A service missing from this list takes no settings. Its page says why.

### [Go](stack-golang.md) — `golang`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `GOMEMLIMIT` | A soft memory limit for the Go runtime, such as `900MiB`. | the image's |
| `GOMAXPROCS` | How many threads run Go code at once. | the image's |

### [MariaDB](stack-mariadb.md) — `mariadb`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `MYSQL_MAX_ALLOWED_PACKET` | The largest single query or row, such as `256M`. | `256M` |
| `MYSQL_INNODB_BUFFER_POOL_SIZE` | The database's cache of tables and indexes. Sized from the database's share of the machine unless you set it lower. | sized from your machine |
| `MYSQL_MAX_CONNECTIONS` | Connections at once. Each one costs memory whether it is busy or not. | `100` |
| `MYSQL_SLOW_QUERY_LOG` | `1` to log queries slower than `MYSQL_LONG_QUERY_TIME`. | `OFF` |
| `MYSQL_LONG_QUERY_TIME` | Seconds after which a query counts as slow. | `2` |

### [Meilisearch](stack-meilisearch.md) — `meilisearch`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `MEILI_MAX_INDEXING_MEMORY` | Memory Meilisearch may use while indexing. | the image's |
| `MEILI_LOG_LEVEL` | How much Meilisearch logs. | `INFO` |

### [MySQL 8](stack-mysql.md) — `mysql`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `MYSQL_MAX_ALLOWED_PACKET` | The largest single query or row, such as `256M`. | `256M` |
| `MYSQL_INNODB_BUFFER_POOL_SIZE` | The database's cache of tables and indexes. Sized from the database's share of the machine unless you set it lower. | sized from your machine |
| `MYSQL_MAX_CONNECTIONS` | Connections at once. Each one costs memory whether it is busy or not. | `100` |
| `MYSQL_SLOW_QUERY_LOG` | `1` to log queries slower than `MYSQL_LONG_QUERY_TIME`. | the image's |
| `MYSQL_LONG_QUERY_TIME` | Seconds after which a query counts as slow. | the image's |

### [Nginx](stack-nginx.md) — `nginx`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `NGINX_CLIENT_MAX_BODY_SIZE` | The largest request body Nginx passes on, such as `64m`. Uploads bigger than this never reach PHP. | `32m` |
| `NGINX_KEEPALIVE_TIMEOUT` | How long an idle browser connection is kept open. | `75s` |
| `NGINX_FASTCGI_READ_TIMEOUT` | How long Nginx waits for PHP to answer, in seconds. | `900` |
| `NGINX_GZIP_COMP_LEVEL` | Compression level, 1 to 6. Higher saves bytes and costs CPU on every response. | `1` |
| `NGINX_STATIC_EXPIRES` | How long browsers may cache static files, such as `7d`. | `1y` |
| `NGINX_ERROR_LOG_LEVEL` | How much Nginx logs: `error`, `warn` or `notice`. | `error` |
| `NGINX_DRUPAL_NOT_FOUND_REGEX` | Drupal presets only: paths matching this regex are answered with 404, so source files such as `composer.json` or `.yml` are never served. | the image's |
| `NGINX_WP_NOT_FOUND_REGEX` | WordPress preset only: paths matching this regex are answered with 404, so files such as `composer.json` or `.sql` dumps are never served. | the image's |

### [Node.js](stack-nodejs.md) — `nodejs`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `NODE_OPTIONS` | Flags for Node itself, such as `--max-old-space-size`. Debugger flags are refused. | the image's |

### [OpenSMTPD](stack-opensmtpd.md) — `opensmtpd`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `RELAY_HOST` | The SMTP server to send through. Without one, mail leaves from the machine's own address. | the image's |
| `RELAY_PORT` | Its port, usually 587. | `587` |
| `RELAY_PROTO` | How to connect to it, such as `smtp+tls`. | `smtp+tls` |
| `RELAY_USER` | The user to log in as. The password is a secret project variable, `RELAY_PASSWORD`, never a line in the file. | the image's |
| `OPENSMTPD_MAX_MESSAGE_SIZE` | The largest message accepted, such as `35M`. | `35M` |
| `OPENSMTPD_EXPIRE` | How long undeliverable mail is retried before it bounces. | `4d` |
| `OPENSMTPD_BOUNCE_WARN` | When the sender is warned that a message is still undelivered. | `1h, 6h, 2d` |

### [PHP-FPM](stack-php.md) — `php`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `PHP_MEMORY_LIMIT` | Memory one request may use. Held to what your plan allows, and `runtime.memory_limit` wins when the file sets both. | `512M` |
| `PHP_MAX_EXECUTION_TIME` | Seconds a request may run. Nginx and Varnish keep their own clocks. | `120` |
| `PHP_POST_MAX_SIZE` | The largest request body PHP accepts. At least `PHP_UPLOAD_MAX_FILESIZE`, and no more than Nginx's `NGINX_CLIENT_MAX_BODY_SIZE`. | `32M` |
| `PHP_UPLOAD_MAX_FILESIZE` | The largest single uploaded file. | `32M` |
| `PHP_OPCACHE_MEMORY_CONSUMPTION` | Megabytes for compiled code. Raise it when a large codebase no longer fits. | `128` |
| `PHP_APCU_SHM_SIZE` | Shared memory for APCu, the in-process cache. Sized from your machine unless you set it lower. | sized from your machine |
| `PHP_FPM_PM_MAX_CHILDREN` | Requests served at once. Each may use up to the memory limit, so this times that has to fit the machine. | `8` |
| `PHP_DISPLAY_ERRORS` | Whether errors are printed into pages. Off unless you turn it on, and it should stay off anywhere a visitor can reach. | `Off` on production, `On` elsewhere |
| `PHP_MAX_INPUT_VARS` | How many input variables one request may carry. Large forms — Drupal's permissions page, menu editors — need more than the default. | `2000` |

### [PostgreSQL](stack-postgres.md) — `postgres`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `POSTGRES_MAX_CONNECTIONS` | Connections at once. | `100` |
| `POSTGRES_SHARED_BUFFERS` | PostgreSQL's own cache. Sized from the database's share of the machine unless you set it lower. | sized from your machine |
| `POSTGRES_WORK_MEM` | Memory for one sort or hash — per operation, per connection, so a small number goes a long way. | `5MB` |

### [RabbitMQ](stack-rabbitmq.md) — `rabbitmq`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `RABBITMQ_VM_MEMORY_HIGH_WATERMARK` | The share of its memory at which RabbitMQ stops accepting messages, such as `0.6`. Of the queue machine when it has one, of your plan otherwise. | the image's |

### [Redis](stack-redis.md) — `redis`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `REDIS_MAXMEMORY_POLICY` | What is evicted when the cache is full: `allkeys-lru` suits a cache, `noeviction` a store that must not lose keys. | `allkeys-lru` |
| `REDIS_DATABASES` | How many numbered databases exist. | `16` |

### [Solr](stack-solr.md) — `solr`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `SOLR_HEAP` | Java heap for Solr, such as `1g`. Sized from the search machine unless you set it lower. | sized from your machine |
| `SOLR_MODULES` | Solr modules to load, comma-separated, such as `extraction,langid`. | `extraction,langid,ltr,analysis-extras` |

### [Valkey](stack-valkey.md) — `valkey`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `VALKEY_MAXMEMORY_POLICY` | What is evicted when the cache is full: `allkeys-lru` suits a cache, `noeviction` a store that must not lose keys. | `allkeys-lru` |
| `VALKEY_DATABASES` | How many numbered databases exist. | `16` |

### [Vinyl Cache (Varnish)](stack-vinyl.md) — `vinyl`

| Variable | What it does | Unless you set it |
| --- | --- | --- |
| `VARNISHD_PARAM_DEFAULT_TTL` | How long, in seconds, a page is cached when the application sends no `Cache-Control: max-age` of its own. | `120.000` |
| `VARNISH_BACKEND_GRACE` | How long an expired page may still be served while a fresh copy is fetched in the background, such as `10m`. | `2m` |
| `VARNISH_CACHE_PER_COUNTRY` | Keep a separate cached copy per visitor country, for sites that vary content by country. Set to `1`. | the image's |
| `VARNISH_MOBILE_SEPARATE_CASH` | Keep a separate cached copy for mobile browsers, for sites that render differently on phones. Set to `1`. | the image's |
| `VARNISH_KEEP_ALL_PARAMS` | Keep marketing query parameters such as `utm_source` in the cache key instead of stripping them. Set to `1`. | the image's |

"Unless you set it" is the platform's value where it sets one. Otherwise it is
the image's own default.

## What stays with the platform

Every service takes far more settings than are listed here. The others are
held back on purpose. Setting one of them would not tune the service. It would
change what the service is, or what it may reach. They fall into a few kinds:

- **Where things connect.** Hostnames, ports, cluster membership, which
  backend Varnish sends requests to. The platform addresses every service in
  your environment. A different value would point it somewhere else, and if
  that worked, the somewhere else would be another customer's.
- **Who and where.** Users, file ownership and paths. Your code is mounted
  read-only at a path the deploy switches atomically, and the services are
  built around that.
- **Credentials.** Database passwords, cache passwords, search admin
  passwords. Generated per environment and handed to your application in
  its [environment](variables.md). You never need to know them.
- **What keeps data safe.** How the database flushes to disk, where it
  stores files, and whether a cache persists. Your backups and restores
  depend on these, and a faster setting is usually a less durable one.
- **Logs.** Where logs go and in what format. The platform collects them
  from a known place, so you can [read them](logs.md) without changing
  your code.
- **Admin and debug endpoints.** Status pages, debuggers, profilers and
  cache purging. Each one is a door into a production site, and a door
  opened by a line in a repository is a door anyone who can push a branch
  can open.

If you need one of these changed, ask support at support@vallic.com. Tell us
what you are trying to do rather than which variable to set. That is usually
a setting the platform should own, or one this page should list.

Source: https://docs.vallic.com/service-settings.md

---

# Service upgrade

> What happens when you change a version in vallic.yaml, which services survive it, and how to move a database to a version that cannot read the old one.

Every service in [`vallic.yaml`](configuration.md#services) names a version:

```yaml
services:
  - mariadb: '11.4'
  - valkey: '8.1'
```

**That is the version your environment runs, from the next deploy.** Change it,
push, and the deploy brings the service up on the version you named. A version
the platform does not offer is not honoured — the service stays on the current
default. [Software stacks](stacks.md) lists what each service offers.

For most services that is all there is to it. For the ones that keep data, it
is not always, and this page is about the difference.

## Services that keep nothing

**Caches** — Valkey, Redis, Memcached — and **Vinyl Cache (Varnish)** hold nothing that
cannot be rebuilt. Move them up or down freely. Their contents may be empty
afterwards; your application refills them as it is used, and a Drupal site can
be helped along with a cache rebuild.

## Services that keep data

**Databases** — MariaDB, MySQL, PostgreSQL — **search** — Solr, Meilisearch —
and the **queue**, RabbitMQ, keep data on disk in a format that belongs to the
version that wrote it. The new version starts on the files the old one left.
Whether it can read them depends on the engine and the direction:

| Service | Moving to a newer version | Moving to an older version |
|---|---|---|
| MariaDB, MySQL | Generally reads the older data and upgrades it in place. Once it has, the older version cannot read it again | Does not start on data a newer version wrote |
| PostgreSQL | **A different major version does not start at all** on the old data | Does not start |
| Solr | Reads one major version back; rebuild the index to be sure | Does not reliably read it; rebuild the index |
| Meilisearch | Does not read another version's index; rebuild it | Rebuild it |
| RabbitMQ | Keeps its queues across minor versions | Empty the queues first |

Your application's own data is never touched by any of this — only the
service's files on the machine. A database that will not start is a site that
is down until one of the paths below is taken, so pick the path **before**
you push.

## Before you change anything

1. **Take a backup** — **Backups → Back up now** on the environment. Database
   backups are SQL dumps, and an SQL dump can be restored into any version.
   That is what makes every path below work.
2. **Keep a dump of your own** as well, over [shell access](shell.md):

   ```
   ssh -p 2417 vc-t-acme@production.acme.vallic.cloud db-export > before-upgrade.sql
   ```

3. **Try it on staging first.** **Backups → Copy from another environment**
   puts production's data on staging; change the version on the branch staging
   tracks, deploy, and see what happens there rather than on your live site.

## Moving a database to a version that can read the old data

MariaDB or MySQL, to a newer version.

1. Take the backup above.
2. Change the version in `vallic.yaml` and deploy.
3. Check the site. If anything is wrong, the backup is your way back — see
   *Going back* below; changing the version back on its own is not, because the
   data has already been upgraded.

## Moving a database to a version that cannot read the old data

PostgreSQL to another major version, or any database to an older version.
There are three ways, from the least to the most disruptive.

### A new environment, then move to it

The safest, because the old environment is not touched until the new one is
proven.

1. Create a new environment — or a new project, for production — whose
   `vallic.yaml` names the new version from its first deploy. A database that
   starts on the version you want never has old files to read.
2. Deploy it, then load your data into it: **Backups → Copy from another
   environment** for a staging environment, or `db-import` with the dump you
   kept:

   ```
   ssh -p 2417 vc-t-acme@production.acme-new.vallic.cloud db-import < before-upgrade.sql
   ```

3. Move your files if they need to go too — see [Shell access](shell.md) for
   rsync.
4. Check the new environment, then move your [domains](domains.md) across and
   remove the old one.

### The same environment, with its database reset

When a new environment is not practical. **Backups → Reset database** on the
environment does it in one action: a backup of the database while it still
answers, then its files emptied on the machine that runs it, then the newest
backup restored into the empty database. Each step waits for the one before.

1. Keep a dump of your own, as above — a second copy costs nothing.
2. Change the version in `vallic.yaml` and deploy. On a version that cannot
   read the old files the database will not start; that is expected.
3. **Backups → Reset database.** Leave *Take a database backup first* ticked if
   the database still answers — before step 2, or on a version change that did
   start — and untick it if it no longer starts; the backups already taken are
   what is restored then. Leave *Restore the newest database backup* ticked.
   Type the environment's name to confirm.
4. The database starts empty on the version `vallic.yaml` declares, and the
   backup loads into it. A backup is SQL, so any version reads it. If you
   would rather load your own dump, untick the restore and `db-import` it.

Only an **Admin** or the **Owner** can reset a database, and only by typing the
environment's name — it deletes every table. Your site is unavailable from the
reset until the restore finishes: do it at a quiet time, and consider
[switching the site offline](edge.md#taking-a-site-offline) first so visitors
see a page saying so.

### Reimporting without clearing

Only for a database that **did** start on the new version but whose data you
want rebuilt anyway — to shed an old format, or after an upgrade that left
things half-converted. `db-import` loads over what is there. A MariaDB or MySQL
dump from `db-export` carries `DROP TABLE` statements and replaces each table
as it goes; a PostgreSQL one does not, so drop the tables it will recreate
first.

## Search indexes and queues

- **Solr and Meilisearch**: after changing the version, rebuild the index from
  your application — for Drupal, `drush search-api:reindex` and then
  `drush search-api:index`. An index is built from your database, so nothing is
  lost that the rebuild cannot put back.
- **RabbitMQ**: let the queues drain, or stop what feeds them, before a change
  that is not a minor version. Messages still waiting when the service is
  replaced may not be there afterwards.

## Going back

- **A cache or Varnish:** change the version back and deploy.
- **A database that has not written anything on the new version** — it did not
  start: change the version back and deploy. The old version finds its own
  files untouched.
- **A database that ran on the new version:** the old version may no longer
  read its files. Take the *same environment, database reset* path above in
  reverse — put the old version back in `vallic.yaml`, deploy, then **Reset
  database** with the backup unticked, so the one you took before you started
  is what is restored.

## Who can do this

Changing a version is a commit, so it is whoever can push to the branch an
environment tracks. Backing up, restoring and copying are on the environment's
Backups tab, for the roles [Teams](teams.md) lists. Resetting a database is an
**Admin** or the **Owner** only.

## Next

- [Configuration](configuration.md) — the rest of `vallic.yaml`
- [Backups](backup-storage.md) — taking, restoring and copying
- [Software stacks](stacks.md) — which versions each service offers

Source: https://docs.vallic.com/service-versions.md

---

# PHP-FPM

> The PHP-FPM container every PHP project runs in

The PHP-FPM container every PHP project runs in: Drupal, WordPress, Laravel, Symfony or anything else. One image, built by Vallic from wodby's PHP, published for PHP 8.2 to 8.5.

It carries PHP and its extensions, Composer, and the clients a shell needs (`rsync`, `mariadb`, `psql`). It carries no framework's tool: `drush` is the one in your project's `vendor/bin`, and a WordPress project fetches `wp-cli` into `bin/` as a build step in its `vallic.yaml` — both are on `PATH` inside the container, and both are versioned with your code rather than with ours.

Inside the container the code is at `/var/www/html/current` (`WEB_ROOT`), read-only; the directory above it holds the releases kept for rollback. The user is `vallic`.

## Choosing the version

`runtime.php` in [`vallic.yaml`](configuration.md#runtime) picks it, for the site and for your build steps alike, so Composer resolves your dependencies against the PHP that will run them:

```yaml
runtime:
  php: '8.4'
  memory_limit: 512M
```

Leave it out and you get the default, marked below. `memory_limit` is held to what your plan allows — see [Configuration](configuration.md#runtime) for the ladder.

## What is set for you

- **No errors on the page in production.** Errors, startup errors and failed assertions go to [the logs](logs.md), not into the response. Other environments behave as the image does.
- **No checking your code for changes.** A deploy switches to a new release and reloads PHP-FPM, so the check every request would otherwise make is off everywhere.
- **APCu is sized from your machine**, rather than left at the image's default.
- **Debuggers and profilers are not in the image at all** — no Xdebug, no XHProf — so no setting can switch one on in production.

The queue worker, your [workers](configuration.md#cron-and-workers) and your scheduled jobs run in this same image, with the same settings. See [Service settings](service-settings.md) for what you can change.

## Versions

| Version | Status |
| --- | --- |
| `8.5` | Supported, and the default |
| `8.4` | Supported |
| `8.3` | Supported |
| `8.2` | Supported |

Pin the version, not the build: name `8.5` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - php:
      version: '8.5'
      environment:
        PHP_MEMORY_LIMIT: …
        PHP_MAX_EXECUTION_TIME: …
        PHP_POST_MAX_SIZE: …
        PHP_UPLOAD_MAX_FILESIZE: …
        PHP_OPCACHE_MEMORY_CONSUMPTION: …
        PHP_APCU_SHM_SIZE: …
        PHP_FPM_PM_MAX_CHILDREN: …
        PHP_DISPLAY_ERRORS: …
        PHP_MAX_INPUT_VARS: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-php.md

---

# Go

> A compiled Go binary serving its own HTTP

A compiled Go binary serving its own HTTP. Like Node it is the web server as
well as the application, and like Node it replaces PHP-FPM and Nginx rather
than joining them. In front of it is the platform's proxy, which terminates
TLS and routes the hostname; behind it, whatever the project chose.

The image is the upstream Go image, so the toolchain your build steps run is
the one the binary is built with. Choose the version with `runtime.go` in
[`vallic.yaml`](configuration.md):

```yaml
version: 1
type: golang

runtime:
  go: '1.27'

build:
  steps:
    - go build -o bin/server ./cmd/server

start: bin/server
port: 8080
```

**`start` is required.** Nothing can guess what your binary is called, and a
container with nothing to run starts and exits. It runs in the release, at
`/var/www/html/current` (`WEB_ROOT`), which is read-only.

Your binary listens on `PORT`, which is `8080` unless `port` says otherwise.
Go's module and build caches are kept between builds for you, so a second build
does not download or compile what the first one did.

Uploads and anything else written at runtime go under `/mnt/files`
(`VALLIC_PUBLIC_DIR`, `VALLIC_PRIVATE_DIR`), which outlive the release. See
[Node.js and Go](framework-node.md) for a fuller example.

## Versions

| Version | Status |
| --- | --- |
| `1.27.0` | Supported, and the default |
| `1.26.7` | Supported |
| `1.26.6` | Supported |

Pin the version, not the build: name `1.27.0` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - golang:
      version: '1.27.0'
      environment:
        GOMEMLIMIT: …
        GOMAXPROCS: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-golang.md

---

# Node.js

> A Node application serving its own HTTP — Next.js, Nuxt, Express or anything else that listens on a port

A Node application serving its own HTTP — Next.js, Nuxt, Express or anything else that listens on a port. Replaces PHP-FPM and nginx rather than joining them: it is the web server as well as the application. In front of it is the platform's proxy, which terminates TLS, routes the hostname and compresses responses; behind it, whatever the project chose.

The container runs as `vallic` with the release at `/var/www/html/current` (`WEB_ROOT`), read-only, and the release's own `node_modules/.bin` on `PATH`. The release keeps `node_modules`: for a Node application they are the runtime, not a build input, so the build's last step prunes what production does not need.

## A Next.js project

`vallic.yaml` at the root of the repository:

```yaml
version: 1
type: nodejs

runtime:
  node: '24'

build:
  steps:
    - name: Dependencies
      run: npm ci
    - name: Build
      run: npm run build
    - name: Production dependencies only
      run: npm prune --omit=dev
  cache:
    - node_modules
    - .next/cache

start: npm start
port: 3000

health:
  path: /
  timeout: 30
```

`npm start` is `next start`, which serves the `.next` directory the build produced and listens on `PORT`. Both `start` and `port` are the defaults, so they can be left out; they are written here so the file says what runs. Next.js binds `HOSTNAME`, which the platform sets to `0.0.0.0`, and `NODE_ENV` is `production`.

Uploads and anything else written at runtime go under `/mnt/files` (`VALLIC_PUBLIC_DIR`, `VALLIC_PRIVATE_DIR`), which outlive the release; the release itself is read-only. Environment variables — a database URL, an API key — are set on the environment in the console and reach the process as ordinary environment variables, not as an `.env` file in the checkout.

## Versions

| Version | Status |
| --- | --- |
| `26.10` | Supported, and the default |
| `24.21` | Supported |
| `22.23` | Supported |
| `26.8` | **Deprecated** — still runs, but move to something newer |
| `24.20` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `26.10` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - nodejs:
      version: '26.10'
      environment:
        NODE_OPTIONS: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-nodejs.md

---

# Node.js for builds

> Node and npm available to build steps, for compiling a theme or bundling front-end assets

Node.js and npm for **build steps** — compiling a theme, bundling front-end
assets, running a bundler over your CSS. A tool the build runs in, not a
container that runs beside your site: nothing of it is started, it belongs to
no machine, and it is gone by the time the release is serving.

That is the whole difference between this and
[Node.js as an application](stack-nodejs.md). If your site *is* a Node
application — Next.js, Nuxt, Express — you want that one, which runs
continuously and answers requests. If your site is PHP and merely needs `npm
run build` during a deploy, you want this one.

Both can be true at once. A Drupal site with a themed front end uses PHP-FPM to
serve and this to build.

## Versions

| Version | Status |
| --- | --- |
| `26.10` | Supported, and the default |
| `24.21` | Supported |
| `22.23` | Supported |
| `26.8` | **Deprecated** — still runs, but move to something newer |
| `24.20` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `26.10` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

Nothing on this one. Everything it takes decides where it connects, what it
is, or whether it starts — and an application able to set those could break
its own environment in ways nothing here would catch.


## Using it

Name it in [`vallic.yaml`](configuration.md), and run the steps that need it in
its image with `image: node`. A step without `image` runs in your application's
own container, and the PHP image has no `node` or `npm` in it:

```yaml
services:
  - node: '26'

build:
  steps:
    - name: PHP dependencies
      run: composer install --no-dev --optimize-autoloader
    - name: Theme
      run: npm ci && npm run build
      image: node
```

The step runs against the same checkout as the others, so what it writes — a
compiled theme, a bundle — is in the release. npm's download cache is kept
between builds for you. Anything it installs that production does not need,
`node_modules` above all, is worth removing in the same step, because the
release is copied to every machine that runs it.

## Next

- [vallic.yaml](configuration.md) — where build steps are declared
- [Node.js](stack-nodejs.md) — running a Node application rather than building with it

Source: https://docs.vallic.com/stack-node.md

---

# MariaDB

> The database, and the default for Drupal, Laravel and WordPress

The database. A Drupal, Laravel or WordPress project gets MariaDB unless it
asks for [MySQL](stack-mysql.md) or [PostgreSQL](stack-postgres.md) instead; a
PHP, Go or Node.js project gets a database only by asking.

It can run on the web server or move onto a machine of its own — see
[Shapes](shapes.md). The service is the same either way, and so is how your
application reaches it.

Transactions run at `READ-COMMITTED`, which is what Drupal recommends.

## Connecting to it

Read the environment rather than committing credentials:

```
DB_HOST      the service name
DB_PORT      3306
DB_DRIVER    mysql
DB_NAME
DB_USER
DB_PASSWORD
DATABASE_URL all of it as one connection string
```

The database, its user and its password are made for the environment and never
change. See [Variables](variables.md) for the full list, and
[`settings.vallic.php`](framework-drupal.md) for a Drupal site, which reads all
of it for you.

## Changing it

A version change is one way: a database that has opened its files with a newer
MariaDB will usually refuse an older one. And `vallic.yaml` cannot change which
database you run — moving from one to another means moving everything in it,
which is a migration rather than a setting. Ask support.

From your own computer — a desktop client, a browser — `vallic tunnel <env> db` opens it on `127.0.0.1:3306`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `11.8` | Supported, and the default |
| `11.4` | Supported |
| `10.11` | Supported |

Pin the version, not the build: name `11.8` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

The version you name is the one your environment runs from the next deploy.
This service keeps data, and not every version can read another's — before
you change it, read [Changing a service's version](service-versions.md).

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - mariadb:
      version: '11.8'
      environment:
        MYSQL_MAX_ALLOWED_PACKET: …
        MYSQL_INNODB_BUFFER_POOL_SIZE: …
        MYSQL_MAX_CONNECTIONS: …
        MYSQL_SLOW_QUERY_LOG: …
        MYSQL_LONG_QUERY_TIME: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-mariadb.md

---

# MySQL 8

> Oracle's MySQL, for applications and teams committed to it rather than to MariaDB

Oracle's MySQL rather than [MariaDB](stack-mariadb.md). The same wire protocol
and the same driver, so an application does not know the difference unless it
is asking for something only one of them has.

Choose it where an application or a team is committed to MySQL itself — a
feature that only exists there, a tool that refuses to run against anything
else, or an operations team who know its behaviour under load. If neither
applies, MariaDB is the default for a reason: it is what most of these sites
run, and it is what this platform's own tooling is exercised against daily.

Like every database here it stays on the machine's own disk. See
[Storage](storage.md) for what can be moved onto a disk of its own.

From your own computer — a desktop client, a browser — `vallic tunnel <env> db` opens it on `127.0.0.1:3306`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `8.0` | Supported, and the default |

Pin the version, not the build: name `8.0` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

The version you name is the one your environment runs from the next deploy.
This service keeps data, and not every version can read another's — before
you change it, read [Changing a service's version](service-versions.md).

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - mysql:
      version: '8.0'
      environment:
        MYSQL_MAX_ALLOWED_PACKET: …
        MYSQL_INNODB_BUFFER_POOL_SIZE: …
        MYSQL_MAX_CONNECTIONS: …
        MYSQL_SLOW_QUERY_LOG: …
        MYSQL_LONG_QUERY_TIME: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

## Connecting to it

Read the environment rather than committing credentials. The same names as
every other database:

```
DB_HOST      the service name
DB_PORT      3306
DB_DRIVER    mysql
DB_NAME
DB_USER
DB_PASSWORD
```

See [Variables](variables.md) for the full list and
[`settings.vallic.php`](framework-drupal.md) for a Drupal site, which reads all
of it for you.

## Backups

Dumped nightly with everything else, and restored the same way. Nothing about
the backup schedule differs from MariaDB — see [Backups](backup-storage.md).

## Next

- [Storage](storage.md) — why the database is not offered a disk of its own
- [MariaDB](stack-mariadb.md) — the default, and what you get if you say nothing

Source: https://docs.vallic.com/stack-mysql.md

---

# PostgreSQL

> The other relational database

The other relational database. Chosen instead of [MariaDB](stack-mariadb.md),
never beside it: a stack runs one database, and choosing PostgreSQL is what
stands MariaDB aside.

Choose it when your application is written for it — plenty of Laravel,
Symfony, Node and Go applications are — or when you need something only it
has. Drupal runs on it too. It is chosen when the environment is configured;
`vallic.yaml` cannot switch an environment's database, because moving from one
to another means moving everything in it.

## Connecting to it

The same names as every other database:

```
DB_HOST      the service name
DB_PORT      5432
DB_DRIVER    pgsql
DB_NAME
DB_USER
DB_PASSWORD
DATABASE_URL all of it as one connection string
```

`DB_USER` is the application's own user, not the superuser. The database, the
user and the password are made for the environment and never change. See
[Variables](variables.md) for the full list.

From your own computer — a desktop client, a browser — `vallic tunnel <env> db` opens it on `127.0.0.1:5432`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `18.6` | Supported, and the default |
| `18.4` | **Deprecated** — still runs, but move to something newer |
| `18.3` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `18.6` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

The version you name is the one your environment runs from the next deploy.
This service keeps data, and not every version can read another's — before
you change it, read [Changing a service's version](service-versions.md).

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - postgres:
      version: '18.6'
      environment:
        POSTGRES_MAX_CONNECTIONS: …
        POSTGRES_SHARED_BUFFERS: …
        POSTGRES_WORK_MEM: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-postgres.md

---

# Queue worker

> Laravel's queue worker

Laravel's queue worker. The same container again: your application's image, at
the same PHP version, with the same code and the same
[settings](service-settings.md) you gave `php`. A worker given less time or
memory than the site would be the one that fails.

It comes with a Laravel project and with nothing else. You do not add it and
cannot take it away.

What processes your jobs is declared in [`vallic.yaml`](configuration.md), as a
worker running `php artisan queue:work` — see
[Laravel](framework-laravel.md#the-queue) for the manifest, and for why
`queue:work` rather than `queue:listen`.

## Versions

This one is your application image running a different command, so its
version is your application's version. Set it under `runtime`.

## What you can change

Nothing on this one. Everything it takes decides where it connects, what it
is, or whether it starts — and an application able to set those could break
its own environment in ways nothing here would catch.

Source: https://docs.vallic.com/stack-queue.md

---

# Nginx

> Serves static files and passes PHP to FPM

Serves static files and passes PHP to FPM. Every PHP project has one — Drupal,
Laravel, WordPress or plain PHP — and a Node.js or Go project has none, because
the application answers HTTP itself.

What it serves is decided by your project type, not by a setting:

| Project | Document root | Configuration |
| --- | --- | --- |
| Drupal | `web/` | Drupal 11's, or Drupal 10's when `runtime.drupal` is `'10'` |
| Laravel | `public/` | Laravel's |
| WordPress | the repository root | WordPress's |
| PHP | `public/` | a plain PHP one |

The code is mounted read-only; uploads are served from the files directory that
outlives the release. In front of Nginx is the platform's proxy, which
terminates TLS and routes the hostname — or [Vinyl Cache (Varnish)](stack-vinyl.md), when the
stack runs it.

A slow request passes through Nginx before it reaches PHP, and whichever gives
up first answers. Raise `NGINX_FASTCGI_READ_TIMEOUT` together with
`PHP_MAX_EXECUTION_TIME` — see [Service settings](service-settings.md).

## Versions

| Version | Status |
| --- | --- |
| `1.31` | Supported, and the default |
| `1.30` | Supported |
| `1.29` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `1.31` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - nginx:
      version: '1.31'
      environment:
        NGINX_CLIENT_MAX_BODY_SIZE: …
        NGINX_KEEPALIVE_TIMEOUT: …
        NGINX_FASTCGI_READ_TIMEOUT: …
        NGINX_GZIP_COMP_LEVEL: …
        NGINX_STATIC_EXPIRES: …
        NGINX_ERROR_LOG_LEVEL: …
        NGINX_DRUPAL_NOT_FOUND_REGEX: …
        NGINX_WP_NOT_FOUND_REGEX: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-nginx.md

---

# Redis

> In-memory cache

In-memory cache. [Valkey](stack-valkey.md) is the fork most stacks now take;
this is here for what expects Redis by name. The two speak the same protocol,
so an application written against one runs unchanged on the other. A stack
runs one cache — Redis, Valkey or [Memcached](stack-memcached.md) — so naming a
different one replaces it.

Your application finds it at `REDIS_HOST`, port 6379 (`REDIS_PORT`). Both are
in its [environment](variables.md), and `REDIS_HOST` is the name the Drupal
redis module reads.

How much memory it holds is sized from your machine: most of a cache machine
when it has one to itself, a share of your plan when it sits beside your site.
When it is full it evicts the least recently used key (`allkeys-lru`), which is
what a cache should do. If you are keeping something that must not be lost, set
`REDIS_MAXMEMORY_POLICY` to `noeviction` — and remember that nothing in it is
backed up.

From your own computer — a desktop client, a browser — `vallic tunnel <env> redis` opens it on `127.0.0.1:6379`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `8.6` | Supported, and the default |
| `8.4` | Supported |
| `8.2` | Supported |

Pin the version, not the build: name `8.6` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - redis:
      version: '8.6'
      environment:
        REDIS_MAXMEMORY_POLICY: …
        REDIS_DATABASES: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-redis.md

---

# Memcached

> A simpler cache than Redis: keys and values, nothing else, and nothing kept when it restarts

A simpler cache than Redis: keys and values, nothing else, and nothing kept
when it restarts. Choose it when your application or a module you use speaks
Memcached and nothing else; otherwise [Valkey](stack-valkey.md) does the same
job and more. A stack runs one cache — Memcached, Redis or Valkey — so naming a
different one replaces it.

Inside the stack it answers at the hostname `memcached`, on port 11211. Unlike
Redis and Valkey, no variable in your environment names it.

How much memory it holds is sized from your machine: most of a cache machine
when it has one to itself, a share of your plan when it sits beside your site.
It is not a setting, so a cache can never grow into the memory your site needs.

From your own computer — a desktop client, a browser — `vallic tunnel <env> memcached` opens it on `127.0.0.1:11211`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `1.6` | Supported, and the default |
| `1.5` | **Deprecated** — still runs, but move to something newer |
| `1.4` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `1.6` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

Nothing on this one. Everything it takes decides where it connects, what it
is, or whether it starts — and an application able to set those could break
its own environment in ways nothing here would catch.

Source: https://docs.vallic.com/stack-memcached.md

---

# Valkey

> Cache and sessions

Cache and sessions. Not required for more than one web node — sessions in the
database work across machines too — but faster for both.

Valkey is the open-source fork of Redis and speaks the same protocol, so
anything written for Redis — the Drupal redis module, Laravel's `redis` driver,
any Redis client — works against it unchanged. A stack runs one cache — Valkey,
[Redis](stack-redis.md) or [Memcached](stack-memcached.md) — so naming a
different one replaces it.

Your application finds it at `VALKEY_HOST`, and at `REDIS_HOST` as well, since
that is the name most clients look for. The port is 6379 (`REDIS_PORT`). See
[Variables](variables.md).

How much memory it holds is sized from your machine: most of a cache machine
when it has one to itself, a share of your plan when it sits beside your site.
When it is full it evicts the least recently used key (`allkeys-lru`). Set
`VALKEY_MAXMEMORY_POLICY` to `noeviction` for a store that must not lose keys —
and remember that nothing in it is backed up.

From your own computer — a desktop client, a browser — `vallic tunnel <env> valkey` opens it on `127.0.0.1:6379`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `9.0` | Supported, and the default |
| `8.1` | Supported |
| `8.0` | Supported |

Pin the version, not the build: name `9.0` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - valkey:
      version: '9.0'
      environment:
        VALKEY_MAXMEMORY_POLICY: …
        VALKEY_DATABASES: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-valkey.md

---

# Meilisearch

> Search without ZooKeeper beside it

Search without ZooKeeper beside it. One container where [Solr](stack-solr.md)
is two. A stack runs one search engine, so naming the other replaces it, and
what is lost is an index that reindexes.

Your application finds it at `MEILISEARCH_HOST`, port 7700, and authenticates
with `MEILI_MASTER_KEY`. Both are in its [environment](variables.md). The key
is made for each environment and never changes, so the API keys Meilisearch
issues under it stay valid across deploys.

It runs in production mode, so every request needs a key and the search preview
page is off. Its index is kept on disk and survives a restart.

From your own computer — a desktop client, a browser — `vallic tunnel <env> meilisearch` opens it on `http://127.0.0.1:7700`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `v1.53.1` | Supported, and the default |
| `v1.53.0` | Supported |
| `v1.52.3` | Supported |

Pin the version, not the build: name `v1.53.1` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

The version you name is the one your environment runs from the next deploy.
This service keeps data, and not every version can read another's — before
you change it, read [Changing a service's version](service-versions.md).

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - meilisearch:
      version: 'v1.53.1'
      environment:
        MEILI_MAX_INDEXING_MEMORY: …
        MEILI_LOG_LEVEL: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-meilisearch.md

---

# RabbitMQ

> A message broker for work that outlives the request that asked for it

A message broker for work that outlives the request that asked for it.

Your application connects as `RABBITMQ_USER` with `RABBITMQ_PASSWORD`, at
`RABBITMQ_HOST` on port 5672, or with all of it in one `RABBITMQ_URL`. All four
are in its [environment](variables.md). The password is made for each
environment and never changes. The image's own `guest` account is removed.

RabbitMQ stops accepting messages when it reaches its memory limit. The limit is
60% of the queue machine when the broker has one to itself, and 60% of your plan
when it shares a machine with your site. Set
`RABBITMQ_VM_MEMORY_HIGH_WATERMARK` to use a different share, between `0.1`
and `0.9`.

Queues and messages are kept on disk, so a restart loses nothing a durable
queue held.

From your own computer — a desktop client, a browser — `vallic tunnel <env> rabbitmq` opens it on `127.0.0.1:5672`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `4.3.5` | Supported, and the default |
| `4.3.4` | Supported |
| `4.3.3` | Supported |

Pin the version, not the build: name `4.3.5` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

The version you name is the one your environment runs from the next deploy.
This service keeps data, and not every version can read another's — before
you change it, read [Changing a service's version](service-versions.md).

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - rabbitmq:
      version: '4.3.5'
      environment:
        RABBITMQ_VM_MEMORY_HIGH_WATERMARK: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-rabbitmq.md

---

# Vinyl Cache (Varnish)

> Caches whole responses in front of your site, so a hit never reaches it

Varnish, as wodby now ships it — their image is called *vinyl*, which is why
the service's id is `vinyl`. It caches whole responses in front of whatever
answers for the site: Nginx for a PHP application, the application itself for
Node.js or Go. A hit never reaches it at all.

It can sit on the web server or on a machine of its own, and with more than
one web server it spreads the misses across all of them. See
[Shapes](shapes.md).

What it caches is what your application says may be cached. A page sent with
`Cache-Control: max-age` is kept that long; one sent without is kept for two
minutes unless you set `VARNISHD_PARAM_DEFAULT_TTL`. A page that is past its
time may still be served for a short while as a fresh copy is fetched behind it,
so a slow page is slow once rather than for every visitor who asks at once.

A request has 60 seconds to start answering through Varnish, whatever Nginx and
PHP allow. Work that takes longer belongs in a
[worker](configuration.md#cron-and-workers).

## Purging

Your application finds it at `VARNISH_HOST`, port 6081. A purge sent there from
inside the stack needs no key; one arriving from outside needs
`VARNISH_PURGE_KEY` in an `X-VC-Purge-Key` header. Both are in the
[environment](variables.md). For Drupal's purger, see
[the snippet](framework-drupal.md).

## Rules of your own

Four files in your repository add to the cache policy rather than replacing it:

| File | Added to |
| --- | --- |
| `.vallic/varnish/recv.vcl` | `vcl_recv` — what to do with a request: a path never to cache, a cookie to ignore |
| `.vallic/varnish/hash.vcl` | `vcl_hash` — what else the cache key varies on, with `hash_data()`: a header, a cookie, a currency |
| `.vallic/varnish/backend-response.vcl` | `vcl_backend_response` — what to do with an answer: a lifetime for one route, a header to keep |
| `.vallic/varnish/deliver.vcl` | `vcl_deliver` — headers on the way out: one to add, one to take off |

Each is read from the commit of the release you deployed, so a change takes
effect with the deploy that carries it — not when you push, and a rollback
brings back the rules of the release you roll back to. Each holds rules for
that one subroutine, at most 8 KB. A file may not declare a backend, define `vcl_init`,
`import` a module or `include` another file — where requests go, and what runs
inside the cache, are the platform's. `hash.vcl` and `deliver.vcl` may not
`return`: they add to what the platform does, which runs after them — in
`vcl_hash` that is adding the URL and the host to the key, and a `return` would
leave them out and serve every page from one cached object. A file that does is left out, and the
policy runs without it. The assembled policy is compiled before it replaces the
one running, and a policy that does not compile is not loaded.

From your own computer — a desktop client, a browser — `vallic tunnel <env> varnish` opens it on `http://127.0.0.1:6081`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `8.0` | Supported, and the default |
| `6.0` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `8.0` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - vinyl:
      version: '8.0'
      environment:
        VARNISHD_PARAM_DEFAULT_TTL: …
        VARNISH_BACKEND_GRACE: …
        VARNISH_CACHE_PER_COUNTRY: …
        VARNISH_MOBILE_SEPARATE_CASH: …
        VARNISH_KEEP_ALL_PARAMS: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-vinyl.md

---

# Solr

> Search

Search. Needs [ZooKeeper](stack-zookeeper.md), which is added automatically.
A stack runs one search engine — Solr or [Meilisearch](stack-meilisearch.md) —
so naming the other replaces it, and what is lost is an index that reindexes.

Solr requires a login. Your application finds it at `SOLR_HOST`, port 8983,
and signs in as `SOLR_USER` with `SOLR_PASSWORD`. Both are in its
[environment](variables.md). The password is made for each environment and
never changes. A request without it is refused, so configure your client with
it. For Drupal, see [the snippet](framework-drupal.md).

Its heap is sized from your machine — half of a search machine it has to
itself, a share of your plan when it sits beside your site. Set `SOLR_HEAP`
lower if you want; a higher value is lowered to that share. The extraction,
language detection, learning-to-rank and analysis-extras modules are loaded
unless you set `SOLR_MODULES` to something else.

From your own computer — a desktop client, a browser — `vallic tunnel <env> solr` opens it on `http://127.0.0.1:8983/solr`. See [Tunnels](shell.md#tunnels-to-your-services).

## Versions

| Version | Status |
| --- | --- |
| `10.0` | Supported, and the default |
| `9.10` | Supported |
| `9.9` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `10.0` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

The version you name is the one your environment runs from the next deploy.
This service keeps data, and not every version can read another's — before
you change it, read [Changing a service's version](service-versions.md).

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - solr:
      version: '10.0'
      environment:
        SOLR_HEAP: …
        SOLR_MODULES: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-solr.md

---

# Extra machines

> Run containers of your own — a background remover, a thumbnailer, some model nobody else needs — on a machine beside your site

The catalogue covers what most applications want beside them: a database, a
cache, a search engine, a mail relay. It will never cover the rest. A real site
wants a background remover, an OCR engine, a barcode reader, some model nobody
else on the platform has heard of.

An **extra** is a machine of your own for exactly that. You buy it in the
configurator with the environment, or add it later from the project's
[Resources tab](upgrade.md#adding-a-service), and you say what runs on it with
an ordinary compose file in your repository. A shared plan has none.

It is on your environment's private network, it has its own disk, and it runs
your containers and nothing else.

**Each environment has its own.** Staging and development read the same
`vallic.yaml`, and production's extra is not theirs: its ports are on
production's private network. Give staging one of its own on the Resources tab
— a machine at full price, not a share of the machine staging runs on. Until
it has one, an extra staging or development has no machine for is left out
rather than refused: the site deploys, the tool is not there, and no `EXTRA_*`
variable names it. Production is different — a deploy that declares an extra
production has no machine for is refused, because the site would be dialling a
tool that does not exist. Add the machine on the Resources tab and deploy
again.

## Two files

`vallic.yaml` says an extra exists and what your application may reach on it:

```yaml
extra:
  voyager:
    expose:
      rembg: 8001:8000
      easyocr: 8002:8000
```

`.vallic/extra/voyager.yml` says what runs — an ordinary compose file:

```yaml
services:
  rembg:
    image: ghcr.io/acme/rembg-api:1.4.0
    environment:
      U2NET_HOME: /cache/rembg
    volumes:
      - cache/models/rembg:/cache/rembg
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"]

  easyocr:
    image: ghcr.io/acme/easyocr-api:3.2.0
    volumes:
      - cache/models/easyocr:/cache/easyocr
```

They are two files because they answer to different rules. `vallic.yaml` is
this platform's, and every key in it is checked. Your compose file is yours.

## Slots

Extras are called `pioneer`, `voyager`, `galileo`, `magellan`, `cassini` and
`juno`, allocated in that order — buy two and you have `pioneer` and `voyager`.

The name is the machine, its hostname on the private network, the file it runs
and the line on your invoice, which is why it is ours to hand out rather than
yours to invent.

## Ports

`8001:8000` reads as compose's own port syntax: **8001** is the port the rest of
your environment reaches, **8000** is the port your container listens on. A
single number means both.

A service can answer on several:

```yaml
expose:
  api:
    - 8003:8000
    - 8004:9090
```

The ports your environment reaches come from a reserved range, **8000 to
8008**. What your container listens on inside itself can be anything — `8000:80`
is fine. No two services on one extra may use the same reachable port; they
share a machine, so they share an address.

**You do not write `ports:` in your compose file.** The platform writes them,
bound to the private network. A compose file that could choose an address could
put your API on the public internet.

## Your data

An extra has its own disk. A volume names a directory on it, and the first word
says whether it is backed up:

```yaml
volumes:
  - keep/results:/var/results      # backed up
  - cache/models/rembg:/cache/rembg  # not backed up
  - keep/reference:/data/reference:ro   # read-only, and backed up
```

- **`keep/`** is for anything you would be sorry to lose. It is included in
  your backups. Restoring it is done by support for now: ask, and say which
  backup.
- **`cache/`** is for anything the container can fetch or rebuild — model
  weights, scratch files, thumbnails it can make again. It is never backed up.
- **`code/`** is your own files, out of your repository. See below.

**There is no default, and you have to pick one.** A file that names neither is
refused rather than guessed at. The two choices are wrong in opposite
directions: backing up a model cache costs you storage and makes every backup
slower, and *not* backing up real data costs you the data — and we cannot tell
which is which, because both are a directory of files on a disk we do not read.

Most model caches belong under `cache/`. A twenty-gigabyte set of weights that
the image downloads on first run is not worth walking on every backup to store
a copy nobody will ever restore.

The part after the first word is a name, not a path — a directory the platform
makes for you and keeps across deploys. You cannot name a path on the machine.

**An extra cannot see what your site has uploaded.** It has its own storage,
not the site's. A tool that needs to read your users' files — a thumbnailer, most
image tooling — needs storage every machine can reach, which is a different
purchase. Ask support before you build around it.

## Your own code on an extra

Most of these images want a little of your code: an entrypoint that knows your
queue names, a config the tool reads on boot, a short Python shim around a
model. Building and publishing a new image every time you edit a ten-line file
would defeat the point.

Put those files in `.vallic/extra/<slot>/` beside the compose file, and mount
them:

```
.vallic/
  extra/
    voyager.yml          # what runs
    voyager/             # and the files it needs
      rembg/
        entrypoint.py
        settings.toml
```

```yaml
services:
  rembg:
    image: ghcr.io/acme/rembg-api:1.4.0
    command: ["python", "/app/entrypoint.py"]
    volumes:
      - code/rembg:/app          # .vallic/extra/voyager/rembg
```

`code/rembg` is always `.vallic/extra/voyager/rembg` — it cannot point anywhere
else in your repository. That is deliberate: an extra runs an image you took
from a registry, and a mount that could name your repository root would hand
your whole codebase to it, committed `.env` files included.

**It comes from your release, not from your branch.** An extra runs the code of
whatever release your site is running, so a script here and the application
that calls it move together. Deploy, and your extra picks up the new files and
restarts. An extra that mounts code on an environment you have never deployed
has nothing to run yet — deploy first.

**It is read-only.** A release is replaced whole when you deploy, so anything
written there disappears at a moment your container cannot predict. Asking for
`:rw` on a `code/` mount is refused rather than quietly ignored; put writable
data under `keep/` or `cache/`.

This is for scripts and configuration, not for an application. An extra still
has no release of your site, no `.env`, no cron and no shell — it is something
your application calls, and this is the handful of files it needs to answer.

## Images, not builds

Name a published image and a version:

```yaml
image: ghcr.io/acme/rembg-api:1.4.0     # good
image: ghcr.io/acme/rembg-api           # refused — no version
image: ghcr.io/acme/rembg-api:latest    # refused — moves under you
```

A moving tag is replaced the next time the machine is reconciled, and nothing
in your log will explain why the behaviour changed. A digest — `image:
ghcr.io/acme/api@sha256:…` — pins hardest of all.

`build:` is not supported. These machines build with buildah and have no
BuildKit, so a Dockerfile that works on your laptop may not work here; and a
build at deploy time on a machine sized for serving is a deploy that looks like
it has hung. Build your image in CI, push it to a registry, name the tag.

**A private registry needs a login.** Add one on your project's Configuration
page, under Other → Registry logins: the host as it appears in the image (`ghcr.io`,
`registry.example.com:5000`), a username, and a password or token. A token
that may pull is enough — one that may push is more than this needs.

The login belongs to the project, so every environment of it can pull the same
image. Your machines use it to fetch and nothing else: it is never given to a
container, never written into your compose file, and never shown back to you
once saved. Replacing one means typing it again.

Remove a login and it leaves your machines on their next reconcile.

## Credentials

A tool usually wants one: a token for the model it downloads, a key for the
bucket it writes to. Do not put it in your compose file — that file is in your
repository, and a credential there is a credential in every clone of it.

Name it in `vallic.yaml` instead, and set the value in the console under
Variables:

```yaml
extra:
  voyager:
    expose:
      rembg: 8001:8000
    env:
      required:
        - REMBG_TOKEN
        - S3_KEY
```

Your containers get those variables and **only** those. An extra has no copy
of your site's `.env`: it runs an image we have not read, so what it is given
is what you wrote down here, where you can see it in review. Asking for a name
you have not set refuses the deploy and says which one, rather than starting a
container that cannot work.

Each slot gets its own. A name declared under `voyager` reaches the voyager
machine and no other.

## Logs

Whatever your containers print goes where the rest of your project's logs go.
If you have pointed the project at a destination of your own, an extra's output
arrives there too, stamped with the project and the environment so you can tell
it apart. Every machine also keeps its own local copy.

There is no filter on which of your containers are interesting, because on an
extra they are all yours. A tool that prints a line per request will send a line
per request — if that is more than you want to store, quieten the container.

## What your compose file may say

Everything a container needs to run, and nothing that reaches past it:

`image`, `restart`, `environment`, `volumes`, `command`, `entrypoint`,
`healthcheck`, `depends_on`, `user`, `working_dir`, `stop_grace_period`,
`tmpfs`, `shm_size`, `ulimits`, `init`, and your own `x-` keys.

Anything else is refused, and the message says why. The ones people reach for
most:

| Key | Why not |
| --- | --- |
| `ports` | The platform binds these from your `expose` block, on the private network |
| `build` | Extras run published images — see above |
| `networks`, `network_mode` | The platform draws the network your environment runs on |
| `env_file`, `secrets`, `configs` | These name files on the machine. Ask for variables by name under `env.required` instead |
| `privileged`, `cap_add`, `devices` | Nothing on an extra runs privileged or reaches the machine |
| `deploy` | The platform sets the limits — see what your containers may use, below |
| `container_name`, `labels` | The platform names and labels these, and reads its own labels back |

At the top level of the file, only `services`, `version` and your own `x-`
keys are allowed — top-level `volumes`, `secrets` and `configs` are refused.
Volumes use the short form shown above; the long form is refused. A file may
declare up to 20 services and be up to 64 KB.

A GPU is not available yet. When it is, it will come with the machine rather
than with a line in your compose file.

## What it can reach

An extra is there to be called. Your application calls it over the private
network; it answers. That direction always works.

**Outward, it is closed by default.** Its containers may reach the machines
running your application — so a tool can call back into your own API when it
has finished — and nothing else. Not your database, not your cache or session
store, not the internet.

That is deliberate, and it is about what an extra is: an image you chose from a
registry, written by somebody else. Everything else on your environment is
either our software or code you wrote, and an extra is neither.

If a tool genuinely needs the internet — many download a model the first time
they run — say so:

```yaml
extra:
  voyager:
    expose:
      rembg: 8001:8000
    # rembg fetches its model on first use
    egress: internet
```

`internet` is the only thing `egress` may say. A list of hostnames is not
offered: names resolve to addresses that change, and a firewall written from
today's answer stops matching tomorrow without telling anyone.

**The internet is reached through a proxy.** Your containers are given
`HTTPS_PROXY` and `HTTP_PROXY`, and web requests — HTTP and HTTPS, ports 80
and 443 — go out through them. Most tools and libraries honour those variables
without being asked; one that does not, or that needs another port, cannot
reach the internet from an extra. Set them in your compose file and they are
replaced.

If you leave it out and your container needs it, the symptom is a container
that starts and then hangs on its first request out — worth knowing, because it
reads like a broken image.

## Reaching it from your application

Your application is given the address in its environment, because the address
is the one thing you cannot write down yourself — it is assigned when the
machine is built.

For the example above:

```
EXTRA_VOYAGER_HOST=10.16.0.7
EXTRA_VOYAGER_REMBG_PORT=8001
EXTRA_VOYAGER_EASYOCR_PORT=8002
```

So `http://$EXTRA_VOYAGER_HOST:$EXTRA_VOYAGER_REMBG_PORT` reaches it. The ports
are there so nothing has to be written twice and kept in step by hand; a
service that declares several gets the first, and your manifest names the rest.

Nothing on an extra is reachable from the internet, and nothing on it is served
through your site's domain.

## What your containers may use

The machine is yours, and the platform puts limits on the containers so that
one of them cannot take all of it and leave nothing answering.

**Memory is divided between the services in your file.** One service has the
machine's memory; two have half each. This is the one limit that has to be
shared out, because a container that runs out of memory is killed — and if the
containers could between them ask for more than the machine has, what gets
killed is decided by the kernel rather than by which one overran.

**CPU is not divided.** Every container may use the whole machine, and they
share it when more than one is busy. A tool that is working while the others
wait gets everything.

Each container may also run 512 processes.

So a second service in your file halves the memory of the first. If you run
several, size the machine for their total rather than for the largest — and if
one of them wants a lot on its own, it is usually better on an extra of its own
than sharing with three others.

You do not write these limits and cannot raise them from the file. If your
machine is the wrong size for what you are running, resize it, add another
extra on the [Resources tab](upgrade.md#adding-a-service), or ask support.

## Knowing it is unwell

The console shows what each of your containers is doing, from the machine's own
report: whether it is running, and — if your image declares a `healthcheck` —
whether that check is passing. A container that is running and well is not
listed; one that has stopped or is failing its check is.

An image that declares no healthcheck is never reported as healthy. We have not
asked it anything, and saying otherwise would be inventing good news.

**A container being unwell is not by itself an outage**, and we do not treat it
as one. Your extra's containers do not fail your deploys — an image we did not
write does not get to block your releases — and an extra's own downtime is not
counted against your uptime promise.

What does count is your site. Uptime is measured at your hostnames, so if your
site starts returning errors because the tool it calls is down, your site is
down and that counts. Design for the tool being unavailable: a request that
hangs waiting on it is worse for your visitors than a page that renders without
whatever it would have added.

## Uptime and support

**Your uptime SLA does not cover an extra.** We cannot promise the availability
of a container we did not write and do not update, so an extra's downtime is
not counted against the promise — and you are not charged for the SLA on it
either. The uptime price is calculated from your other machines.

Worth knowing before it matters: uptime is measured against your site's
hostnames. If your site depends on an extra and starts returning errors because
that extra is down, your site is down, and that does count. What is excluded is
the extra's own availability, not what your application does without it.

**Support does cover it**, and the support price includes it. If something on
an extra needs a person, that is what support is for.

## What it does not have

No release of your code, no `.env`, no cron, and no shell. An extra is not
somewhere your application runs — it is something your application calls.
Secrets your containers need are yours to put in their `environment`, or to ask
support about; the environment's own variables are not handed to them.

Source: https://docs.vallic.com/extras.md

---

# ZooKeeper

> Coordination for Solr

Coordination for Solr. Never selected on its own; [Solr](stack-solr.md) pulls
it in, and it goes where Solr goes — on the same machine, whether that is your
web server or a search machine of its own.

Solr runs in cloud mode, and cloud mode keeps its configuration — the
collections, their schemas, the login — in ZooKeeper rather than on Solr's own
disk. It runs as a single node, keeps its data on disk so a restart loses
nothing, and clears out its old snapshots once a day.

Your application never talks to it. Solr finds it at `zookeeper:2181`, and
nothing else needs to. Its memory is sized from the machine it runs on.

## Versions

| Version | Status |
| --- | --- |
| `3.9` | Supported, and the default |

Pin the version, not the build: name `3.9` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

The version you name is the one your environment runs from the next deploy.
This service keeps data, and not every version can read another's — before
you change it, read [Changing a service's version](service-versions.md).

## What you can change

Nothing on this one. Everything it takes decides where it connects, what it
is, or whether it starts — and an application able to set those could break
its own environment in ways nothing here would catch.

Source: https://docs.vallic.com/stack-zookeeper.md

---

# OpenSMTPD

> Outbound mail relay for environments not using a hosted provider

Outbound mail relay. Every site gets one, and it is what talks to the
outside world, so an application only has to know where it is
(`SMTP_HOST` — see [Variables](variables.md)).

A Drupal site using [`settings.vallic.php`](framework-drupal.md) is pointed at
it already and needs nothing.

## Two ports, two hops

**Your site to the relay: port 25, inside the stack.** That hop never leaves
the environment's private network, and nothing blocks it.

**The relay to the world: port 587, through your provider.** Set a relay host
and mail goes there over STARTTLS on 587 — the submission port every sending
provider takes. Without one, the relay tries to deliver straight to each
recipient's mail server, which is always port 25 — and **outbound port 25 is
blocked by default at most cloud providers, the ones this platform runs on
included.** Mail then waits in the queue and never arrives. Set a relay host
for any environment that sends mail.

It is the better answer anyway. Sending straight from a cloud address is the
thing most likely to put your mail in a spam folder — the address has no
reputation and often sits in a range that has.

## Relaying through your own provider

```yaml
version: 1
type: drupal

services:
  - mariadb: '11.8'
  - opensmtpd:
      version: '7.8'
      environment:
        RELAY_HOST: smtp.sendgrid.net
        RELAY_PORT: '587'
        RELAY_USER: apikey
```

**The password does not go here.** Anything set on a service is written into
the environment the platform renders, and that is stored where a task can be
read from. Add `RELAY_PASSWORD` as a **secret** project variable instead: it is
encrypted at rest, never shown again, and every container is given the whole
environment — so the relay receives it without your manifest naming it.

That split is the general rule on this platform, not a quirk of mail: what is
safe to read belongs in the repository, and what is not belongs in the console.

`RELAY_PORT` defaults to `587` and `RELAY_PROTO` to `smtp+tls` (STARTTLS), so
for most providers `RELAY_HOST` and `RELAY_USER` are all there is to write.

| Provider | `RELAY_HOST` | `RELAY_USER` | `RELAY_PASSWORD` |
|---|---|---|---|
| SendGrid | `smtp.sendgrid.net` | `apikey`, literally | An API key |
| Mailgun | `smtp.mailgun.org` (`smtp.eu.mailgun.org` in the EU) | The domain's SMTP login | Its SMTP password |
| Postmark | `smtp.postmarkapp.com` | A server API token | The same token |
| Amazon SES | `email-smtp.<region>.amazonaws.com` | SMTP credentials made in SES — not an IAM access key | Their password |
| Brevo | `smtp-relay.brevo.com` | Your SMTP login | An SMTP key |
| Mailjet | `in-v3.mailjet.com` | An API key | Its secret key |
| MailerSend | `smtp.mailersend.net` | The domain's SMTP user | Its password |

Check the values against your provider's own page; these are where each
publishes its relay today. A provider that offers only implicit TLS on 465
takes `RELAY_PROTO: smtps` and `RELAY_PORT: '465'`.

## Deliverability is still yours

The relay sends what it is given. Whether it arrives depends on SPF, DKIM and
DMARC records on the domain you send *from*, and those live in your DNS. A
provider will tell you which records it needs; nothing here can publish them
for you, because it is your domain.

## Versions

| Version | Status |
| --- | --- |
| `7.8` | Supported, and the default |
| `7.6` | **Deprecated** — still runs, but move to something newer |
| `7.5` | **Deprecated** — still runs, but move to something newer |

A deprecated version still runs and is still what some sites are on. It is
listed so you can move before it goes, rather than finding out on the
morning a build stops resolving it.

Pin the version, not the build: name `7.8` and the platform matches it to the
current build, so a security rebuild reaches you without anybody editing a
repository.

## What you can change

In [`vallic.yaml`](configuration.md) — what each one does is on [Service settings](service-settings.md):

```yaml
services:
  - opensmtpd:
      version: '7.8'
      environment:
        RELAY_HOST: …
        RELAY_PORT: …
        RELAY_PROTO: …
        RELAY_USER: …
        OPENSMTPD_MAX_MESSAGE_SIZE: …
        OPENSMTPD_EXPIRE: …
        OPENSMTPD_BOUNCE_WARN: …
```

Anything not on this list refuses the deploy, naming the variable — rather
than being accepted and quietly ignored.

Source: https://docs.vallic.com/stack-opensmtpd.md

---

# Drupal

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

## The manifest

The whole file our own Drupal demos deploy with — Umami and Commerce Kickstart
run exactly this:

```yaml
version: 1
type: drupal

runtime:
  php: '8.5'
  # Installing a profile is the heaviest thing a Drupal site ever does, and
  # 256M is where an install dies halfway with nothing anybody can act on.
  memory_limit: 512M

services:
  - mariadb: '11.8'
  - valkey: '9.0'

build:
  steps:
    - name: PHP dependencies
      run: composer install --no-dev --optimize-autoloader
  # What Composer installs, kept for the next build, which then only fetches
  # what changed rather than unpacking every module again.
  cache:
    - vendor
    - web/core
    - web/modules/contrib
    - web/themes/contrib
    - web/profiles/contrib
    - web/libraries

deploy:
  steps:
    - sh scripts/deploy.sh
  on_failure: rollback

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

health:
  path: /
  timeout: 10
```

And the deploy script it names, `scripts/deploy.sh`. The first deploy finds an
empty database and installs the site; every deploy after that runs
`drush deploy` — database updates, config import, cache rebuild:

```sh
#!/bin/sh
set -eu

root="${COMPOSER_ROOT:-$(pwd)}"
drush="$root/vendor/bin/drush"
cd "${DRUPAL_ROOT:-$root/web}"

if "$drush" status --field=bootstrap 2>/dev/null | grep -qi successful; then
  "$drush" deploy --yes
  exit 0
fi

# An admin password that is never in the repository and is the same after
# every deploy: derived from VALLIC_ENTROPY, which each environment has.
password=$(php -r 'echo substr(hash("sha256", (string) getenv("VALLIC_ENTROPY")), 0, 20);')

"$drush" site:install standard --yes --account-name=admin --account-pass="$password"
```

A site that already has its database — moved in with `vallic db import` — never
reaches the install: its first deploy is a `drush deploy` like every other.

### With a theme to build

```yaml
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:
  cache:
    - vendor
    - web/core
    - web/modules/contrib
  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's and npm's downloads are cached between builds without being asked
for. What Composer *installs* — `vendor/` and the directories Drupal's
installers put modules in — is not, and `build.cache` keeps it: the next build
checks it against `composer.lock` and fetches only what changed. See
[Configuration](configuration.md#buildcache) for the rules.

**`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](service-versions.md) 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](configuration.md) 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.

### Removing a module takes two releases

The new code is live **before** `drush deploy` runs, so for those seconds the
site runs new code against the old database. That matters when a module goes:

1. **First release:** uninstall it — an update hook calling
   `\Drupal::service('module_installer')->uninstall(['the_module'])` — and keep
   its code in `composer.json`.
2. **A later release,** once every environment has run that hook: remove the
   code.

Removed in the same release, the module is still enabled while its code is
gone: the site breaks until the update runs, and the update hook that should
uninstall it can break with it. Before deploying the removal to an
environment, `drush pm:list` there should no longer show the module.

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 deploy does not clear the CDN. With the CDN on, see
[After a deploy](cdn.md#after-a-deploy) for the step that does.

## 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](variables.md) 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:

```php
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.

```php
<?php

/**
 * @file
 * Settings for a Drupal site running on Vallic Cloud.
 *
 * Copy this file next to settings.php and include it from there, after
 * anything it should be allowed to override:
 *
 * @code
 * if (file_exists($app_root . '/' . $site_path . '/settings.vallic.php')) {
 *   include $app_root . '/' . $site_path . '/settings.vallic.php';
 * }
 * @endcode
 *
 * Everything it sets comes from the environment the platform writes for the
 * container — the names are documented in the handbook under "Variables". The
 * platform never edits this file: it is yours, committed with the project,
 * and what it reads is the contract. Anywhere else — DDEV, a laptop, another
 * host — none of those names are set, and the file does nothing.
 */

use Drupal\Core\Installer\InstallerKernel;
use Symfony\Component\HttpFoundation\Request;

// Only on the platform. Without this name nothing below is present either.
if (getenv('VALLIC_ENVIRONMENT') === FALSE) {
  return;
}

$settings['vallic_environment'] = getenv('VALLIC_ENVIRONMENT');
$settings['vallic_environment_type'] = getenv('VALLIC_ENVIRONMENT_TYPE') ?: 'development';

// Database. The service is reachable by DB_HOST from every container in the
// stack; the credentials were generated once with the environment.
if (getenv('DB_HOST')) {
  $driver = getenv('DB_DRIVER') ?: 'mysql';
  $databases['default']['default'] = [
    'driver' => $driver,
    'host' => getenv('DB_HOST'),
    'port' => getenv('DB_PORT') ?: '3306',
    'database' => getenv('DB_NAME'),
    'username' => getenv('DB_USER'),
    'password' => getenv('DB_PASSWORD'),
    'prefix' => '',
  ];
  if ($driver === 'mysql') {
    // READ COMMITTED is what Drupal recommends for MariaDB and MySQL; it
    // avoids the gap-lock deadlocks the default isolation level produces
    // under concurrent cache writes.
    $databases['default']['default']['init_commands'] = [
      'isolation_level' => 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
    ];
  }
  unset($driver);
}

// The hash salt. VALLIC_ENTROPY is 256 random bits generated once for this
// environment and never changed, so sessions and one-time links survive every
// deploy — and nothing secret is committed.
if (getenv('VALLIC_ENTROPY')) {
  $settings['hash_salt'] = getenv('VALLIC_ENTROPY');
}

// Files. Both directories are mounted into every release, so uploads outlive
// the code that received them. Drupal wants the public path relative to the
// document root, so it is derived from the absolute one the platform gives.
if (getenv('VALLIC_PUBLIC_DIR') && str_starts_with(getenv('VALLIC_PUBLIC_DIR'), $app_root . '/')) {
  $settings['file_public_path'] = substr(getenv('VALLIC_PUBLIC_DIR'), strlen($app_root) + 1);
}
if (getenv('VALLIC_PRIVATE_DIR')) {
  $settings['file_private_path'] = getenv('VALLIC_PRIVATE_DIR');
}

// Trusted hosts: exactly the hostnames the edge routes here. Nothing else can
// reach the container, but Drupal builds absolute URLs from the Host header
// and should not take anyone's word for it.
if (getenv('VALLIC_HOSTNAMES')) {
  $settings['trusted_host_patterns'] = array_map(
    static fn(string $hostname): string => '^' . preg_quote(trim($hostname), '/') . '$',
    explode(',', getenv('VALLIC_HOSTNAMES')),
  );
}

// The edge terminates TLS and proxies to the container, so the request Drupal
// sees comes from the proxy over plain HTTP. Trusting the address the request
// arrived from — which is only ever the edge — restores the client's address,
// scheme and port from the X-Forwarded headers the edge sets.
$settings['reverse_proxy'] = TRUE;
$settings['reverse_proxy_addresses'] = ['REMOTE_ADDR'];
$settings['reverse_proxy_trusted_headers'] = Request::HEADER_X_FORWARDED_FOR
  | Request::HEADER_X_FORWARDED_HOST
  | Request::HEADER_X_FORWARDED_PORT
  | Request::HEADER_X_FORWARDED_PROTO;

// Cache in Redis (or Valkey — same protocol, same name) when the stack runs
// one and the site ships the module. The module does not have to be installed:
// its services are registered here and its classes made loadable, so the
// container cache itself lives in Redis from the first request — and so does
// the installer's, which is why the block steps aside during installation.
if (getenv('REDIS_HOST')
  && extension_loaded('redis')
  && file_exists($app_root . '/modules/contrib/redis/redis.services.yml')
  && !InstallerKernel::installationAttempted()) {
  $settings['redis.connection']['interface'] = 'PhpRedis';
  $settings['redis.connection']['host'] = getenv('REDIS_HOST');
  $settings['redis.connection']['port'] = (int) (getenv('REDIS_PORT') ?: 6379);
  // Every environment on a server shares nothing, but a prefix costs nothing
  // and keeps two sites apart should they ever share a service.
  $settings['cache_prefix'] = getenv('VALLIC_SLUG') ?: 'drupal';
  // Every bin not set otherwise. Core already puts bootstrap, config,
  // discovery and routes on its chained backend — APCu in the process, Redis
  // behind it — and a bin's own default is read before this one, so those
  // need no line of their own; naming one is how a bin is taken off it.
  $settings['cache']['default'] = 'cache.backend.redis';
  // Lock, flood and cache-tag checksum in Redis too; then the module's own
  // services, so the backends exist before the module is enabled.
  $settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';
  $settings['container_yamls'][] = 'modules/contrib/redis/redis.services.yml';
  $class_loader->addPsr4('Drupal\\redis\\', 'modules/contrib/redis/src');
  // The container cache is read before the container exists, so it needs
  // its own definition of the services that reach Redis.
  $settings['bootstrap_container_definition'] = [
    'parameters' => [],
    'services' => [
      'redis.factory' => [
        'class' => 'Drupal\redis\ClientFactory',
      ],
      'cache.backend.redis' => [
        'class' => 'Drupal\redis\Cache\CacheBackendFactory',
        'arguments' => ['@redis.factory', '@cache_tags_provider.container', '@serialization.phpserialize'],
      ],
      'cache.container' => [
        'class' => '\Drupal\redis\Cache\PhpRedis',
        'factory' => ['@cache.backend.redis', 'get'],
        'arguments' => ['container'],
      ],
      'cache_tags_provider.container' => [
        'class' => 'Drupal\redis\Cache\RedisCacheTagsChecksum',
        'arguments' => ['@redis.factory'],
      ],
      'serialization.phpserialize' => [
        'class' => 'Drupal\Component\Serialization\PhpSerialize',
      ],
    ],
  ];
  // And the container itself chained the same way, which core cannot do for
  // it: the definition is a megabyte or so, read on every request, and from
  // APCu that is a local read where from Redis it is a fetch and an
  // unserialise. Redis stays authoritative.
  //
  // Wherever the APCu functions exist — not only where APCu is on — as core
  // decides for its own chained bins. The CLI usually has APCu off, and
  // drush is what rebuilds the container on a deploy. Chained, its write
  // moves the bin's last-write timestamp in Redis, which is what makes every
  // web server drop the copy in its own APCu; the fast half simply misses
  // and stores nothing. Left plain on the CLI, drush wrote to Redis alone,
  // the web servers went on running the container from before the deploy,
  // and rebuilt plugin definitions with modules the deploy had just
  // uninstalled — a search that failed days later, when APCu was emptied.
  if (function_exists('apcu_fetch')) {
    $settings['bootstrap_container_definition']['services'] += [
      'cache.container.consistent' => $settings['bootstrap_container_definition']['services']['cache.container'],
      'cache.container.fast' => [
        'class' => 'Drupal\Core\Cache\ApcuBackend',
        'arguments' => ['container', $settings['cache_prefix'], '@cache_tags_provider.container', '@datetime.time'],
      ],
      'datetime.time' => [
        'class' => 'Drupal\Component\Datetime\Time',
      ],
    ];
    $settings['bootstrap_container_definition']['services']['cache.container'] = [
      'class' => 'Drupal\Core\Cache\ChainedFastBackend',
      'arguments' => ['@cache.container.consistent', '@cache.container.fast', 'container'],
    ];
  }
}

// What differs by kind of environment. Production hides errors from visitors;
// everything else shows them to the people who are there to find them.
switch ($settings['vallic_environment_type']) {
  case 'production':
    $config['system.logging']['error_level'] = 'hide';
    $config['environment_indicator.indicator'] = [
      'name' => 'Production',
      'bg_color' => '#8b0000',
      'fg_color' => '#ffffff',
    ];
    break;

  case 'staging':
    $config['system.logging']['error_level'] = 'some';
    $config['environment_indicator.indicator'] = [
      'name' => 'Staging',
      'bg_color' => '#b86e00',
      'fg_color' => '#ffffff',
    ];
    break;

  default:
    $config['system.logging']['error_level'] = 'verbose';
    $config['environment_indicator.indicator'] = [
      'name' => ucfirst($settings['vallic_environment_type']) . ': ' . $settings['vallic_environment'],
      'bg_color' => '#005f87',
      'fg_color' => '#ffffff',
    ];
    break;
}

// Outbound mail through the stack's relay, when it runs one.
//
// Drupal sends through PHP's mail() by default, which hands the message to a
// sendmail binary the application container does not have — so a site with a
// relay sitting beside it still could not send a password reset until somebody
// found out why. The relay is what talks to the outside world; the application
// only has to be told where it is.
//
// Written as configuration rather than as a module setting, so it applies
// whether the site uses Symfony Mailer 1.x or 2.x, and is simply ignored by a
// site that uses neither. A project sending through a hosted provider sets its
// own key and overrides this in its own settings.
if (getenv('SMTP_HOST')) {
  foreach (['symfony_mailer', 'mailer_transport'] as $prefix) {
    $config[$prefix . '.settings']['default_transport'] = 'vallic';
    $config[$prefix . '.mailer_transport.vallic']['plugin'] = 'smtp';
    $config[$prefix . '.mailer_transport.vallic']['configuration'] = [
      'user' => '',
      'pass' => '',
      'host' => getenv('SMTP_HOST'),
      // 25, stated rather than read: the relay listens on it inside the stack
      // and the platform publishes no variable to change it. Reading one would
      // suggest it is configurable and quietly fall back the day somebody set
      // it expecting that to matter.
      'port' => '25',
    ];
  }
}
```

## 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**, through the stack's 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, inside the stack,
instead. The relay sends it on through the provider you give it as
`RELAY_HOST`, on port 587; without one, mail does not leave — see
[OpenSMTPD](stack-opensmtpd.md). A site that uses neither module is
unaffected; one that sends through a hosted provider's API 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:

```php
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);
}
```

**Vinyl Cache (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:

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

**The CDN**, when the project has one. Install
[Vallic Purge](https://www.drupal.org/project/vallic_purge) and there is
nothing to write here: it reads the purge settings from the environment and
clears the pages a change affects as it is saved. See
[CDN](cdn.md#drupal).

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

Source: https://docs.vallic.com/framework-drupal.md

---

# WordPress

> Running WordPress here — the manifest, and a wp-config that reads the environment instead of committing it.

WordPress needs no bootstrap file of ours, and very nearly no work: the
variables the platform writes are **already the names WordPress reads**.
`DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DB_HOST` are its own constants. All
`wp-config.php` has to do is read them from the environment rather than hold
them.

## The manifest

```yaml
version: 1
type: wordpress

runtime:
  php: '8.4'

services:
  - mariadb: '11.8'
  - valkey: '8'

build:
  # What Composer installs, kept for the next build.
  cache:
    - vendor
  steps:
    - composer install --no-dev --optimize-autoloader
    - 'mkdir -p bin && curl -fsSL -o bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x bin/wp'

cron:
  - name: wp-cron
    schedule: '*/5 * * * *'
    command: 'wp cron event run --due-now'
```

**`services` is checked, not obeyed, for the database.** A service such as
Valkey 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](service-versions.md) before you
change one that keeps data.

`wp-cli` is fetched as a build step rather than baked into the image, so it is
versioned with your project rather than with ours — the same reasoning as
`drush` living in a Drupal project's `vendor/bin`. `bin/` is on the `PATH` of
every PHP container, which is how `wp` is found. The `mkdir` is not decoration:
`curl` will not create the directory it is asked to write into.

The document root is the root of the repository, so `wp-config.php` and
WordPress itself sit beside `vallic.yaml` — the classic layout, not one that
nests WordPress in a subdirectory.

WordPress's own `wp-cron` fires on visitor requests, which makes scheduled work
depend on somebody turning up. Run it from `cron` instead and add
`define('DISABLE_WP_CRON', true);` to your config.

## What the platform runs for you

Two commands run in your PHP container without being asked for, and both are
wp-cli:

| When | Command | |
|---|---|---|
| Hourly | `wp cron event run --due-now` | Only if your manifest declares no `cron` of its own |
| After a backup is restored | `wp cache flush` | So the object cache does not serve what the restore replaced |

Hourly, as for Drupal. `--due-now` catches up on whatever is overdue, so
nothing scheduled is lost — but with `DISABLE_WP_CRON` set this is the only
thing running WordPress's schedule, so a post scheduled for 9:10 goes out at
10:00. If that is too late, declare your own, as the manifest above does every
five minutes. Declaring any `cron` replaces the platform's job, so to add a job
of your own, keep a `wp cron event run` beside it.

Both need `bin/wp` in the release, which is what the wp-cli build step puts
there. Without it the scheduled run fails every hour and the flush
after a restore fails with it, and nothing warns you beforehand — keep the
build step even if you never run wp-cli yourself.

A change to the schedule reaches an environment on its next deploy, which is
when its cron is written. However many web servers the environment has, cron
runs on one of them, so nothing scheduled runs twice.

## What is kept, and what is read-only

`wp-content/uploads` is linked out of every release into storage that outlives
it — it is what `VALLIC_PUBLIC_DIR` points at — so uploads survive a deploy
with nothing set in `wp-config.php`.

Everything else in the release is **read-only** while the site runs. Installing
or updating a plugin or theme from the dashboard cannot work, and neither can
WordPress's automatic updates: add plugins in the repository (or with Composer
in the build) and deploy. The config below turns those buttons off rather than
leaving them to fail.

## wp-config.php

```php
// The database, from the environment.
define('DB_NAME', getenv('DB_NAME'));
define('DB_USER', getenv('DB_USER'));
define('DB_PASSWORD', getenv('DB_PASSWORD'));
define('DB_HOST', getenv('DB_HOST') . ':' . (getenv('DB_PORT') ?: '3306'));
define('DB_CHARSET', 'utf8mb4');

// Salts, derived from the environment's own entropy rather than committed.
// One value, eight distinct keys: deriving them means they differ from each
// other and from every other environment, and rotating the entropy rotates
// all eight.
$vallic_entropy = getenv('VALLIC_ENTROPY') ?: '';
foreach ([
  'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY',
  'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT',
] as $vallic_salt) {
  define($vallic_salt, hash_hmac('sha256', $vallic_salt, $vallic_entropy));
}

// The edge terminates TLS and proxies over plain HTTP, so WordPress has to be
// told the scheme or it writes http:// into every absolute URL and redirects
// in a loop.
if (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') {
  $_SERVER['HTTPS'] = 'on';
}

if (getenv('PROJECT_BASE_URL')) {
  define('WP_HOME', getenv('PROJECT_BASE_URL'));
  define('WP_SITEURL', getenv('PROJECT_BASE_URL'));
}

// Debugging everywhere but production, and errors to the log, never to a
// visitor. The log goes to VALLIC_LOG_DIR because wp-content is read-only, and
// because that directory is collected with the rest of your logs.
define('WP_DEBUG', getenv('VALLIC_ENVIRONMENT_TYPE') !== 'production');
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', (getenv('VALLIC_LOG_DIR') ?: '/var/log/app') . '/wordpress.log');

// The release is read-only: code changes arrive by deploy, not from wp-admin.
define('DISALLOW_FILE_MODS', true);
define('AUTOMATIC_UPDATER_DISABLED', true);

define('DISABLE_WP_CRON', true);
```

The `X-Forwarded-Proto` line matters more than it looks. Without it WordPress
sees plain HTTP, writes `http://` into every generated URL, and a browser that
arrived over HTTPS is redirected back and forth until it gives up.

## More than one web server

WordPress core keeps logins in signed cookies rather than server-side
sessions, so a visitor stays logged in whichever machine answers. A plugin that
starts PHP sessions of its own is the exception: those live in files on one
machine, and need a Redis session handler once there are two.

The object cache is worth moving to Valkey at the same point, so every server
sees the same cached values. With the Redis Object Cache plugin, tell it where
the stack's Valkey is:

```php
if (getenv('REDIS_HOST')) {
  define('WP_REDIS_HOST', getenv('REDIS_HOST'));
  define('WP_REDIS_PORT', (int) getenv('REDIS_PORT'));
  define('WP_REDIS_PREFIX', getenv('VALLIC_SLUG'));
}
```

## Next

- [Variables](variables.md) — everything available to read
- [vallic.yaml](configuration.md) — the manifest in full
- [Logs](logs.md) — where your output goes and how to send a copy on

Source: https://docs.vallic.com/framework-wordpress.md

---

# Laravel

> Running Laravel here — the manifest, where migrations run, and the few lines Laravel needs to read the environment.

Laravel needs very little copied into it. The platform hands every container
its settings as **environment variables**, under the names Laravel already
reads — `DB_CONNECTION`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`,
`DB_PASSWORD`, and `REDIS_HOST` and `REDIS_PORT` when the stack runs Valkey or
Redis.

Do not commit a `.env`. Commit `.env.example` as usual; a committed `.env`
applies to every environment the code is deployed to, which is exactly what
the platform's per-environment variables exist to avoid.

Two things the platform does not give you, and Laravel will not start without
the first:

- **`APP_KEY`.** Set it on the environment as a secret variable under
  [Variables](variables.md), or derive it in `config/app.php` from
  `VALLIC_ENTROPY`, which is generated once per environment and never changes:

  ```php
  'key' => env('APP_KEY') ?: (getenv('VALLIC_ENTROPY')
      ? 'base64:' . base64_encode(substr(hash('sha256', getenv('VALLIC_ENTROPY'), true), 0, 32))
      : null),
  ```

- **`APP_URL`.** The environment's own address is `PROJECT_BASE_URL`, so
  `'url' => env('APP_URL', env('PROJECT_BASE_URL', 'http://localhost'))` in
  `config/app.php` saves setting it twice.

## Bootstrap: two things to add

`bootstrap/app.php` needs a few lines before `Application::configure()`, and
both are consequences of how the platform runs your code rather than
preferences.

```php
// The platform's variables, where env() can see them. Laravel reads $_ENV and
// $_SERVER, and PHP's command line leaves both empty of the environment — so
// without this a deploy step or a cron job sees no DB_HOST at all, and
// `artisan migrate` quietly falls back to Laravel's defaults.
foreach (getenv() as $name => $value) {
    $_ENV[$name] ??= $value;
    $_SERVER[$name] ??= $value;
}

// Compiled config and routes somewhere writable. The release is mounted
// read-only, so bootstrap/cache cannot be written and `config:cache` fails.
// Per release, so a rollback does not run the configuration of the release
// it rolled back from.
$compiled = (getenv('VALLIC_PRIVATE_DIR') ?: dirname(__DIR__) . '/private')
    . '/bootstrap/' . basename(dirname(__DIR__));
if (!is_dir($compiled)) {
    @mkdir($compiled, 0775, true);
}
foreach ([
    'APP_CONFIG_CACHE' => 'config.php',
    'APP_ROUTES_CACHE' => 'routes.php',
    'APP_EVENTS_CACHE' => 'events.php',
    'APP_SERVICES_CACHE' => 'services.php',
] as $variable => $file) {
    $_ENV[$variable] = $_SERVER[$variable] = $compiled . '/' . $file;
    putenv($variable . '=' . $compiled . '/' . $file);
}
```

The packages manifest stays in `bootstrap/cache`: `composer install` writes it
during the build, while the release is still writable, and nothing rewrites it
afterwards.

## The manifest

```yaml
version: 1
type: laravel

runtime:
  php: '8.4'

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

build:
  # Composer's and npm's downloads are cached for you. What Composer installs,
  # and Vite's own cache, are not.
  cache:
    - vendor
    - node_modules/.vite
  steps:
    - composer install --no-dev --optimize-autoloader
    # The PHP image has no npm, so the assets build in the Node image.
    - name: Assets
      image: node
      run: npm ci && npm run build

deploy:
  steps:
    # storage/framework is kept outside the release and starts empty, so the
    # directories your repository ships there are not the ones Laravel sees.
    - 'mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views'
    - 'php artisan migrate --force'
    - 'php artisan config:cache'
    - 'php artisan route:cache'
    - 'php artisan view:cache'
  on_failure: rollback

health:
  path: /up

workers:
  - name: queue
    command: 'php artisan queue:work --sleep=1 --tries=3'
    replicas: 2

cron:
  - name: scheduler
    schedule: '* * * * *'
    command: 'php artisan schedule:run'
```

**`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](service-versions.md) before you
change one that keeps data.

With no `cron` declared, the platform runs `schedule:run` every five minutes.
Laravel's scheduler only runs what is due at the minute it is called, so on
that default an `everyMinute()` task runs every five minutes and a task at a
minute that is not a multiple of five — `dailyAt('13:32')` — never runs.
Declare the scheduler every minute, as above, if you need that; declaring any
`cron` replaces the platform's job.

If you run the scheduler as a process instead — `php artisan schedule:work`
as a worker — the platform adds no cron for it, so tasks do not run twice.
It is not the queue: `schedule:run` decides *when* work happens, and
`queue:work`, below, processes what it and your application put on the
queue.

The schedule runs on one machine only, however many web servers the
environment has, so a task is never started twice.

## Where migrations run

`php artisan migrate --force` in `deploy.steps`. `--force` because the machine
is non-interactive and Laravel refuses to migrate in production without it.

It runs **on the machine, after the release is live** — a build has no
database, and the artifact it produces could be deployed to staging or
production. The caching commands go after the migration and in that order:
`config:cache` first, because the others read configuration. Nothing runs
here unless you list it, and on more than one web server the steps run once,
on one of them.

`/up` is the health route Laravel ships; with `health` set, a deploy waits for
it to answer before it counts as done, and `on_failure: rollback` puts the
previous release back if it never does.

Never cache config locally and commit the result. A cached config file freezes
whatever the environment said at the moment it was built, which on this
platform is the wrong environment's.

When a backup is restored, the platform runs `php artisan optimize:clear`
afterwards so nothing cached from the replaced database is served. That clears
the config and route caches too; the next deploy builds them again.

## The queue

`queue:work`, not `queue:listen`, and as a **worker** rather than as cron. A
worker is a process the platform keeps running and restarts if it stops; cron
would start a new one every minute on top of the last.

Workers are restarted on deploy, which is what `queue:restart` exists to do
elsewhere — you do not need it here.

## Sessions, cache and storage

Point `CACHE_STORE` and `SESSION_DRIVER` at `redis`; the stack's Valkey answers
on `REDIS_HOST` and `REDIS_PORT`, which are the names `config/database.php`
already reads. Without Valkey, `SESSION_DRIVER=database` works as well. Set
these on the environment under [Variables](variables.md).

Once an environment has more than one web server this stops being a
preference. Laravel's default, `file`, keeps sessions in `storage/framework`,
which the platform keeps outside the release and shares between machines over
the network — so on every machine but one, each request reads and writes its
session over the network.
Valkey is what sessions shared between servers are for.

Four paths inside your code are linked out of the release, so they survive
every deploy:

| In your code | Kept as |
|---|---|
| `public/storage` | `VALLIC_PUBLIC_DIR` — served |
| `storage/app` | `VALLIC_PRIVATE_DIR` — never served |
| `storage/logs` | a directory under the private one |
| `storage/framework` | a directory under the private one |

Everything else in the release is read-only while the site runs.

`public/storage` is already linked, so there is no `storage:link` to run —
but Laravel's `public` disk writes to `storage/app/public` by default, which is
now inside the private directory and not served. Point it at the public one:

```php
// config/filesystems.php
'public' => [
    'driver' => 'local',
    'root' => env('VALLIC_PUBLIC_DIR', storage_path('app/public')),
    'url' => env('APP_URL').'/storage',
    'visibility' => 'public',
],
```

## Logging

Laravel's `daily` channel writes to `storage/logs`. That directory survives
deploys, but it is the application's own: nothing collects it, so its lines
never reach the console's logs or a destination you forward to.

Point it at `VALLIC_LOG_DIR` instead. The variable is already set in every
container, and the directory behind it outlives every release:

```php
// config/logging.php
'daily' => [
    'driver' => 'daily',
    'path' => env('VALLIC_LOG_DIR', '/var/log/app') . '/laravel.log',
    'level' => env('LOG_LEVEL', 'debug'),
    // Rotate, and the platform's limit never comes up.
    'days' => 7,
],
```

The fallback in `env()` is there so the same config works on your laptop,
where the variable does not exist.

Then set `LOG_CHANNEL=daily` on the environment — that one is yours to set,
under [Variables](variables.md), because the platform does not write it.

Anything in that directory is collected with the rest of your logs, kept on
the machine for a week, and forwarded if you have set up a destination. There
is a size limit and rotation keeps you well under it — see
[Logs](logs.md).

## Next

- [Logs](logs.md) — where your output goes and how to send a copy on
- [Variables](variables.md) — everything available to read
- [Storage](storage.md) — what survives a deploy, and what does not

Source: https://docs.vallic.com/framework-laravel.md

---

# Symfony

> Running Symfony here — the manifest, where migrations run, and the DATABASE_URL Doctrine wants.

A Symfony application is a **PHP** project here — the console's choice for
"Symfony, or any other PHP application". There is no Symfony type, and it would
change nothing: the image is the same plain PHP image, served from `public/`.
In `vallic.yaml`, `type: symfony` and `type: php` are both accepted.

The platform hands every container **`DATABASE_URL`** as an environment
variable — the single value Doctrine reads, assembled from the same
credentials as everything else.

It is one string rather than parts on purpose: Doctrine will not build a URL
from a host and a database name, so a platform that emitted only the parts
would leave every Symfony project writing the same three lines of glue.

Keep committing `.env` as Symfony expects — real environment variables win
over it — but leave `DATABASE_URL` out of it, or put your laptop's value in an
uncommitted `.env.local`. A value in the committed file is one more place the
database can come from, and on the command line, where deploy steps run, it is
not guaranteed to lose.

## What you set yourself

**`APP_ENV`.** The platform does not set it, and Symfony's own `.env` says
`dev`. Pin `APP_ENV=prod` in the committed `.env`, or set it per environment
under [Variables](variables.md) if you want staging to behave differently from
production. Left at `dev`, production runs in debug mode.

**The secret.** `VALLIC_ENTROPY` is generated once per environment and never
changes, so it can be the secret without anything being committed:

```yaml
# config/packages/framework.yaml
framework:
    secret: '%env(VALLIC_ENTROPY)%'
```

## The manifest

```yaml
version: 1
type: symfony

runtime:
  php: '8.4'

services:
  - postgres: '18'
  - valkey: '8'
  # Only for the asset build below; nothing of it runs beside the site.
  - node: '24'

build:
  # Composer's and npm's downloads are cached for you. What Composer installs,
  # and Webpack Encore's own cache, are not.
  cache:
    - vendor
    - node_modules/.cache
  steps:
    - composer install --no-dev --optimize-autoloader
    # The PHP image has no npm, so the assets build in the Node image.
    - name: Assets
      image: node
      run: npm ci && npm run build

deploy:
  steps:
    - 'php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration'
    - 'php bin/console cache:clear --no-warmup'
    - 'php bin/console cache:warmup'
  on_failure: rollback

workers:
  - name: messenger
    command: 'php bin/console messenger:consume async --time-limit=3600'
    replicas: 2
```

**`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](service-versions.md) before you
change one that keeps data.

The cache steps only work once the cache has somewhere to go — see *Var and
cache directories* below.

## Where migrations run

`doctrine:migrations:migrate` in `deploy.steps`, on the machine and after the
release is live. `--no-interaction` because nothing is there to answer the
prompt, and `--allow-no-migration` so a deploy with nothing to migrate is a
success rather than a non-zero exit that rolls the release back.

Nothing runs here unless you list it, and on more than one web server the
steps run once, on one of them.

## Cron

A PHP project has no scheduler the platform knows how to name, so nothing is
scheduled for you. If you use Symfony Scheduler, run its transport as a worker;
for anything else, declare it:

```yaml
cron:
  - name: cleanup
    schedule: '0 3 * * *'
    command: 'php bin/console app:cleanup'
```

Cron runs on one machine only, however many web servers the environment has.

For the same reason, restoring a backup clears no cache for you, as it does for
Drupal, Laravel and WordPress. Run `cache:pool:clear` yourself if your pools
hold anything the restore replaced.

## Messenger

`messenger:consume` as a **worker**, with `--time-limit` set. A consumer that
runs forever slowly leaks the memory of everything it has handled; giving it an
hour and letting the platform restart it is the usual answer, and restarts are
free here because the worker is supervised.

`--time-limit=3600` means it exits cleanly every hour and is started again. It
is not a failure and does not appear as one.

## Var and cache directories

The release is mounted **read-only** while the site runs, so `var/` cannot be
written — not by a request, and not by `cache:clear` in a deploy step. The one
writable directory a PHP project is given is `VALLIC_PRIVATE_DIR` (`private/`
at the root of the release), which is never served and outlives every release.
Move the cache and the logs there in `src/Kernel.php`:

```php
public function getCacheDir(): string
{
    return $this->writableDir('cache');
}

public function getLogDir(): string
{
    return $this->writableDir('log');
}

private function writableDir(string $name): string
{
    $base = getenv('VALLIC_PRIVATE_DIR') ?: $this->getProjectDir() . '/private';
    $path = sprintf('%s/symfony/%s/%s', $base, $this->environment, $name);

    if (!is_dir($path)) {
        @mkdir($path, 0775, true);
    }

    return $path;
}
```

Logs you want collected belong in `VALLIC_LOG_DIR` rather than there: point
Monolog's file handler at `%env(VALLIC_LOG_DIR)%/symfony.log` and its lines
reach the console's logs with everything else — see [Logs](logs.md).

**Uploads** have no directory of their own on a PHP project: there is no
`VALLIC_PUBLIC_DIR`, because there is no framework convention for where one
would be. Declare the directory you serve them from as a mount, and it is kept
outside the release and linked back into every one:

```yaml
mounts:
  - public/uploads
```

## Sessions

Symfony's default keeps sessions in files on the machine that answered the
request. With one web server that is fine; with more than one, a visitor is
logged out whenever the next request lands on another machine. Keep them in
Valkey, which answers on `REDIS_HOST` and `REDIS_PORT`:

```yaml
# config/packages/framework.yaml
framework:
    session:
        handler_id: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%'
```

## Next

- [Variables](variables.md) — everything available to read
- [Storage](storage.md) — what survives a deploy, and what does not
- [Logs](logs.md) — where your output goes and how to send a copy on

Source: https://docs.vallic.com/framework-symfony.md

---

# Node and Go

> Running an application that serves its own HTTP, and why there is no config file for it.

A Node or Go application serves its own HTTP and replaces PHP-FPM and nginx
rather than joining them. In front of it is the platform's proxy, which
terminates TLS, routes the hostname and compresses responses.

**There is no bootstrap file for either, and that is not an omission.** The PHP
frameworks need one because each has its own idea of where configuration comes
from; a Node or Go application reads the environment directly, which is what
the platform already hands it. Shipping a file would be imposing a framework
opinion on applications that do not have one.

What is worth knowing is which names to read.

## What to read

```
PORT                what to listen on — bind this, not a port of your choosing
DATABASE_URL        one connection string, which is what most drivers take
REDIS_HOST          the cache, when the stack runs one — with REDIS_PORT
VALLIC_PRIVATE_DIR  a directory that outlives every release — where uploads go
VALLIC_LOG_DIR      where log files go, if you write any, to be collected
```

`DATABASE_URL` exists for exactly this: `pg.Pool({connectionString})`,
`sql.Open("postgres", url)` and their equivalents all take a URL, and
assembling one from five parts is the glue nobody should be writing. The parts
are there too if you prefer them — see [Variables](variables.md).

**Bind the port you are given**, and bind it on all interfaces rather than
`localhost`: the proxy reaches your process across the container boundary, and
a server listening on `127.0.0.1` inside its own container is reachable by
nothing.

## What is writable

The release is mounted **read-only** while the application runs: what the
build produced is what serves, and nothing can change it in place. Anything
your application writes and expects to keep goes under `VALLIC_PRIVATE_DIR`
(`private/` at the root of the release), which is kept outside the release and
linked back into every one. There is no public counterpart — nginx is not in
front of you, so what gets served from where is your application's decision.

A directory at a path of your own choosing works the same way when declared as
a mount:

```yaml
mounts:
  - uploads
```

Output to stdout and stderr is collected without any of this — see
[Logs](logs.md).

## More than one web server

Once an environment has two, consecutive requests from one visitor can land on
different machines, so anything kept in a process's memory — `express-session`'s
default store, an in-memory cache — is not there on the next request. Keep
sessions in Valkey, at `REDIS_HOST` and `REDIS_PORT`; `connect-redis` and its
equivalents take exactly those two.

## What starts it, and where it listens

Two keys, and both have defaults you can usually leave alone.

| | Node | Go |
|---|---|---|
| Default command | `npm start` | none — **`start` is required** |
| Default `PORT` | `3000` | `8080` |

`start` is the command that serves. It only applies to a runtime that is its
own web server, which is why no PHP framework has one — there, PHP-FPM is the
process whatever the code says.

`port` overrides the default. Setting it changes the `PORT` your application is
given *and* the port the platform expects to reach, together — which is the
reason to set it there rather than hard-coding a number in your code. If you
read `PORT` and never set `port`, everything already agrees.

```yaml
start: node dist/server.js
port: 4000
```

### A binary that does not serve

Nothing requires `start` to listen. It is a command, and a command that
processes a queue, consumes a stream or sits in a loop is a perfectly good
one — leave `health` out and there is no check to fail, so the deploy
succeeds on the process staying up rather than on an HTTP answer.

What you get anyway is the environment's hostname pointed at that container,
and with nothing listening it answers 502. Harmless, but it means the
environment looks broken to anyone who visits it.

So a background process that belongs *beside* a site is better written as a
worker, which is supervised the same way but has no port and no hostname
attached to it:

```yaml
workers:
  - name: queue
    command: bin/worker
```

Workers run your application's own image against the same release, so they
need an application in the stack to run in — they are extra processes for a
deployed application, not a way to deploy a process on its own.

## Node

```yaml
version: 1
type: nodejs

runtime:
  node: '24'

services:
  - postgres: '18'

build:
  # npm's download cache is provided. This is the bundler's build cache,
  # which is what makes the second build of an unchanged app quick.
  cache:
    - .next/cache
  steps:
    - npm ci
    - npm run build
    - npm prune --omit=dev

deploy:
  steps:
    - 'npm run migrate'
  on_failure: rollback

health:
  path: /healthz
```

A database under `services` is a check, not a request: it has to be the one
the environment was created with, because a commit cannot add or change a
database. Other services, such as Valkey, are started by the next deploy if the
environment lacks them. The version after each name is required, and is what
the environment runs from the next deploy — see
[Service upgrade](service-versions.md) before you change one that keeps data.

`node_modules` is the runtime rather than a build input, so the release keeps
it — `npm prune` at the end of the build drops what production does not need.

**No `start` here, on purpose.** Left unsaid, the image runs `npm start`, so a
project with a `start` script in its `package.json` needs nothing. Add one when
your entry point is something else:

```yaml
start: node dist/server.js
```

## Go

```yaml
version: 1
type: golang

runtime:
  go: '1.27'

build:
  steps:
    - go build -o bin/server ./cmd/server

start: bin/server

health:
  path: /healthz
```

**`start` is required for Go.** The image is the upstream Go image and has no
default worth running — nothing can guess that your binary is `bin/server`, so
leaving it out deploys a container that starts and immediately exits.

A Go build produces one binary and the release is that binary, which is why
there is nothing to prune and usually nothing to run at deploy time. If your
schema needs migrating, a `deploy` step running your own migration tool is the
place for it.

The language version is keyed by the language — `node`, `go` — whatever the
`type` says: `nodejs`, `node`, `next` and `express` all mean Node, and `golang`
and `go` both mean Go.

## Cron

Nothing is scheduled for you. Neither runtime has one scheduler the platform
could name, so periodic work is yours to declare, and runs in the application's
container against the live release:

```yaml
cron:
  - name: digest
    schedule: '0 * * * *'
    command: node dist/jobs/digest.js
```

It runs on one machine only, however many web servers the environment has.
For the same reason, restoring a backup clears no cache for you: the platform
knows no command that would.

## Health checks

Worth setting for both. With `health.path` set, a deploy waits for that path to
answer before it counts as done, and `on_failure: rollback` puts the previous
release back if it never does. The platform also watches production and tells
you when it stops answering — see [Notifications](notifications.md) — and an
endpoint that checks the database is a far better signal than one that returns
200 because the process is alive.

## Next

- [Variables](variables.md) — everything available to read
- [vallic.yaml](configuration.md) — the manifest in full
- [Logs](logs.md) — where your output goes

Source: https://docs.vallic.com/framework-node.md
