Skip to content

Services and Compose

Every app is a service of its project. Services find each other on the project's private network, share variables, deploy together, keep data on a Drive, and can be imported from a Compose file.

업데이트

이 페이지는 아직 한국어로 번역되지 않아 영어로 보여 드립니다.

Every app is a service of its project. Beside what an app has, a service can:

  • wait for others when they deploy together: depends_on, each started, ready or completed (--depends-on db:ready);
  • be found on the project’s network by other names (network.aliases, --alias), which answer with the service’s address, or with each instance’s (network.endpoint_mode: vip or dnsrr);
  • stay off the internet (public: false).

The private network

Each project has its own network, inside 10.64.0.0/10, given out in ranges of 64 addresses as the project needs them. Each service gets an address that does not change, even when it is renamed, and each sandbox on the network gets one too. An address is the same on every machine: the network reaches the project’s sandboxes and instances wherever they run, over the platform’s own network between its machines. A sandbox or service of the project reaches a service by <service>.internal, or by its address. A sleeping instance on the network is woken by a connection to it. Platform variables tell a service its own: SBX_SERVICE, SBX_PRIVATE_DOMAIN, SBX_PRIVATE_ADDRESS, SBX_PROJECT.

New connections to a service’s address are shared out among its instances by the machines they run on. Projects cannot reach each other’s networks.

Shared variables and references

A shared variable is a name and a value that the project’s services can use (glac variables put REGION eu, /v1/variables): up to 1,000 a project, each value up to 32 KiB. Unlike a secret, its value can be read back.

An environment variable of a service can refer to a shared variable, ${{ shared.REGION }}, or to a variable of another service, ${{ db.POSTGRES_USER }}. The reference is replaced by the value when a version is made: the version keeps that value, the app keeps the reference. A reference to another service’s secret must be the whole value ("${{ db.POSTGRES_PASSWORD }}"): the secret is passed by name and never written into the version.

Changing a shared variable deploys again the services whose versions would change (a project’s deploy, cause variables): PUT /v1/variables/{name} answers with its id (deploy), or null where no version changes. Where that deploy cannot be made (its services would wait for each other in a ring, say), the change is refused with the deploy’s own code, and the value is not written: the message says the variable was not changed, names the services that refer to it, and says why their deploy cannot be made, at its place among them (services.api.env.DATABASE_URL).

Deploying a project

glac deploys create (POST /v1/deploys) deploys several services of the project together. Each service named gets what a deploy of it alone would give it, in place of what it has (api=IMAGE: a new image; web alone: as it is), and the services whose environment refers to them are deployed with them:

glac deploys create api=ghcr.io/acme/api:2 web
glac deploys ls

-f FILE gives the services as JSON, each with the fields of a version ({"api": {"image": "…", "env": {…}}}). The deploy takes them in the order of what each waits for (its depends_on, and the services its environment refers to):

  1. Stateful services first: the one instance of each is replaced, as below.
  2. Jobs (kind: job) next: each runs once, to its end, in a sandbox of the project on its private network. Exit code 0 is completed; any other fails the deploy (job_failed, with the job’s last lines). A job that has not ended 30 minutes after its sandbox was made fails it (job_timed_out), and its sandbox is deleted; one whose sandbox is gone before its end is seen fails it too (job_lost). A job is never run twice in one deploy.
  3. The rest: their versions are built side by side, each once what it waits for is done, and take their traffic together.

A deploy that fails stops where it failed: what was switched before stays switched, every other service keeps its version, and the deploy says which service failed and why (the event project.deploy_failed). A project runs one deploy at a time: one that comes meanwhile waits (queued), and a later one takes the place of one that waits (replaced), deploying its services with its own. If the control plane restarts, a deploy goes on where it was: a job that has ended is not run again, and its 30 minutes still count from when it began.

glac deploys create waits for the deploy, says on stderr how each service goes, and exits 1 when it fails; --no-wait returns at once, and glac deploys get ID --wait waits later. A deploy (GET /v1/deploys/{id}) has its status (queued, deploying, deployed, failed or replaced), its cause (api; variables, a shared variable changed; compose, an import), each service’s status, version and what it waits for, and how long each step took (steps). The SDKs have the same calls (glac.deploys in Python and TypeScript), and the MCP server has project_deploy and project_deploys.

A service that keeps data

A service with a Drive of its own is stateful. It has exactly one instance, kept until you delete it, on the machine that holds the Drive, at the service’s address:

glac drives create --name pgdata --size-bytes 5GiB --region eu
openssl rand -hex 16 | glac secrets put PG_PASSWORD -
glac deploy postgres:16 --name db --port 5432 --region eu --secret POSTGRES_PASSWORD=PG_PASSWORD \
  -e PGDATA=/var/lib/postgresql/data/pgdata --drive pgdata:/var/lib/postgresql/data --public false

