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, 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:
extra:
voyager:
expose:
rembg: 8001:8000
easyocr: 8002:8000
.vallic/extra/voyager.yml says what runs — an ordinary compose file:
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:
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:
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
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:
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:
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:
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, 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.