CI/CD
CI/CD connects a git repository to your cluster: every time you push, Cosmos builds your code on one of your nodes, stores the result in your own registry, and deploys it. Push to main, and a minute later the new version is live. This is the workflow of Vercel, Netlify or Railway, on your own servers.
It can build and deploy three kinds of projects:
- Containers: an application built into a Docker image and run as a deployment, with or without a Dockerfile.
- Static sites: a website served from a static site registry.
- Functions: Cloud Functions published and deployed from your repository.
Before you start
- CI/CD lives in the Constellation section, so Constellation must be enabled. A single server works.
- Create the registry the builds will go to: a Docker registry for containers, a Static site registry for websites, an npm or PyPI registry for packages and functions. You can also create it from the wizard.
- Your repository must be reachable over
https, on GitHub, GitLab, Gitea / Forgejo, Bitbucket or any other git server. Cosmos clones with the access token, sosshkeys are not needed: ansshaddress pasted in the wizard is converted to itshttpsform. - For pushes to start builds, your git provider must be able to reach your Cosmos server from the internet. Otherwise, you can still start builds by hand.
Git connections
A Git connection is an access token Cosmos keeps for a provider, or for one self-hosted instance, and that every project on that provider shares. Create it once in the Git connections tab of Constellation > CI / Builds, and connect repositories without pasting a token again. Rotate the token on the connection, and every project follows from its next build.
- Provider: GitHub, GitLab, Gitea / Forgejo, Bitbucket, or Git for any other server. For GitHub Enterprise or your own GitLab, tick Self-hosted instance and give the Git host (
git.example.com, a pasted address is reduced to it); Gitea / Forgejo and Git servers always ask for it. The wizard offers the connection for repositories on that host, and lists the repositories the token can reach on it. - Access token: a token created on your git provider, that lets Cosmos list and read the repositories, install the webhooks and report the build results on your commits:
- GitHub: a fine-grained token with Contents: read, Webhooks: write and Commit statuses: write (plus Pull requests: read if you build pull requests from collaborators). Give it access to the repositories you want to build: these are the ones the wizard lists.
- GitLab: a personal or group access token with the
apiscope. - Gitea / Forgejo: an access token with repository read and write.
- Bitbucket: an app password with Repositories: read and Webhooks: read and write, and your Username.
- Git: the password or token to clone with, and the Username, for any other git server.
- Connection name: lower-case letters, digits and dashes. It cannot be changed later.
For a self-hosted instance whose API is not at the address of the repositories (behind a path prefix, or on another subdomain), set the connection's apiUrl through the API or Terraform (for example https://git.example.com/api/v4); the address of the repositories is right for everyone else.
A connection in use cannot be deleted: move its projects to another connection first. The token is never shown again once saved.
Connecting a repository
Go to Constellation > CI / Builds and click Connect a repository. A short wizard takes you through it.
1. Repository
- Credentials: the Git connection to use. The wizard picks the one matching the repository's host, or the only one you have. Choose A token for this project only to paste an access token instead, with the same permissions as above, for a repository that should keep its own credentials.
- Repository URL: with a connection, pick the repository in the list of what the token can reach, or type its address. With a token, type the address; the Provider is recognised from it. Both the
httpsand thesshforms of the address work. - Project name: suggested from the repository. Lower-case letters, digits and dashes. It cannot be changed later.
In Advanced: the Root directory to build when the repository holds several projects, the Default branch, a Branch filter to only build some branches (main, release/*), and Build node tags to choose which nodes run the builds.
2. Build
Cosmos clones the repository and tells you how it is going to build it. It looks, in this order, for:
- a
cosmos.jsonfile, where you describe the build and the deployment yourself (see cosmos.json); - a
.woodpecker.ymlpipeline, which is run as it is; - a
Dockerfile, which is built into an image; - a known language. Cosmos recognises Node, Python, Go, PHP, Ruby, Java, Rust, Deno, .NET, Elixir and more, and builds an image by itself, no Dockerfile needed.
The result is the first entry of Build strategy, for example Auto-detected: Dockerfile or Auto-detected: Railpack (node), and most of the time there is nothing to change. The other entries force what the build produces:
- Dockerfile or Railpack: a container image, from the
Dockerfilewhose path is filled in for you, or from the sources. - Static site: a website, from the Folder to publish (
dist,public...). - npm package or PyPI package: the package of the repository, published to a package registry of your cluster instead of built into an image. See Publishing a package.
- Woodpecker pipeline: the repository's own
.woodpecker.yml, run as it is. - No build: only the
ci.stepsofcosmos.jsonrun.
In Advanced, the Build-time environment variables. For anything sensitive, use secrets instead. The image platform, the build timeout (60 minutes) and the artifact quota are set from cosmos.json when the defaults do not fit.
3. Registry & deploy
The step asks for the registry that matches what the build produces, and picks it for you when you have only one of that kind. Add a registry at the bottom of the list creates one of the right kind without leaving the wizard.
- Docker registry and Image name for images. Cosmos creates its own token on the registry, and the nodes use it to pull the images: nothing to configure.
- Static-site registry for static sites.
- Package registry for npm and PyPI packages. A package build has nothing to deploy, so the deploy settings below do not apply.
- Deploy the artifact: on by default. Turn it off to only build. An image is deployed as a container deployment, a site as a static site.
- Deployment name (or Site name): the deployment is created on the first build, and updated with the new image on every build after.
- Replicas, Container port and Hostname: with a hostname and a port, Cosmos creates the URL of the application, load balanced between the replicas and protected by the SmartShield.
- Runtime environment of the created deployment: the environment variables of the application.
These settings are applied again on every build: change the replicas or the hostname here and the next build carries them over. Everything else you add to the deployment from the Deployments page (volumes, extra services, more environment variables) is kept. A deployment you switched to autoscaling keeps its autoscaling, and a cosmos.json in the repository takes over for what it declares (see below).
- Single-page application, for static sites: unknown addresses fall back to
index.html.
The Environments, Pull-request previews and Pull requests settings are explained below. Their defaults are safe: the default branch is deployed, and pull requests are built without any access to your secrets.
4. Done
Cosmos installs the webhook on your git provider, and from now on every push starts a build. If it could not (plain git server, token without the webhook permission), the last screen gives you the Webhook URL and the Webhook secret to add by hand on the repository, for push and pull-request events, in JSON. You can find them again, retry with Register on the provider, or Rotate the secret, from the Overview tab of the project.
Push a commit, or click Run a build on the project page, and watch your first build.
Following a build
The Builds tab, on the main page for all projects or inside a project, lists the builds with their status, the branch and commit that triggered them, what they produced and where it was deployed. Click a build to open it.
The build page shows the Steps on the left (prepare, then checkout, your own steps, Build & push or Publish, then Deploy) and the log of the selected step on the right, live while it runs. The first step, prepare, sums up what Cosmos decided: the strategy, the image name, the deployment target. A repository that declares several builds shows one block of steps per job, each with the node it ran on. The Result card gives the image that was built (with a link to the registry), the files each job kept, and the deployment that was created or updated, with its URL.
A build is Queued, Running, then Passed or Failed. It can also be Canceled, Skipped when the rules of your pipeline exclude the commit, or Awaiting approval (see Pull requests).
The buttons, on the build page and in the list:
- Cancel build: stops a queued or running build. Pushing a new commit on a branch also cancels the builds of that branch that are still waiting.
- Run again: builds the same commit again. On a failed build with several jobs, the jobs that passed are kept as they were and only the others run again.
- Deploy this build: deploys again what this build produced, without rebuilding. This is how you roll back: open the last good build and deploy it.
- Approve: lets a pull request waiting for approval run.
The result of each build is also reported on the commit on your git provider, with a link back to the build page. When a build fails, admins get a notification, and a CI Build Failed alert is created for you in the monitoring alerts, where you can add an email or tune it.
Images are named after the registry, the project and the application, and tagged with the commit (docker.mydomain.com/myproject/myapp:sha-1a2b3c4d5e6f), plus a tag that follows the branch (:main).
Branches and environments
By default, only the default branch is deployed. Other branches are built, which is handy to know they still compile, and not deployed.
Environments, in the project settings, decide which branch deploys where. Click Add an environment for each:
- Branch (glob):
main,develop,release/*... - Environment: a name, such as
productionorstaging. - Deployment name and Hostname: where this branch goes. Each environment is its own deployment, with its own URL: the hostname of the environment replaces the one of the application's routes, including the routes declared in
cosmos.json. - auto: on by default. Turn it off for an environment you want to deploy by hand: builds are prepared, and go live when you click Deploy this build.
- Variables: environment variables set on the deployed application for this environment only, on top of the Runtime environment of the project.
${NAME}inserts a secret, and${COSMOS_BRANCH},${COSMOS_ENVIRONMENT}and the other build variables work too. With acosmos.json, they are set on the services that take the built image.
A classic setup: develop deploys automatically to staging.mydomain.com with LOG_LEVEL=debug, and main to production with auto turned off.
Pull requests
Previews
Turn on Deploy a preview per open pull request and every pull request gets its own temporary deployment, with its own URL, to review the change for real before merging.
- Preview hostname: a pattern such as
pr-{{pr}}.preview.mydomain.com. Empty, Cosmos prefixes the hostname of the application withpr-and the number of the pull request. - Preview lifetime:
72hby default, counted from the last build of the pull request.
Previews are removed when the pull request is closed or merged, or at the end of their lifetime. The live ones are listed on the Overview tab of the project. Previews are made for container deployments, and need pull requests to run the full pipeline, which brings us to the next setting.
Who can run what
A pull request runs code written by someone else, on your servers, potentially with your secrets. The Pull requests setting decides how much you trust them:
- Build without secrets and without pushing (the default): pull requests are only compiled, to check that they build. Nothing is pushed or deployed.
- Full pipeline for collaborators, without secrets for others: people with write access to the repository get the full pipeline, previews included. Others are only compiled.
- Wait for an administrator's approval: the build waits as Awaiting approval until someone clicks Approve in Cosmos, then runs the full pipeline.
- Do not build pull requests.
Secrets
The Secrets tab of a project holds the sensitive values your builds need: API keys, tokens, passwords. They are stored once and never shown again, and they are hidden from the build logs.
Secrets are available as environment variables in every step of the build. In a Dockerfile, they are mounted the BuildKit way, so they never end up in the image:
RUN --mount=type=secret,id=NPM_TOKEN \
NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) npm ci
In a .woodpecker.yml, use them with from_secret and the name in lower case.
Secrets also reach the deployed application: write ${NAME} anywhere in the deploy block of cosmos.json, typically in the environment of a service, and the build fills it in when it deploys. See cosmos.json.
Each secret has a PR builds switch, off by default: even a trusted pull request only receives the secrets you explicitly allow, in its steps and in its preview deployment alike.
Copy from another project takes the secrets, the build-time environment and the runtime environment of another project and sets them on this one. Entries with the same name are replaced, the others kept. The values are copied on the server: they never pass through your browser.
cosmos.json
Everything above can be set from the interface. But you can also commit a cosmos.json file at the root of the repository, and keep the build and the deployment next to the code. When the file exists, it takes over from the settings of the project for what it declares.
A container application
{
"ci": {
"steps": [
{ "name": "test", "image": "node:22-alpine", "commands": ["npm ci", "npm test"] }
]
},
"deploy": {
"name": "myapp",
"replicas": 2,
"compose": {
"services": {
"myapp": {
"container_name": "myapp",
"image": "${artifact}",
"restart": "unless-stopped",
"environment": ["DATABASE_URL=${db.main.app.url}"],
"routes": [
{
"Name": "myapp",
"Mode": "SERVAPP",
"Target": "http://myapp:8080",
"UseHost": true,
"Host": "app.mydomain.com",
"Tunnel": "_ANY_",
"LBMode": "load_based",
"SmartShield": { "Enabled": true }
}
]
}
}
}
},
"environments": {
"main": "production"
}
}
ci.stepsrun before the build, each in the container image of your choice. If a step fails, the build stops there: this is where your tests go.deployis exactly what you would write in the deployment form: name, replicas or autoscaling, tags, and the compose. Everything a deployment can do is available, including template variables.${artifact}is replaced by the image that was just built. Other services of the compose (a Redis, a worker using a public image) are left as they are.- The build variables and the project secrets are available anywhere in
deploy:${COSMOS_BRANCH},${COSMOS_SHA},${COSMOS_BUILD},${COSMOS_ENVIRONMENT},${COSMOS_PR}and the others listed in Using your own pipeline, and${NAME}for a secret."environment": ["APP_VERSION=${COSMOS_SHA}", "DATABASE_PASSWORD=${DB_PASSWORD}"]stamps the running container with its build and gives it its credentials. - What
deployleaves out falls back to the project settings: noreplicasin the file means the Replicas of the project. environmentsmaps branches to environments, like the Environments table. The long form gives each branch its own target, and its ownenvvariables. Add"autoDeploy": falseto an environment you want to deploy by hand:
"environments": {
"main": "production",
"develop": { "name": "staging", "deployment": "myappstaging", "host": "staging.mydomain.com", "env": { "LOG_LEVEL": "debug" } }
}
A static site
{
"build": { "strategy": "static", "publishDir": "dist" },
"ci": {
"steps": [
{ "name": "build", "image": "node:22-alpine", "commands": ["npm ci", "npm run build"] }
]
},
"deploy": { "type": "static", "site": "blog", "host": "blog.mydomain.com", "spa": false }
}
The steps build the site, then the dist folder is published as a new version of the site blog and goes live. The previous versions stay in the registry, one click away from a rollback.
Functions
{
"build": { "strategy": "none" },
"deploy": {
"type": "function",
"name": "myfunctions",
"function": {
"runtime": "node22",
"source": { "registry": "npm" },
"handlers": [
{ "name": "hello", "handler": "hello", "route": { "Host": "functions.mydomain.com", "PathPrefix": "/hello", "StripPathPrefix": true } },
{ "name": "contact", "handler": "contact", "route": { "Host": "functions.mydomain.com", "PathPrefix": "/contact", "StripPathPrefix": true } }
]
}
}
}
On every push, Cosmos publishes the package of the repository (read from its package.json or pyproject.toml) to the registry, and deploys that exact version to the functions. The function block is the one described in Cloud Functions. For Python packages, remember to bump the version when the code changes.
Publishing a package
A library, a CLI or a shared module does not need a container: pick npm package or PyPI package as the build strategy (or set it in cosmos.json) and the project's Package registry, and every push publishes the package of the repository, read from its package.json or pyproject.toml, with the project's own token on the registry.
An npm package is installed and built exactly like a Node application: the Node version comes from .nvmrc or engines, the package manager from the lockfile (npm, pnpm, yarn or bun), then npm run build when the package has a build script, and npm publish runs from the built folder. The files field of package.json decides what goes in, as with a manual publish. A PyPI package is built with python -m build, so any build backend declared in pyproject.toml works.
{
"build": { "strategy": "npm" }
}
The version follows the branch:
- a push to the default branch publishes the version of the manifest as it is, tagged
latest. When the registry already holds that version, the build says so and keeps it as its result instead of failing, so bump the version when the code changes; - any other branch publishes a prerelease of it,
1.2.3-1a2b3c4d5e6ffor npm under a dist-tag named after the branch (npm install mylib@feature-login),1.2.3+1a2b3c4d5e6ffor PyPI, so a branch build never takes a real version.
The build page shows the published name@version with a link to the registry, and pull requests from outside collaborators build the package without publishing it.
The build block
build accepts the same settings as the Build step of the wizard, plus the ones the wizard leaves at their defaults: strategy (dockerfile, railpack, static, npm, pypi, woodpecker or none), dockerfile, publishDir, platform (the image architecture, linux/arm64 for instance; the build node's own by default), timeoutMinutes (60) and env. Leave it out to let Cosmos detect the build. rootDir, at the top of the file, selects a sub-folder.
Several builds in one repository
A repository can hold more than one thing to build: a frontend and a backend, the same service for two CPU architectures, a test suite that must pass before anything is published. Replace build and ci with a builds map, one entry per job:
{
"builds": {
"fe": {
"rootDir": "web",
"steps": [{ "image": "node:22", "commands": ["npm ci", "npm run build"] }],
"artifacts": { "dist": "dist" },
"output": "none"
},
"be": {
"rootDir": "api",
"strategy": "dockerfile",
"needs": ["fe"]
}
},
"deploy": {
"compose": { "services": { "api": { "image": "${artifact.be}" } } }
}
}
Each entry accepts everything the build block does, plus:
rootDir: the folder of the job, under the root folder of the project.steps: the job's own steps, as inci.steps.needs: the jobs that must pass before this one starts. Jobs with no dependency run at the same time, each on its own node; a job whose dependency fails is skipped and the build fails.tags: the node tags the job needs, on top of the project's Build node tags. This is how anarm64image gets built on an ARM node: tag the node, put the tag on the job. Without a tag, any node builds it, with QEMU emulation when the platform is not the node's own (see Where builds run).artifacts: files to keep for the jobs that need this one, and for download: a name and a path inside the job folder. See Build artifacts.output: what the job produces,image,site,packageornone. Cosmos infers it from the strategy; setnonefor a job that only runs tests,sitewith apublishDirto publish a folder from a job of any strategy.
Images are named after the entry: docker.mydomain.com/myproject/be:sha-1a2b3c4d5e6f. In the deploy block, ${artifact.<job>} is the image of that job, and ${artifact} still works when exactly one job produces an image. A static or function deploy takes the one job that produces a site or a package.
Every step of a job that needs another sees that job's image or package as ${artifact.<job>} in its commands and environment, and as the variable COSMOS_ARTIFACT_<JOB>.
A multi-platform image
Build the image once per platform on the right nodes, then merge them with a job of strategy manifest:
{
"builds": {
"be-amd64": { "strategy": "dockerfile", "platform": "linux/amd64", "tags": ["amd64"] },
"be-arm64": { "strategy": "dockerfile", "platform": "linux/arm64", "tags": ["arm64"] },
"be": { "strategy": "manifest", "needs": ["be-amd64", "be-arm64"] }
},
"deploy": {
"compose": { "services": { "api": { "image": "${artifact.be}" } } }
}
}
The be job runs no container: it writes an image index under its own name, tagged like any build, that Docker resolves to the right platform on every node. The per-platform images stay in the registry as the parts of that index; their own tags are removed when the build is deleted.
Build artifacts
A job that declares artifacts keeps the named folders or files once its steps have run. The jobs that need it find them under /cosmos/artifacts/<job>/<name> in every step (the path is also in COSMOS_ARTIFACTS), and a Dockerfile copies them from the context artifact.<job>:
FROM nginx:alpine
COPY --from=artifact.fe dist/ /usr/share/nginx/html/
The files are stored in a registry named ciartifacts that Cosmos creates the first time a build keeps something: a generic package registry, internal to the constellation, managed by CI/CD and listed with your other registries, where each project is a package and each build a version. The build page links every file for download. A node that built or already fetched a file does not download it again.
Each project can keep up to 8 GiB of files, a limit you can change in the project's settings (registry.artifactQuotaBytes through the API or Terraform). Deleting a build frees its files, and Cosmos deletes the oldest builds past the last 200. The registry itself cannot be deleted by hand; it goes away with the last CI/CD project.
Using your own pipeline
If the repository already has a .woodpecker.yml, Cosmos runs it as it is, with its steps, services and when rules. You can also call it from cosmos.json with a step of type woodpecker, and keep the deployment in cosmos.json.
Every step, in either format, receives variables describing the build: COSMOS_PROJECT, COSMOS_BUILD, COSMOS_BRANCH, COSMOS_SHA, COSMOS_ENVIRONMENT, COSMOS_PR (for pull requests), COSMOS_ROOT_DIR, and for pipelines that push the image themselves, COSMOS_IMAGE, COSMOS_IMAGE_REPO, COSMOS_IMAGE_TAGS, COSMOS_REGISTRY_HOST, COSMOS_REGISTRY_USER and COSMOS_REGISTRY_TOKEN. An image pushed as COSMOS_IMAGE is picked up and deployed like the ones Cosmos builds. The same variables can be used in the deploy block of cosmos.json.
Where builds run
Builds run on the nodes of your cluster. For each build, Cosmos picks the least busy node among those carrying the Build node tags of the project (any node when empty); that node reads the repository and starts the first job, and every other job of the build goes to the least busy node carrying the project's tags and the job's own. A node runs up to two jobs at a time, and a project builds one commit at a time, so deployments always happen in order.
The Build nodes tab shows, for each node, the platforms it can build an image for: its own, and the ones it emulates with QEMU when the host has binfmt handlers registered (the tonistiigi/binfmt image installs them). An emulated build is many times slower than a native one; for an image used on both architectures, prefer one job per platform on the matching nodes, merged by a manifest job.
Building is demanding: on a cluster that also serves traffic, it is a good idea to dedicate a node or two with a build tag. The Build nodes tab shows your nodes, their tags and what they are building.
Cosmos keeps the last 200 builds of each project, with their logs.
Monitoring and events
The Monitoring tab graphs the builds that passed and failed and how long they took, for all projects or for one. The Events tab keeps the history: builds queued, started, passed, failed, approved, deployments and previews. Each deployment made by CI/CD is also recorded in the events of the deployment itself.
Deleting a project
The Danger Zone tab disconnects the repository: the project, its builds and logs are removed, along with its webhook, its registry tokens and its pull-request previews. What was deployed keeps running, and stays yours to manage from the Deployments and Registries pages.