(PGDATA puts the database in a directory of the Drive: the top of a fresh file system holds lost+found, which PostgreSQL will not start in.)

  • One writer: such a service has one instance. --max-instances other than 1, or keys, are refused (409 drive_single_writer).
  • One region: a Drive is mounted in its own region only, and the instance is made in the app’s first region. So give the service the Drive’s region, as above: a version whose first region is another fails (409 not_on_one_machine).
  • Read-write only: a version cannot mount a snapshot of a Drive read-only yet (mode: "snapshot" is refused, 400 invalid_field). Read-only mounts are for sandboxes.
  • A new version first takes a backup of the service’s Drive (reason: before_version), then stops the old instance and starts the new one with the same Drive, address and name. Connections that arrive meanwhile wait, up to 10 seconds, instead of being refused. If the new version does not come up, the previous one is started again on the same Drive.
  • When it counts as ready. By the image’s own HEALTHCHECK; without one, for a database image the platform knows, by that database’s own command (pg_isready for PostgreSQL’s, mysqladmin ping for MySQL’s and MariaDB’s, redis-cli ping for Redis’s, Valkey’s and KeyDB’s, a mongosh ping for MongoDB’s); otherwise when its port takes a connection. Each waits first for the server to listen on an address other than loopback, so the temporary server a database runs while it sets itself up does not count. So the first query does not meet a database still starting (PostgreSQL’s 57P03).
  • Started again from its Drive (a restart, its machine back), it holds the connections that come for it until it is ready, up to 150 seconds, instead of refusing them; one whose check has not passed after 120 seconds is let through all the same, and its log says so.
  • If its server stops by itself (it crashes, or the kernel’s OOM killer or a kill ends it), it is started again in the same instance, with its Drive as it is, once nothing of the run before is left: 0.1 seconds later, and each time it stops again within 10 seconds of starting, after twice the wait before, up to a minute (Docker’s restart: always, the same numbers). Each start is an event instance.restarted, with how the run before ended (exit_status, signal), and the instance’s start_command (GET /v1/sandboxes/{id}) says how its server runs (running, or restarting and when), how often it was started again and how it last ended. A stop the platform asks for (a delete, a restart, a new version) is none of these. An ordinary (stateless) service’s instance whose server stops is replaced by another instead.
  • It sleeps by what happens in it, as a sandbox does, but its own timers (a database’s checkpoints) do not keep it awake. A connection to it wakes it.
  • It is not in the app’s log: read what it prints with glac sandboxes logs <instance> exe-start.

A database is such a service and its Drive, made and changed by /v1/databases alone: its app says so in its managed_by ({"object": "database", "id": "db-…"}), and an update, a deploy, a rollback or a delete of it here is refused (409 managed_by_database). Its log and its metrics are read as any app’s.

Importing a Compose file

glac compose up          # compose.yaml in this directory, or the one above

Each service of the file becomes an app of the project, with the same name. Every item of the file is reported as done, adapted (done another way, and why), ignored or refused (with how to change it); one refusal, and nothing is written. Secrets of the file are put into the project first; volumes are made. The services are then deployed together, as a project’s deploy (cause compose), which glac compose up waits for. Importing the same file again changes nothing.

  • The file is found as Docker Compose finds it: -f (-f - reads it from standard input), else COMPOSE_FILE from the environment, else from the .env file (--env-file, or .env in --project-directory or here; COMPOSE_PATH_SEPARATOR splits it), else a compose.yaml (or docker-compose.yml, …) here or in a directory above.
  • --dry-run only prints the report; --strict refuses what would be ignored.
  • Runtime settings. ulimits, sysctls, tmpfs, dns and shm_size become the service’s runtime settings, applied in each of its instances; dns is reported adapted, since on the project’s network the platform answers its questions in the servers’ place.
  • What is ignored is said. Settings the platform does not apply (cap_add, security_opt, logging, pull_policy, …) are reported as ignored, each with why, and glac compose up lists them all again on its last line. network_mode: none is adapted: the service’s egress rules refuse all traffic out, and it stays on the project’s network.
  • Where this machine has no Docker Compose, glac reads the files itself, as Docker Compose v5.6.1 reads them (--normalise local asks for that reading).
  • Volumes the image declares. A path the image declares as a volume (VOLUME, as WordPress’s /var/www/html or Redis’s /data) where the service mounts no volume or Drive is a directory of each instance’s own disk, which a new version does not keep. The version’s resolved.unmounted_volumes lists such paths, and glac compose up and glac deploy warn of each.
  • A service the project has and the file no longer has is kept, and named in the report; --remove-orphans deletes the apps an earlier import of the same Compose project made for such services, once the deploy is done.
  • glac compose ps lists the project’s services and how each stands, glac compose logs prints their logs together, each line with its service (-f follows), and glac compose restart deploys each again as it is, so its instances start anew (a stateful service’s on its Drive, after a backup of it).
  • --build builds the services that have build: with this machine’s Docker Compose and pushes each to the registry its image: names, then imports: the version made of it holds the image by its digest. A service whose image: names the same tag as before is unchanged and gets no new version: give each build a tag of its own.
  • glac compose down deletes the apps the import made. Volumes and secrets stay.
  • A file that cannot be imported this way (a build with no image, host paths, network_mode: service:…) can run whole in one sandbox of the project with Docker: glac compose up --one-sandbox. Its services then wake, sleep and are billed together, as one sandbox.
탐색 메뉴

검색어를 입력하세요…

↑↓ 이동↵ 선택Esc 닫기