Automating applications
Create, configure, deploy and remove applications from a terminal or a script, with every flag explained.
Everything the console does for an application, the CLI does with flags. This page covers creating one, changing it afterwards, and the commands you will run day to day. For the pipeline around them, see Deploying from CI/CD.
Naming things
An application is named by its slug, its name, or its id. A unique prefix of the
slug or id also works, which is convenient at a terminal and risky in a script:
api would match api-gateway if that were the only one. Scripts should use
apps get --exact when they ask whether something exists.
Flags may come before or after the name:
isogrid apps deploy shop --wait and isogrid apps deploy --wait shop are the
same command.
Regions, sizes, networks, vaults, groups and members are resolved the same way: by id, slug or the name shown in the console. When a name matches nothing or more than one thing, the error lists the valid choices.
Creating an application
isogrid apps create --name NAME --provider github|gitlab|registry --url REPO_URL|IMAGE_REF
[--cluster REGION] [--tier SIZE] [--network NETWORK] [--organization ORG]
[--branch main] [--dockerfile Dockerfile] [--context .] [--port N] [--replicas N] [--public]
[--env KEY=VALUE]... [--env-file PATH] [--secret KEY=VALUE]... [--secrets-file PATH]
[--vault-secret VAULT/KEY[:TARGET]]... [--slug SLUG] [--wait] [--follow] [--timeout 20m] [--json]
Only --name and --url are required. Everything else has a sensible default.
Where the code comes from
--provider is github, gitlab or registry. You can leave it out when
the URL says: a host of github.com means GitHub, and a host containing
gitlab means GitLab. A self-hosted GitLab on any other name, or a bare
owner/repo, needs it.
--url takes whatever you would paste. For GitHub, all of these name the
same repository:
acme/api
https://github.com/acme/api
https://github.com/acme/api.git
git@github.com:acme/api.git
For GitLab, including self-hosted instances and projects in nested groups:
https://gitlab.com/acme/api
https://gitlab.example.com/platform/backend/api
git@gitlab.example.com:platform/backend/api.git
A URL copied from a branch page builds that branch:
https://github.com/acme/api/tree/release/2.0 and
https://gitlab.com/acme/api/-/tree/staging set the branch unless --branch
says otherwise. Without either, the branch is main.
The repository must be one your organization has connected, and one you have
been given access to. isogrid repos list shows both; see
Git repositories.
From the registry, --url is an image reference:
api:1.2
acme/api:1.2
registry.example.com:5000/acme/api:1.2
The tag defaults to latest, and --tag overrides the one in the reference.
--image acme/api:1.2 is shorthand for --provider registry --url acme/api:1.2.
The image must already be pushed, into a namespace you can deploy from.
How it is built
For a repository:
| Flag | Default | |
|---|---|---|
--branch |
from the URL, else main |
The branch every build follows. |
--dockerfile |
Dockerfile |
Path to the Dockerfile, from the repository root. |
--context |
. |
The build context directory. |
Where it runs, and how big
--cluster is the region. You can leave it out when only one region is open
to you; otherwise the error lists the ones you can choose.
--tier is the instance type. It defaults to the region's smallest.
isogrid apps tiers <app> lists them once an application exists; until then
the console's create form shows the same list.
--network defaults to your organization's own network in that region.
Name another to put the application beside something it needs to reach
privately.
All three accept a slug, a name or an id.
--port is the port the container listens on. Without it, the port the
image declares is used, and 8080 if it declares none.
--replicas is how many copies to run, 1 by default. See
Deploying an application for why one copy
means a short gap during every deployment.
--public publishes the application on a public address. Without it, it
gets none; you can publish it later with apps update --public.
--slug sets the identifier used in addresses and commands. It is derived
from the name when you leave it out.
Configuration and secrets
There are three kinds of value, and which one you use decides who can read it afterwards.
Environment variables — --env KEY=VALUE (repeatable) and
--env-file PATH. Plain configuration, readable by anyone who can see the
application. When the same key is in both, the flag wins: the file holds the
defaults, the command line is the exception.
Container secrets — --secret KEY=VALUE and --secrets-file PATH. Written
to the secret store before the first deployment and never shown again, to
anyone, by any command or API.
Vault secrets — --vault-secret VAULT/KEY[:TARGET]. Maps a key that already
lives in one of your vaults into the application. payments/stripe-key:STRIPE_KEY
exposes the key stripe-key from the vault payments as STRIPE_KEY; without
:TARGET the name is derived from the key. Nothing is copied: rotating the
value in the vault is enough.
Files use the usual dotenv format:
# comments and blank lines are ignored
LOG_LEVEL=info
export REGION=eu # "export" is allowed, and so is an inline comment
GREETING='taken literally, $HOME included'
TLS_KEY="-----BEGIN PRIVATE KEY-----
spans several lines until the closing quote
-----END PRIVATE KEY-----"
There is no variable expansion: a $ in a value is a $. Variable names are
letters, digits and underscores, not starting with a digit, and names starting
with ISOGRID_ belong to the platform. Secret names may also contain . and
-, so tls.key is valid. A mistake is reported with the file name and line
number before anything is sent.
Waiting for the result
Without --wait, create returns as soon as the application exists and its
first build or deployment is queued. That is not the same as working.
--wait blocks until the build (if there is one) and the deployment have
finished, and exits non-zero if either failed. --follow does the same
while printing the build and deployment logs. --timeout bounds the wait;
it defaults to 20m.
A wait fails — exit code 1 — when:
- the build fails;
- the deployment fails;
- the new version never became healthy and was rolled back to the previous one, even though the application is still serving;
- the application is deployed but stays short of its copies for more than three minutes, which means they are crashing;
- the timeout runs out. The work carries on on the platform; only the waiting
stops. Check it with
isogrid apps status.
--json prints the application as the API returned it, on standard output, once
the command is done. Progress goes to standard error, so the two never mix.
Examples
A repository, with defaults for everything that has one:
isogrid apps create --name shop --url https://github.com/acme/shop --wait
A GitLab project on your own host, a named branch, a size and two copies:
isogrid apps create \
--name billing-api \
--provider gitlab \
--url https://git.acme.internal/platform/billing/api \
--branch release \
--cluster eu-west --tier medium --replicas 2 \
--env-file deploy/production.env \
--secrets-file deploy/production.secrets.env \
--vault-secret payments/stripe-key:STRIPE_KEY \
--public --wait
An image you have already pushed:
isogrid apps create --name worker --image acme/worker:2024.10.1 --cluster eu-west --wait
Changing an application
isogrid apps update <app> [--name N] [--branch B] [--dockerfile P] [--context D] [--url URL|IMAGE] [--tag T]
[--port N] [--replicas N] [--tier SIZE] [--cluster REGION] [--network NETWORK]
[--env KEY=VALUE]... [--unset-env KEY]... [--env-file PATH] [--replace-env]
[--secret KEY=VALUE]... [--secrets-file PATH] [--unset-secret KEY]... [--replace-secrets]
[--public | --private] [--deploy [--ref REF]] [--wait] [--follow] [--json]
Only what you give changes. A command that would change nothing says so and
exits 0, which makes update safe to run on every pipeline.
Environment variables: merged unless you say otherwise
| Flag | Effect |
|---|---|
--env KEY=VALUE, --env-file PATH |
Add or overwrite those keys. The others are kept. |
--unset-env KEY |
Remove one key. |
--replace-env |
Make the values given the whole environment. Everything else is removed. |
--replace-env with no values clears the environment. Use it deliberately.
isogrid apps env <app> prints the current environment as a dotenv file, so the
round trip is:
isogrid apps env shop > shop.env
# edit shop.env
isogrid apps update shop --env-file shop.env --replace-env
Secrets: the same, with the values never shown
| Flag | Effect |
|---|---|
--secret KEY=VALUE, --secrets-file PATH |
Add or overwrite those secrets. |
--unset-secret KEY |
Remove one. |
--replace-secrets |
Make the given set the only secrets. Needs at least one value. |
isogrid apps secrets <app> lists the names. No command returns a value.
When the change takes effect
A running application is redeployed by the platform when its environment
changes, or when it is resized (--tier, --replicas). You do not need to
ask.
Everything else takes effect on the next deployment. Pass --deploy to make
that now:
- For an application built from git,
--deploybuilds the branch at its latest commit and deploys the result.--refbuilds a specific branch, tag or commit SHA this one time; later builds still follow the application's branch. - For an image,
--deployredeploys the image and tag it is set to.
--wait, --follow and --timeout work as they do for create. With --wait
and nothing being deployed, the command says there is nothing to wait for.
Images and repositories
On an image application, --tag switches to another tag of the same image and
--url to another image in your registry. This is how a pipeline that builds
its own images deploys each one:
isogrid apps update worker --tag "$GIT_SHA" --deploy --wait
An application's repository cannot be changed. --url on an application
built from git may only restate the same repository, for example to pick up a
branch from a /tree/BRANCH URL. To build from a different repository, create a
new application.
Examples
Turn on debug logging; the platform redeploys it:
isogrid apps update shop --env LOG_LEVEL=debug --wait
Rotate a credential and remove an old one:
isogrid apps update shop --secret DATABASE_PASSWORD="$NEW_PASSWORD" --unset-secret LEGACY_TOKEN
Follow a new branch and build it now:
isogrid apps update shop --branch release/3.0 --deploy --wait
Build one exact commit, which is what a pipeline does:
isogrid apps update shop --deploy --ref 3f2c1ab --wait
Deploying a stack file
A docker-compose.yml or docker stack file can be deployed as it is. Each
service becomes one application, named <stack>-<service>:
isogrid stack preview SOURCE [--stack NAME] [--region REGION] [--var KEY=VALUE]... [--var-file PATH] [--json]
isogrid stack apply SOURCE [--stack NAME] [--region REGION] [--var KEY=VALUE]... [--var-file PATH]
[--size [FILE:]SERVICE=TIER]... [--registry [FILE:]SERVICE=NAME]... [--network [FILE:]SERVICE=NAME]...
[--placement [FILE:]SERVICE=CONSTRAINT]... [--port [FILE:]SERVICE=N]... [--skip [FILE:]SERVICE]...
[--no-deploy] [--wait] [--follow] [--timeout 20m] [--json]
SOURCE: -f FILE|DIR ... (repeatable; - reads standard input)
--repo REPO [--provider github|gitlab] [--ref REF] --path PATH ... (--path repeatable)
A short example, with a shared logging block and a rolling update:
x-logging: &logging
driver: json-file
options:
max-size: "20m"
max-file: "5"
services:
api:
image: registry.gitlab.com/fraus/api:${IMAGE_TAG}
env_file: [api.env]
ports: ["8000:8000"]
logging: *logging
deploy:
replicas: 2
update_config:
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
monitor: 30s
restart_policy:
condition: on-failure
placement:
constraints: ["node.labels.zone == a"]
worker:
image: registry.gitlab.com/fraus/worker:${IMAGE_TAG}
logging: *logging
Deploy it, and wait until every service is running:
isogrid stack apply -f stack.yml --stack fraus --var IMAGE_TAG=1.4.2 --wait
This creates fraus-api and fraus-worker. Applying the same file again with
the same --stack updates those applications and redeploys them, so the file
can live in your repository and be applied from a pipeline. The stack name is a
lowercase slug of at most 40 characters.
Run isogrid stack preview first. It changes nothing, and shows per service
whether its application would be created, updated or skipped, the image and
which registry pulls it, the size, the network, the placement that would be
applied beside the one the file asks for, the rollout and log settings, and
the warnings. -f - reads the file from standard input.
${VAR} in the file is filled from --var KEY=VALUE and --var-file
(a dotenv file); the flag wins. ${VAR:-default} works as in compose. A variable the file uses and nobody gave stops
apply before anything changes.
From a connected repository
The file does not have to be on your machine. --repo and --path name a file
in a GitHub or GitLab repository your organization has connected, and the
platform reads it with the connection's own access; nothing is cloned locally
and no token passes through the CLI:
isogrid stack apply --repo gitlab:global_fraus/deployment-config \
--path stacks/prod/login.yml --stack prod-login
--repo is owner/name (GitHub, the default) or group/project; write
gitlab:group/project, or add --provider gitlab, for GitLab. A repository URL
works too. --ref reads a branch, tag or commit instead of the default branch.
The preview says which ref it read, and apply reads that same ref, so a push in
between does not change what you reviewed. -f and --repo cannot be combined.
A whole folder
-f also takes a folder, and deploys every *.yml and *.yaml file directly
inside it, in name order (subfolders are not read). -f can be repeated, and so
can --path. Each file is its own stack:
isogrid stack apply -f stacks/prod --stack prod --var IMAGE_TAG=1.4.2 --wait
With several files, --stack is a prefix: stacks/prod/login.yml becomes the
stack prod-login, stacks/prod/billing.yml the stack prod-billing (the file
name is lowercased, anything other than letters and digits becomes -, and the
whole name is kept to 40 characters). Without --stack, the prefix is the name
of the folder the file sits in, which gives the same names here. With a single
file, --stack is the stack's name, as before.
The files are previewed first, then applied one after another, each under its
own header. A file that fails (a missing variable, a service that failed) does
not stop the others; the command prints how many were applied and exits 1 if
any failed. --wait waits for the applications of all the files, within the
one --timeout.
What is applied, and what is not
| From the file | |
|---|---|
image, deploy.replicas, environment |
Applied. |
command, entrypoint, user, healthcheck, stop_grace_period |
Applied to the container, and shown in the preview. healthcheck: {disable: true} turns the image's own check off. |
ports, expose |
The container port is where the application is routed; --port overrides it. Published ports are not opened on the nodes: the application stays private until you make it public. |
deploy.update_config, rollback_config, restart_policy |
Applied: parallelism, delay, order, failure action and monitor period, and the restart condition. |
logging with the json-file driver |
Applied as the application's log retention, with its max-size and max-file. Other drivers are ignored. |
deploy.resources.limits |
Shown in the preview. The size is an instance type: the region's smallest unless --size names another. |
deploy.placement.constraints |
Kept only when the constraint is one of the region's placement choices, listed at the end of the preview. Pick one with --placement. |
env_file |
Not read. The file stays on your machine; apply prints the command to set its variables afterwards, e.g. isogrid apps update fraus-api --env-file api.env. Put secrets in --secrets-file instead. |
volumes |
Not mounted. Use a managed database or object storage for data that must survive a redeploy. |
cap_add |
Not applied. Containers run with the default capabilities. |
A service with build: and no image is skipped: build it from its repository as its own application, or push the image and name it. Services are placed
on your organization's network in the region, where they reach each other by
their service name; --network puts one elsewhere.
Per-service choices
| Flag | |
|---|---|
--size SERVICE=TIER |
The instance type, by name or id. |
--registry SERVICE=NAME |
Which of your registries pulls the image, needed when several serve its host. |
--network SERVICE=NAME |
A network other than your organization's. |
--placement SERVICE=CONSTRAINT |
One of the region's placement choices, as written or by its label. Repeatable. |
--port SERVICE=N |
The port the container listens on. |
--skip SERVICE |
Leave the service out this time. |
A flag naming a service the file does not have, or a choice the region does not offer, fails before anything is sent, and lists what would have been valid.
With several files, --size web=Large applies to the web service of every file
that has one. Prefix it with a file name, with or without its extension, to
target one file only: --size login:web=Large, --skip billing.yml:worker.
--wait waits for every created or updated application, within one
--timeout for all of them, and exits 1 if any deployment failed or was
rolled back, just as apps deploy --wait does. apply also exits 1 when any
service failed to apply; the others are still applied, and the reason for each
failure is printed. --no-deploy saves the applications without deploying them.
Other container registries
Besides the platform's own registry, applications can deploy images from GitLab, GitHub, Docker Hub, Azure, Google, AWS, Quay or any registry that speaks the standard API. Store the credentials once per organization:
isogrid registries list
isogrid registries add --name NAME --kind KIND [--host HOST] --username USER --password-stdin [--no-verify]
isogrid registries update <registry> [--name NAME] [--username USER] [--password-stdin]
isogrid registries test <registry>
isogrid registries remove <registry> [--yes]
--kind is dockerhub, gitlab, github, azure, google, aws, quay
or generic. --host may be left out for the kinds with one well-known host.
The password or token is never a flag. It is read from standard input with
--password-stdin, or else from the ISOGRID_REGISTRY_PASSWORD environment
variable, so it does not end up in your shell history or a CI log. Use a token
with read access to the images rather than an account password:
# GitLab: a deploy token with read_registry
echo "$GITLAB_DEPLOY_TOKEN" | isogrid registries add --name gitlab --kind gitlab \
--username gitlab+deploy-token-1 --password-stdin
# A self-hosted GitLab
echo "$TOKEN" | isogrid registries add --name gitlab-acme --kind gitlab \
--host registry.git.acme.internal --username deployer --password-stdin
# GitHub Container Registry: a token with read:packages
echo "$GHCR_TOKEN" | isogrid registries add --name ghcr --kind github --username acme-bot --password-stdin
# Docker Hub: an access token
echo "$DOCKERHUB_TOKEN" | isogrid registries add --name hub --kind dockerhub --username acme --password-stdin
# Azure Container Registry
echo "$ACR_PASSWORD" | isogrid registries add --name acr --kind azure \
--host acme.azurecr.io --username acme-pull --password-stdin
The credentials are checked against the registry before they are saved, unless
you pass --no-verify. test checks them again and exits 1 when the registry
refuses them. A registry cannot be removed while applications deploy from it.
Then deploy an image from it by its full reference:
isogrid apps create --name web --external-image registry.gitlab.com/acme/web:1.4 --registry gitlab --wait
isogrid apps update web --external-image registry.gitlab.com/acme/web:1.5 --registry gitlab --deploy --wait
A public image needs no registry: --external-image nginx:1.27. --image and
--url remain for images in the platform's own registry. A stack file picks
the registry by the image's host on its own, and asks for --registry only when
more than one of yours serves that host.
Day-to-day commands
isogrid apps list # source, status, size and address of each
isogrid apps get <app> [--exact] # one application in detail
isogrid apps status <app> # copies running and wanted, and why they differ
isogrid apps build <app> [--ref REF] # build from git and deploy the result
isogrid apps deploy <app> # redeploy the current image, without building
isogrid apps stop <app> # scale to zero
isogrid apps start <app> # and back
isogrid apps scale <app> [--replicas N] [--tier SIZE]
isogrid apps tiers <app> # sizes its region offers, with prices
isogrid apps logs <app> [--follow] # the deployment log
isogrid apps logs <app> --search QUERY # search what the application printed
isogrid apps env <app> # environment variables, as a dotenv file
isogrid apps secrets <app> # secret names, never values
isogrid apps delete <app> [--yes]
build and deploy are different. build fetches new code, builds it and
deploys what it built. deploy (also redeploy) restarts the application from
the image it already has, and picks up nothing new from git. Both take --wait,
--follow and --timeout.
A failed deployment rolls back to the version that was running, so deploy
is safe to retry: the worst case is that the application carries on as it was.
Under --wait, the rollback still counts as a failure. That is the default
policy; on the application's page in the console you can instead have a failed
rollout stop where it is, or never roll back, and set how long a new copy must
stay up to count as healthy. The page's Roll back button returns to the
version before the last deploy.
status reads the cluster now, not the last thing the platform recorded.
When copies are missing, it says why, and lists recent copies with the node and
the error. It is the first command to run when an application is up but not
healthy.
get --exact only matches a full slug, name or id, and exits 1 when there
is no such application. Scripts use it as an existence test:
if isogrid apps get shop --exact --json > /dev/null 2>&1; then
echo "shop exists"
fi
delete asks first. Without a terminal to ask on, as in a pipeline, it
refuses with exit code 2 unless you pass --yes, so an unattended delete is
always one somebody wrote down.
Searching logs
isogrid apps logs <app> prints the deployment log: what the platform did to
roll the application out. To look through what the application itself printed,
search it:
isogrid apps logs <app> --search QUERY [--since 1h] [--limit 500] [--json]
Each match is printed as timestamp [replica] LEVEL message. A query is a list
of terms that must all hold:
| Term | Matches |
|---|---|
timeout |
lines containing the word |
"connection reset" |
the exact phrase |
-healthcheck |
lines without the word |
/5\d\d/ |
a regular expression |
level:error |
a level: error, warn, info or debug |
replica:2 |
lines from the second copy |
isogrid apps logs api --search 'level:error -healthcheck' --since 1h
isogrid apps logs api --search '"payment failed" replica:2' --limit 50
--since takes a duration (30m, 1h) or an RFC 3339 time. --limit is the
most lines returned, up to 2000; when more matched, the most recent are kept
and the command says so. The matches go to standard output and the notes to
standard error, so > errors.txt gets only lines.
Without log retention, only what the running copies still hold can be
searched, and a redeploy starts that over. The command says when retention is
off. Turn it on in the application's settings in the console, or with a
json-file logging block in a stack file.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success, including an update that had nothing to change. |
1 |
Something failed: an API error, a failed build, a failed or rolled-back deployment under --wait, a timeout, or no match for apps get --exact. |
2 |
The command was used wrongly: an unknown flag, a missing argument, contradictory flags, or delete without --yes and without a terminal. |
Edge cases worth knowing before you meet them
"is not a repository you have been given access to". The repository is connected, but through somebody else's account, and they have not opened it to you. See Git repositories.
The build succeeded and --wait still failed. The new version was built
and then did not become healthy, so the previous one was put back. The site is
up; the new code is not. Read isogrid apps logs <app>.
--tag says it applies to image applications. The application is built
from git, and its images are tagged by its builds. Use --deploy --ref instead.
update --env-file removed nothing you deleted from the file. Merging is
the default. Add --replace-env when the file is meant to be the whole
environment.
A value containing $ arrived unchanged. That is intended. Nothing is
expanded, because a password that silently changed would be much worse.