Skip to content
On this page

Config as code

A project can live in a file. anyport.yaml names the apps, managed services, secrets (by name and key), scheduled tasks, variables and domains a project is made of, and anyport apply makes a project match it. The same file serves development, staging and production: where it runs is an argument to apply, not a line in the file.

apiVersion: anyport.dev/v1alpha1
kind: Project
name: demo
environment: staging
variables:
  REGION: eu
  CLERK_SECRET_KEY: { secret: true, description: "Clerk backend key" }
  SESSION_SECRET: { secret: true, generate: true }
secrets:
  - name: creds
    keys: [API_KEY]
  - name: session
    generate: [APP_SECRET]
services:
  - name: db
    type: postgres
    config:
      version: "17"
apps:
  - name: api
    image: ghcr.io/acme/api:1.4.0
    replicas: 2
    ports:
      - { name: http, port: 8080 }
    env:
      - { name: REGION, value: "${{ REGION }}" }
      - { name: CLERK_SECRET_KEY, value: "${{ CLERK_SECRET_KEY }}" }
      - { name: DATABASE_URL, fromService: { service: db, key: DATABASE_URL } }
      - { name: API_KEY, fromSecret: { secret: creds, key: API_KEY } }
    probes:
      readiness: { path: /healthz, port: 8080 }
    domains:
      - host: api.acme.dev
    publicPorts:
      - { port: 20017, target: 50051 }
  - name: web
    source:
      repo: acme/web
      branch: main
    ports: [{ port: 3000 }]
tasks:
  - name: nightly
    app: api
    command: ["python", "manage.py", "cleanup"]
    schedule: "0 3 * * *"

The full format is published as a JSON Schema at /schema/anyport.json. Point your editor at it with a # yaml-language-server: $schema=https://anyport.dev/schema/anyport.json line at the top of the file and it completes and checks every key. Unknown keys are refused, not ignored: a misspelled setting that silently did nothing is the failure this exists to prevent.

A variable is a plain string, or an object when it is secret: { secret: true } declares the name and nothing else. The value is set with anyport project variable set NAME=… --secret or in the console, and never written to the file; a file that carries one is refused before anything is read. generate: true is for a value that only needs to exist, a session key or a signing secret: apply mints it the first time and never overwrites it. A plain variable is written in clear, so the export warns when a plain one is named like a credential (STRIPE_KEY, DB_PASSWORD), and anyport project export --strict fails instead, for a job that commits the file.

publicPorts opens raw TCP or UDP ports beside the hostnames (see Domains). via is left out on purpose: the console picks what the cluster serves for free, and on a managed cloud where a load balancer costs money it refuses to guess until you write via: loadBalancer or via: nodePort.

Scripts#

The file can also carry scripts: commands a developer runs on their own machine with an app's environment, as anyport run <name>, the way package.json holds npm scripts. They are not part of the project, so plan, apply and the sync from your repository ignore them, the console's copy of the file leaves them out, and anyport project export -o anyport.yaml keeps the section of the file it overwrites. See Save commands as scripts for the format and which value wins when a script overrides the app's.

Start from a project you have#

anyport project export demo -o anyport.yaml

writes a running project as a file. That is the fastest way to adopt this, and it is also how the format is kept honest: everything the console can build has to come back out of an export, and a test in the codebase fails when a setting exists that the file cannot say.

See what would change#

anyport plan -f anyport.yaml
Project demo on prod (update)

ACTION     RESOURCE                   DETAIL
blocked    secret/creds               secret "creds" lacks API_KEY; set it first: anyport secret set creds API_KEY=… -p demo
blocked    variable/CLERK_SECRET_KEY  variable "CLERK_SECRET_KEY" has no value; set it first: anyport project variable set CLERK_SECRET_KEY=… -p demo
create     variable/SESSION_SECRET    secret, generated on first apply
update     app/api                    env, replicas
create     task/nightly
delete     variable/OLD_TOKEN         secret; the value cannot be recovered
unmanaged  app/legacy                 in the project but not in the file; left alone (pass prune to remove)
noop       service/db

2 to create, 1 to update, 1 to remove, 1 unchanged, 1 in the project but not in the file.

