Skip to content
Sirio is in private beta: sources and installer are sent to testers by email. Ask to join.

Pipeline

A pipeline takes a commit from a Git repository and gets it running on this machine. It has two phases, both optional: build and release. Steps run in order; the first one that fails stops everything, except steps marked “continue even if it fails”.

In Repositories → Connect repository:

  • URL: https://github.com/acme/api.git or git@github.com:acme/api.git.
  • Access:
    • public: no credential;
    • HTTPS token: on GitHub create a fine-grained token with “Contents: read” permission and use x-access-token as the username; on GitLab use a deploy token;
    • SSH key: paste the private key of a read-only deploy key.

The credential is encrypted before being saved and can no longer be read from the interface. When editing, leaving the field empty keeps the existing one.

Every repository has a persistent checkout at <SIRIO_DATA_DIR>/workspaces/<name> (by default /var/lib/sirio/workspaces/<name>), reused across runs: the first time it’s a git clone, later ones are just fetch + checkout on the requested commit. It’s never recreated from scratch unless deleted by hand.

<name> comes from the repository’s name when it’s connected: lowercased, with anything other than letters, digits and _ turned into - (My App → my-app), and a -2, -3, … suffix if another repository already maps to the same directory. The Repositories page shows it under each name. It’s chosen once and doesn’t follow later renames, because it’s also what Compose uses as the project name when a compose step’s file sits at the root of the checkout — and so the prefix of the containers it creates (my-app-web-1). Moving it would make the next deploy start a second copy of the stack next to the old one. For the same reason, repositories connected before this naming existed keep their repo-<id> directory.

The Detect from compose button in the pipeline editor, on the other hand, doesn’t touch this checkout: it does a shallow, ephemeral clone in a temporary subfolder of the same workspaces/, just to look for compose.yaml/compose.yml/docker-compose.yaml/docker-compose.yml in the root, and deletes it right after — it doesn’t run or read anything else from the repository.

Run with sh -c in the checkout folder. Available variables:

Variable Content
SIRIO_COMMIT full commit sha
SIRIO_COMMIT_SHORT first 7 characters
SIRIO_REF full ref, e.g. refs/heads/main
SIRIO_BRANCH branch name
SIRIO_RUN_ID run number
SIRIO_PIPELINE, SIRIO_REPOSITORY configured names
SIRIO_WORKSPACE checkout path
DOCKER_HOST configured Docker socket
Terminal window
npm ci
npm run build

Runs docker build. The placeholders {{commit}}, {{commit_short}}, {{branch}}, {{run}}, {{pipeline}}, {{repository}} are expanded in the tag:

acme/api:{{commit_short}}

The Dockerfile and context are relative to the pipeline’s folder.

Runs docker compose with a subcommand among up, down, pull, build, restart, stop. With up Sirio adds --detach --remove-orphans, with down --remove-orphans.

Additional arguments (e.g. --pull always --wait) are appended after those, split on spaces and passed to docker compose as separate arguments, without a shell: quotes aren’t interpreted, so a single argument can’t contain a space. The editor shows the full command as it will run.

Restarts a container by name via the Engine API, without going through the CLI. The name accepts the same placeholders as the image tag.

Runs one of the site’s backup plans and waits for the snapshot: placed before the deploy steps, it saves the site as it was before the deploy changes it. It needs the pipeline linked to a site and a plan of that site, and fails the run if the backup fails. See docs/backups.md. An exported pipeline keeps the plan’s ID, which means nothing on another instance: pick the plan again after importing it there.