Has to happen first:
  - secret "creds" lacks API_KEY; set it first: anyport secret set creds API_KEY=… -p demo
  - variable "CLERK_SECRET_KEY" has no value; set it first: anyport project variable set CLERK_SECRET_KEY=… -p demo

Nothing is changed by plan. The rows are what apply would do, in the file's own vocabulary.

Apply#

anyport apply -f anyport.yaml
anyport apply -f anyport.yaml --cluster staging   # a new project lands here

apply shows the plan, asks, and then works in dependency order: the project, then its managed services, then apps, tasks and domains. Three rules decide what it will and will not do:

  • Secret values never enter the file, so apply never writes a secret. A file declares which secrets, keys and secret variables the project needs; apply checks each has a value and stops if one does not, naming the anyport secret set or anyport project variable set command that fixes it. Everything before that point is applied, and running apply again finishes the job. The one exception is generate: a signing key or a session secret needs a value, not a particular one, so apply mints a random one the first time and never overwrites it.
  • Apply never deletes by omission. A resource in the project that the file does not mention is reported as unmanaged and left alone. --prune removes unmanaged apps, tasks and domains. Managed services are never removed by apply, prune or not: an uninstall destroys data and has its own confirmation in the console and in anyport service uninstall.
  • What the file owns, it owns completely. A key removed from an app in the file is removed from the app, and a variable removed from variables is removed from the project — a secret one too, and the plan says so, because its value cannot be recovered. Settings the file does not carry (whether a project is paused, for one) are not touched, and a file with no variables section at all says nothing about them.

In the console#

Project → Settings → Config as code shows the file for a project, to copy or download, and takes a pasted file: the same plan, then apply. Projects → New project → Create from file starts a project from one, with the cluster chosen in the dialog.

Kept in sync from the repository#

Once an anyport.yaml is on the branch an app deploys from, the console applies it on every push that changes it, before the build that push triggers. So a pull request that changes the file and the code together deploys both, in that order, and the repository is the place the project is described. The rules are the ones above: nothing is deleted by omission (a push is never a prune), and a missing secret stops the apply where it is, with the reason in the repository's deployment log next to the build.

The file has to name the project the app deploys into; a file that names another project is left alone, so two apps in one repository deploying into two projects cannot rewrite each other's. In a monorepo the file is looked for in the app's build directory first, then at the root.

A pull request's preview gets the pull request's own file: applied into the preview when it is created, and on every push after. Hostnames and public ports in the file stay with the base project, since a preview is reached on generated hostnames of its own, and the preview's name and variables are the preview engine's.

What goes in the file, and what does not#

In the fileNot in the file
Apps: image or source, ports, env, resources, autoscaling, probes, volumes, sidecars, pull secret, which projects may reach itThe cluster: pass --cluster, or let a new project land on the org default
Managed services with their configurationSecret values: set with anyport secret set or anyport project variable set, or in the console
Secrets by name and the keys the project reads, and the keys apply may generateWhether a project or app is paused
Tasks: schedule, command, the app whose image and env they borrowPreview settings, which live on the base project in the console
Variables and their use as ${{ NAME }}; a secret variable by name, with generate when apply may mint itAnything a build produced: a source-built app is described by its repository, never by the image its last build pushed
Domains, on the app that serves themHow a database was created: a restore's source and point in time

A database restored from a backup records where it came from (restoreFrom, restoreServerName, restoreTargetTime, restoreBasePath). Those markers are managed by the console's restore flow and are not part of the manifest: an export leaves them out, and a file that still carries them (an older export, or written by hand) has them ignored, and the plan row names them. Otherwise applying the file to a new project would create a database that recovers from the old one's backups instead of an empty one. To bring data across, restore from a backup in the console.

A source-built app carries source instead of image. Applying it creates the app and its build from the repository the same way "deploy from GitHub" does, and a push to the branch keeps it current from then on.

Next steps#

  • CLI — Export, plan and apply from a terminal or a CI job.
  • Deploy from GitHub — The build a source app gets, and the push that keeps it current.
  • Templates & duplication — Copy a live project from the console instead of a file.