Set the trigger to push or tag and give a branch or pattern (main, release/*, v*). On creation Sirio generates the secret and shows it once only.

On the Git provider, configure:

  • URL: the one shown in the pipeline’s Webhook tab;
  • Content type: application/json;
  • Secret: the generated one;
  • Events: push only.

If you’ve lost the secret, generate a new one from the same tab and update it on the provider.

Calls with a missing or wrong signature get a 401 and end up in the activity log. Valid calls on a ref that doesn’t match the trigger get a 200 with an “ignored” outcome.

Runs are serial: one at a time, queued. On a single machine, two simultaneous deploys do more harm than good.

Every run shows the log in real time (WebSocket) and keeps it on disk. Lines are tagged by origin: system (Sirio), stdout, stderr. The last 50 runs per pipeline are kept, then older logs and records are removed.

Repeat this commit on a previous run re-runs the pipeline on the same sha, not on the branch’s latest commit. It’s the simplest way to roll back: the same build-and-release sequence, on the code that used to work.

For it to be a real rollback, steps must not depend on latest: use {{commit_short}} in image tags, so every version stays distinct and reproducible.

A deploy recreates containers from the image and the compose file, so a variable changed by hand on a running container is lost at the next push. To keep one, set it from the panel instead: Environment overrides (gear button in the container list, or in the container’s page), one KEY=value per line. An override either adds a variable or replaces the value the image or the compose file sets — useful for a secret you don’t want in the repository, or for a debug flag on one host only.

Saving applies the change immediately by recreating the container (a brief interruption). The rest of its configuration is kept as it is: mounts and volumes, including the anonymous ones an image declares with VOLUME, networks and their aliases, so the other services of a compose project still reach it by name, restart policy, labels. Saving an unchanged list restarts nothing.

Overrides are stored in Sirio’s database by container name, not by ID, and reapplied after every successful run: a pipeline that recreates the container gets them back right away, before the proxy is realigned. Containers that already carry them aren’t touched. In the container’s Environment tab, the variables set this way have an override badge.

Removing an override behaves differently depending on who owns the container:

  • managed by Compose: Sirio first has Compose recreate it from the compose file and .env, then applies the remaining overrides, so the original value comes back. Compose runs as Sirio’s own user, so it has to be able to read every file of the project: an env_file created by root with mode 0600 stops it, and the error names the file (chgrp sirio FILE && chmod g+r FILE fixes it — the sirio user already controls Docker, so this gives it nothing it couldn’t reach);
  • any other container: there is no source to restore the previous value from, so the last one applied stays in place. Set it explicitly if you need it back.

An override can’t remove a variable the image or compose file defines, only change its value. Names follow the usual rule: letters, digits and _, not starting with a digit. Values are stored in plaintext, like Docker itself stores a container’s environment; editing them requires the edit permission on containers.

Export on the Pipelines page downloads every pipeline you can see as one JSON file; the download icon on a row exports just that one. Import reads such a file back, on this instance or another one, and shows each pipeline before creating anything:

  • the repository is proposed by matching the original Git URL (then the name), and has to be chosen when nothing matches: IDs mean nothing across instances, so the file only describes it;
  • the site is proposed by name, falling back to the chosen repository’s site;
  • the name can be changed, since it must be unique within a repository.

The import is all or nothing: if one pipeline is refused (invalid steps, a name already taken, a site you can’t create pipelines in), none is created and the row at fault is pointed out.

Webhook secrets are never exported. A pipeline with a push or tag trigger gets a new secret on import, shown once at the end together with its new URL: update both on the Git provider. Everything else travels as it is, step environment variables and build args included, so treat the file like the configuration it is.

{
"format": "sirio-pipelines",
"version": 1,
"exported_at": "2026-10-15T09:30:00Z",
"pipelines": [
{
"name": "deploy",
"enabled": true,
"trigger_type": "push",
"trigger_ref": "main",
"workdir": "",
"build_steps": [{ "name": "test", "type": "shell", "command": "go test ./..." }],
"deploy_steps": [{ "name": "up", "type": "compose", "compose_command": "up" }],
"repository": { "name": "api", "git_url": "https://github.com/acme/api.git" },
"site": { "name": "acme" }
}
]
}

The same endpoints work from a script, with a session: GET /api/pipelines/export (optionally ?ids=1,2) and POST /api/pipelines/import with the document above, each entry given a repository_id and optionally a site_id.

Repository acme/api, deploy with compose on every push to main.

Build

  1. test — shell: go test ./...
  2. image — docker build, tag acme/api:{{commit_short}}, Dockerfile Dockerfile

Release

  1. tag current — shell: docker tag acme/api:$SIRIO_COMMIT_SHORT acme/api:current
  2. start — compose up, file docker-compose.yml

With docker-compose.yml pointing at acme/api:current, a rollback amounts to repeating a previous run: it re-tags that commit’s image and restarts the stack.