# Overview

Coolship is a project-local developer CLI for Coolify. Link a repository to its Coolify application once, then deploy, read logs, and sync variables from the terminal.

URL: https://coolship.itrocas.com/docs

Coolship brings a Wrangler-like developer experience to [Coolify](https://coolify.io/), focused on the workflow between your local project and its deployed application. Instead of repeatedly dealing with application UUIDs, projects, environments, and dashboard navigation, Coolship links a local repository to a Coolify application and lets you work with it directly from the terminal:

```bash
coolship link
coolship status
coolship deploy
coolship logs
coolship open
```

Every command after `link` reads the binding it wrote, so none of them need resource identifiers.

## Install

On Linux (glibc or musl) and macOS, amd64 or arm64:

```bash
curl -fsSL https://raw.githubusercontent.com/joaomnuno/coolship/main/scripts/install.sh | sh
```

The script downloads the release archive from GitHub Releases, checks its SHA-256 against the release's `checksums.txt`, installs `coolship` into `$HOME/.local/bin`, and never runs `sudo`. See [Get started](https://coolship.itrocas.com/docs/get-started) for the options and for building from source.

## Why not coolify-cli?

Coolify already has [`coolify-cli`](https://github.com/coollabsio/coolify-cli), which provides command-line access to Coolify and its resources. Coolship is not intended to replace it or to become another general-purpose Coolify administration CLI. The distinction is:

* **`coolify-cli`** manages Coolify resources.
* **Coolship** manages the developer workflow around the project you are currently working on.

Instead of

```bash
coolify deploy uuid <application-uuid>
```

the workflow is

```bash
cd my-project
coolship deploy
```

because the repository is already linked to the correct Coolify project, environment, and application. The two tools share one login: Coolship reads and writes the same credentials file as `coolify-cli`, so authenticating in either is enough for both. Operations such as creating servers, private keys, or team members deliberately stay outside Coolship's scope.

## A session

```text
$ coolship deploy
Deployment 03dusayin5rleswixblvdqba: queued
Deployment 03dusayin5rleswixblvdqba: in_progress
Starting deployment of joaomnuno/example-coolify-project:main to Master Ubuntu.
Building docker image started.
Building docker image completed.
Rolling update started.
Attempt 2 of 10 | Healthcheck status: "healthy"
Rolling update completed.
Deployment 03dusayin5rleswixblvdqba: finished
Deployment: 03dusayin5rleswixblvdqba
Application: coolship-example (mm4c0zpbrzx8z96t0qiw3tff)
Status: finished
https://coolship.example.com
```

`deploy` submits one deployment and then observes exactly the UUID that submission returned, streaming the server's build log while it waits. It deploys the source and branch already configured in Coolify; it does not upload your worktree or push local commits.

## Status

Coolship is in early development. `init`, `link`, `status`, `deploy`, `logs`, `open`, `unlink`, `config`, `doctor`, `env pull|diff|push`, `preview`, `dev`, `domain`, and `login` are implemented, tested, and verified end to end against a live **Coolify 4.3.18** instance. [Server compatibility](https://coolship.itrocas.com/docs/concepts/server-compatibility) describes what that does and does not cover, and [Limits](https://coolship.itrocas.com/docs/platform/limits) lists what the API does not allow.

## Where next

- [Get started](https://coolship.itrocas.com/docs/get-started): Install, log in, link a repository, deploy, and read its logs in five minutes.
- [Concepts](https://coolship.itrocas.com/docs/concepts/project-binding): The binding file, credentials, variable scopes, what a deployment is, and previews.
- [Command reference](https://coolship.itrocas.com/docs/commands): Every command with its flags, output, JSON shape, and exit codes.
- [Guides](https://coolship.itrocas.com/docs/guides/ci): GitHub Actions, monorepos, and the environment-variable workflow.

---

# Get started

Install Coolship, log in to your Coolify instance, link a repository, deploy it, and read its logs.

URL: https://coolship.itrocas.com/docs/get-started

This walk-through takes about five minutes. You need a Coolify instance you can reach, an API token for it, and a repository that is (or will be) an application on that instance.

## Install

On Linux (glibc or musl) and macOS, amd64 or arm64:

```bash
curl -fsSL https://raw.githubusercontent.com/joaomnuno/coolship/main/scripts/install.sh | sh
```

The script downloads the release archive from GitHub Releases, checks its SHA-256 against the release's `checksums.txt`, installs `coolship` into `$HOME/.local/bin`, and prints the installed version plus a one-liner for your shell if that directory is not on `PATH`. It needs `curl` or `wget`, `tar`, and `sha256sum` or `shasum`; it never runs `sudo` (if you point it at a system directory it prints the command to run instead). Windows is not supported by the script; build from source or use WSL.

Overrides, as environment variables or flags (`sh -s -- --version 0.3.0 --dir /opt/bin`):

| Variable               | Flag        | Default            | Meaning                                                              |
| ---------------------- | ----------- | ------------------ | -------------------------------------------------------------------- |
| `COOLSHIP_VERSION`     | `--version` | latest             | Release to install; a pre-release such as `0.3.0-rc.1` must be named |
| `COOLSHIP_INSTALL_DIR` | `--dir`     | `$HOME/.local/bin` | Directory to install into, created if missing                        |
|                        | `--dry-run` |                    | Resolve the version and print what would happen                      |

### Build from source

Coolship needs Go 1.26 or newer, matching `coolify-cli` so code can move upstream. Newer toolchains build it unchanged.

```bash
git clone https://github.com/joaomnuno/coolship.git
cd coolship
scripts/build            # bin/coolship, version stamped from the nearest tag
bin/coolship --version
```

A plain `go build -o coolship .` also works and reports the Git revision it was built from. Every `vX.Y.Z` tag publishes a [GitHub Release](https://github.com/joaomnuno/coolship/releases) with archives for Linux, macOS, and Windows on amd64 and arm64, plus a `checksums.txt`.

## Log in

Log in once. Coolship verifies the URL and token against the server, then stores them in the same file [`coolify-cli`](https://github.com/coollabsio/coolify-cli) uses, so if you have already authenticated there, skip this step, and if you log in here, coolify-cli is logged in too.

```text
$ coolship login
Coolify URL: https://coolify.example.com
Context name [coolify]: home
API token:
Logged in to home (https://coolify.example.com) as team Personal on Coolify 4.3.18, now the default
Saved to /home/you/.config/coolify/config.json
```

Create the token in Coolify under *Keys & Tokens* with the read, write, and deploy abilities. The token is never echoed and never accepted as a flag. For CI, either set `COOLSHIP_URL` and `COOLSHIP_TOKEN` (no login needed) or pipe the token: `echo "$TOKEN" | coolship login --url … --name ci --token-stdin`. See [Credentials](https://coolship.itrocas.com/docs/concepts/credentials).

## Link the repository

If the repository is already an application on Coolify, `link` binds it. If it is not on Coolify yet, `init` creates the application from the repository's public remote and binds it in one step.

```bash
cd my-app
coolship link          # bind to an existing application
coolship init          # or: create the application first, then bind
```

`link` walks the hierarchy, asking only when a choice is genuinely ambiguous:

```text
Select project
> Personal
  Work
```

Arrow keys and Enter pick, typing filters a long list, and Esc cancels. The picker then gives way to the result:

```text
Linked project in /home/you/my-app/coolship.toml
Application: fenix-bot (9f8e7d6c)
Environment: production
Project: Personal
Context: home
```

It writes a small, credential-free `coolship.toml` at the repository root. Commit it; see [Project binding](https://coolship.itrocas.com/docs/concepts/project-binding).

> **Private repositories and build packs**
> A public repository is cloned without credentials. A private one is created through a GitHub App or a deploy key registered in Coolify; `init` asks which, or takes `--github-app`, `--deploy-key`, or `--create-deploy-key`. The build pack is detected from the application root (Compose file, Dockerfile, plain HTML, or Railpack for anything else) and refined with the flags in the [init reference](https://coolship.itrocas.com/docs/commands/init).

## Deploy

```bash
coolship deploy
```

Coolship deploys the source and branch **already configured in Coolify**, submits exactly one deployment, and waits for that deployment to finish, streaming the server's build log to stderr when your token may read it. Interrupting the command stops local waiting only; the remote deployment continues, and its UUID is reported. `--no-wait` returns the queued UUID immediately, `--force` rebuilds without cache, and `--timeout` bounds the wait (default 10 minutes). See [Deployments](https://coolship.itrocas.com/docs/concepts/deployments).

## Read the logs

```bash
coolship logs
coolship logs --lines 500
coolship logs --follow
```

`logs` reads the application's runtime logs; `--follow` keeps polling snapshots and reports any gap it cannot rule out instead of hiding it. `coolship open` opens the application's public URL in your browser, or its Coolify page with `--dashboard`, and `coolship status` reports the current status and URL.

## Next steps

- [Check the setup](https://coolship.itrocas.com/docs/commands/doctor): `doctor` runs every step a command performs and reports each one.
- [Sync environment variables](https://coolship.itrocas.com/docs/guides/environment-variables): Pull, diff, and push a `.env` file against one scope of the application's variables.
- [Deploy from CI](https://coolship.itrocas.com/docs/guides/ci): Use `COOLSHIP_URL` and `COOLSHIP_TOKEN` in GitHub Actions, and deploy pull-request previews.
- [Monorepos](https://coolship.itrocas.com/docs/guides/monorepos): Bind several applications in one repository with named targets.

---

# Project binding

The committed coolship.toml file that ties a repository to a Coolify application, in its single and named-target forms.

URL: https://coolship.itrocas.com/docs/concepts/project-binding

`link` (and `init`) write a small, credential-free, versioned file at the repository root. Every later command reads it, so none of them need resource identifiers.

```toml title="coolship.toml"
version = 1

[project]
context = "home"
project = "Personal"
environment = "production"
application = "fenix-bot"
root = "."
```

| Field         | Meaning                                                                                                                            |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `version`     | Schema version, currently `1`. Unknown fields and unsupported versions produce an error rather than being ignored.                 |
| `context`     | The name of a locally configured Coolify instance. It is neither a URL nor a token; see [Credentials](https://coolship.itrocas.com/docs/concepts/credentials). |
| `project`     | Exact Coolify project name.                                                                                                        |
| `environment` | Exact environment name within that project.                                                                                        |
| `application` | Exact application name within that environment.                                                                                    |
| `root`        | The application root, relative to the configuration file's directory.                                                              |
| `dev`         | Optional shell line for [`coolship dev`](https://coolship.itrocas.com/docs/commands/dev) to run when no command is given, for example `"npm run dev"`.         |

Optional `project_uuid`, `environment_uuid`, and `application_uuid` fields pin identity when names are ambiguous or when you ask for a pin (`link --project-uuid`, `--environment-uuid`, `--application-uuid`). A pinned UUID is authoritative: if it disappears, Coolship reports that rather than falling back to a same-named replacement. `link` writes a pin automatically when a name cannot describe the selection uniquely.

**Commit this file.** Tokens and secret values are never written to it, and neither are pulled variables.

## Discovery

Configuration is discovered by searching upward from the working directory, stopping at the enclosing Git worktree root, so a project never picks up a parent repository's binding. `--cwd` changes the directory that search starts from without changing the process directory, and `--config` names an explicit file instead.

Overrides such as `--context`, `--environment`, and `--target` apply to a single invocation and never rewrite `coolship.toml`. [`coolship config`](https://coolship.itrocas.com/docs/commands/config) shows the effective result after overrides, without making a request.

## Named targets

A repository with several applications uses named targets instead of `[project]`:

```toml title="coolship.toml"
version = 1

[apps.web]
context = "home"
project = "Personal"
environment = "production"
application = "frontend"
root = "apps/web"

[apps.api]
context = "home"
project = "Personal"
environment = "production"
application = "backend"
root = "apps/api"
```

Target names match `[A-Za-z0-9][A-Za-z0-9_-]*`, and `default` is reserved for the single form. Commands pick the target whose root most specifically contains the current directory; from anywhere, name it instead as a positional argument (`coolship deploy api`) or with `--target api`. Two targets with the same root, or a directory outside every root, require an explicit name rather than a guess. The [Monorepos](https://coolship.itrocas.com/docs/guides/monorepos) guide walks through linking each target.

## Migration rules

Rules that keep the file unambiguous:

* A file uses `[project]` **or** `[apps.<name>]`, never both; a mixed file is rejected so that it has one meaning.
* Adding a target keeps the existing ones and needs no confirmation.
* Changing a target's binding, or converting between the two forms (which drops the other form's bindings), asks for confirmation or requires `--replace` when noninteractive. The prompt says explicitly what would be dropped.
* Re-running `link` with the same result leaves the file, including its comments, untouched. A file that changed since it was read is a conflict, not silently overwritten.

[`coolship unlink`](https://coolship.itrocas.com/docs/commands/unlink) deletes the file; nothing on the server changes.

---

# Credentials

Where Coolship finds a Coolify URL and token, how it shares them with coolify-cli, and how CI supplies them.

URL: https://coolship.itrocas.com/docs/concepts/credentials

Coolship never asks you to authenticate twice. [`coolship login`](https://coolship.itrocas.com/docs/commands/login) writes, and every command reads, the Coolify CLI configuration:

* Unix and macOS: `~/.config/coolify/config.json`
* Windows: `%APPDATA%\coolify\config.json`

If you have already logged in with [`coolify-cli`](https://github.com/coollabsio/coolify-cli), Coolship uses that login; if you log in with Coolship, coolify-cli is logged in too. `--coolify-config PATH` reads an explicit file instead.

## Which instance is used

That file can hold several instances, called contexts. The one used for an invocation is selected by, in order:

1. `--context NAME` on the command line;
2. the `context` committed in `coolship.toml`;
3. the single default instance in the file.

A missing or ambiguous choice is an error rather than a guess. `coolship config` reports which credentials would be used, without making a request or showing the token.

## Tokens

Create a token in Coolify under *Keys & Tokens* with the read, write, and deploy abilities. `login` verifies the URL and token against the server (`GET /version` and the current team) before saving them. The token is read without echo, or from stdin with `--token-stdin`; it is never accepted as a flag, so it stays out of shell history and process listings.

Build logs and secret values can be withheld by token ability and team role. Coolship reports withheld data as unavailable, never as empty data: a deployment continues without its log, and a hidden variable value is noted rather than written as an empty string. Reading build logs during `deploy` needs a token that may read sensitive data.

`coolship logout NAME` removes a context from the file. The token remains valid on the server until you revoke it in Coolify.

## CI

Where Coolify CLI may not be installed, supply the pair as environment variables:

```bash
export COOLSHIP_URL=https://coolify.example.com
export COOLSHIP_TOKEN=…
coolship deploy
```

Both are required together, and the pair cannot be combined with `--context` or `--coolify-config`. It overrides any committed context for that invocation, and Coolship says so in a warning. The [CI guide](https://coolship.itrocas.com/docs/guides/ci) shows the GitHub Actions setup.

## What is never stored

Tokens and fetched secret values are never written to `coolship.toml`, to fixtures, or to debug output. Credential files are kept read-only, and the file Coolship writes on `login` is the same one coolify-cli manages.

---

# Environment variables

The two scopes Coolify keeps per application, withheld values, shared references, and what each command does with them.

URL: https://coolship.itrocas.com/docs/concepts/variables

Coolify keeps environment variables per application, and Coolship's [`env`](https://coolship.itrocas.com/docs/commands/env) and [`dev`](https://coolship.itrocas.com/docs/commands/dev) commands work with them under three rules.

## Two scopes

Coolify keeps a separate copy of every variable for preview deployments. Commands act on the **regular** scope unless `--preview` is given, and never touch the other one.

Coolify itself creates a preview copy of any regular variable that `push` creates, so a later `--preview` diff will show them. Selecting a remote environment (`--environment`) and selecting the preview scope are different dimensions: an environment holds applications, and each application holds both scopes.

## Withheld values are never invented

A value Coolify hides (a shown-once secret, or a value your token may not read) is:

* noted in the pulled file as a comment rather than written empty;
* reported as `?` in a diff, and listed under `withheld` in JSON output;
* overwritten by `push` only with `--force`;
* reported by `dev` and left to your own environment.

## References stay references

A shared variable such as `{{team.API_KEY}}` is pulled and compared as that reference, never as the value it resolves to, so a push cannot replace the reference with the secret. `dev` is the exception by design: it injects shared references as the values they resolve to, because a local process needs the real value.

## Flags and timing

`push` preserves each variable's literal, multiline, and shown-once flags. The server resets them when an update omits them, so Coolship restates the remote values on every update. Build-time and runtime are independent flags on the server; `dev` injects the **runtime** variables.

Changes made by `push` take effect on the next deployment.

## Where values appear

Values are masked in human and JSON output unless `--show-values` is given. Pulled values land only in the dotenv file, which defaults to `.env` in the application root and is created with private permissions (mode 0600) and replaced atomically; comments and ordering in an existing file are preserved. Raw application logs can themselves contain secrets: Coolship prints them as requested output and does not persist them anywhere else.

The [environment-variable workflow](https://coolship.itrocas.com/docs/guides/environment-variables) guide puts these rules into a day-to-day sequence.

---

# Deployments

What coolship deploy does and does not do, how a deployment is observed, and how interruptions and uncertainty are handled.

URL: https://coolship.itrocas.com/docs/concepts/deployments

`coolship deploy` triggers Coolify's configured deployment for the linked application. Understanding what that means avoids the most common surprise.

## What deploy does not do

Coolship deploys the source and branch **already configured in Coolify**. It does not upload your worktree, push local commits, or change the application's configured branch. Uncommitted or unpushed local changes are therefore not deployed; push them first, or use [`dev`](https://coolship.itrocas.com/docs/commands/dev) to run them locally with the application's variables.

## Exactly one deployment

Coolship submits one deployment (`POST /deploy` with the application UUID) and validates that the response names the selected resource and a nonempty deployment UUID; an informational message alone is not a queued deployment. It then observes **exactly that UUID**, never "the latest deployment" for the application, which could belong to someone else.

Deployment states are interpreted as `queued`, `in_progress`, `finished`, `failed`, and `cancelled-by-user`. Only a confirmed successful terminal state completes a waiting command successfully. An unknown state is shown as-is and waits for the timeout instead of being guessed as terminal. Deployment completion is reported separately from the application's health, which `status` reads.

## Stages and the build log

Coolify writes fixed lines into a deployment's build log as its job moves along: `Building docker image started.` and `completed.`, `Rolling update started.` and `completed.`, `New container started.`, `New container is healthy.` or `unhealthy.`, `Removing old containers.`, and `Deployment failed.` Coolship reads them as **stages** — build, rolling update, container, cleanup — each started once and ended once, done or failed. A compose deployment that pulls images without a build line, or a container without a health check, still ends its stage: the new container starting finishes an open build, and the rolling update completing accepts the container.

In a terminal, the stages are a checklist under the target: a spinner and elapsed time on whatever is open, `✓` and its duration on what finished, `✗` on what failed, the stages not reached yet dim. Warnings print above it and stay. The build log is collapsed by default and printed in full, after the checklist, when the deployment fails, so its tail is right before the deployment page and the error; no command can fetch a past build log. `--logs` streams the log live above the checklist for one run and `--no-logs` keeps it collapsed; without either, the `build_logs` key of the preferences file decides, and without that the verbosity: collapsed at normal, streamed with `--verbose` or `--debug`, which also print the plain status lines instead of the checklist.

```text
$ coolship deploy
→ coolship-example
→ production

✓ Deployed                      0:52
  ✓ build                       0:41
  ✓ rolling update              0:08
  ✓ container                   0:06
  ✓ cleanup                     0:00
Deployment: 03dusayin5rleswixblvdqba
Application: coolship-example (mm4c0zpbrzx8z96t0qiw3tff)
Status: finished
https://coolship.example.com
```

Piped, in CI, or with `--format json`, the build log streams to stderr as it always did — clone, build, rolling update, health checks — and each stage transition is one plain line among the status lines (`Stage build: started`). Each visible log entry is emitted once, ahead of the status change it led to, and the stage it announces follows it. All of this needs a token that may read the build log; otherwise progress continues without it. Build logs belong to deployment observation; [`logs`](https://coolship.itrocas.com/docs/commands/logs) means application runtime logs.

```text
$ coolship deploy 2>&1 | cat
Deployment 03dusayin5rleswixblvdqba: queued
Deployment 03dusayin5rleswixblvdqba: in_progress
Starting deployment of joaomnuno/example-coolify-project:main to Master Ubuntu.
Building docker image started.
Stage build: started
Building docker image completed.
Stage build: done
Rolling update started.
Stage rolling update: started
New container started.
Stage container: started
Attempt 2 of 10 | Healthcheck status: "healthy"
New container is healthy.
Stage container: done
Removing old containers.
Stage cleanup: started
Stage cleanup: done
Rolling update completed.
Stage rolling update: done
Deployment 03dusayin5rleswixblvdqba: finished
Deployment: 03dusayin5rleswixblvdqba
Application: coolship-example (mm4c0zpbrzx8z96t0qiw3tff)
Status: finished
https://coolship.example.com
```

## Waiting, or not

By default `deploy` waits for the deployment to finish, bounded by `--timeout` (10 minutes unless changed). `--no-wait` returns the queued deployment UUID immediately. `--force` asks Coolify to rebuild without cache.

The last line is where to look: the application's URL when the deployment finished and the application has a domain, otherwise the deployment's page in Coolify — with `--no-wait`, when `--timeout` elapses, and for a preview, whose application URL is the production one. A failed deployment prints the build log it gathered, then names its page on stderr, where the retry lives. `start`, `restart`, and [`preview`](https://coolship.itrocas.com/docs/commands/preview) show the same checklist, take the same `--logs` and `--no-logs`, and end the same way.

Interrupting the command stops local waiting only; the remote deployment continues, and the UUID is reported so you can pick it back up in Coolify. The checklist ends with `✗ Observation stopped` rather than `Deployment failed`, because the server gave no verdict. Coolship does not cancel the remote deployment automatically.

## Uncertain submission is never retried

The Coolify API defines no idempotency key for deployments, so a `POST /deploy` whose outcome is uncertain — a network error after the request was sent, for example — is never replayed automatically. Coolship reports what it knows so you can recover manually, rather than risking a duplicate deployment.

## Other commands that deploy

[`init --deploy`](https://coolship.itrocas.com/docs/commands/init) submits and observes the first deployment of a newly created application exactly like `deploy`, and [`preview`](https://coolship.itrocas.com/docs/commands/preview) deploys the preview Coolify holds for a pull request through the same observation. Both share the flags above.

---

# Preview deployments

How Coolship deploys pull-request previews, and why Coolify must already know the pull request.

URL: https://coolship.itrocas.com/docs/concepts/previews

Coolify can deploy a preview of a pull request. [`coolship preview`](https://coolship.itrocas.com/docs/commands/preview) deploys the preview Coolify holds for a pull request and observes it exactly like [`deploy`](https://coolship.itrocas.com/docs/concepts/deployments).

## Coolify must know the pull request

The Coolify API accepts a pull request number when deploying, but only for a pull request it already holds as a preview record. There is no API endpoint that creates or lists previews: they are created through the Coolify UI, or by Coolify's GitHub webhook when *Preview Deployments* is enabled on the application and a pull request is opened.

`coolship preview` therefore cannot create a preview either. When the server does not know the pull request it answers with a message and no deployment, and Coolship repeats that answer with what to do about it:

```text
$ coolship preview --pr 999999
Error: server did not confirm a deployment for application mm4c0zpbrzx8z96t0qiw3tff: Pull request 999999 not found for this resource. (Coolify must already know the pull request: enable preview deployments and add it through its webhook or the UI)
```

To make previews work:

1. Enable *Preview Deployments* on the application in Coolify.
2. Let Coolify's GitHub webhook register the pull request (or add it in the UI).
3. Run `coolship preview --pr N`, or plain `coolship preview` in a GitHub Actions `pull_request` job, where the number is read from `GITHUB_REF`.

This path was verified live against a public repository through the webhook: a webhook-created preview deployed in 12 seconds.

## What Coolship can and cannot show

Preview URLs follow the application's preview URL template but are not readable through the API, so [`open`](https://coolship.itrocas.com/docs/commands/open) does not offer them. Preview deployments use the application's **preview** variable scope; [`env --preview`](https://coolship.itrocas.com/docs/commands/env) and [`dev --preview`](https://coolship.itrocas.com/docs/commands/dev) act on it.

The [CI guide](https://coolship.itrocas.com/docs/guides/ci) shows a `pull_request` workflow that runs `coolship preview`.

---

# Server compatibility

Coolship is verified against Coolify 4.3.18. What the verification covered, what the server was observed to do, and what other versions can expect.

URL: https://coolship.itrocas.com/docs/concepts/server-compatibility

**Verified against Coolify 4.3.18.** Every command was run end to end against a live instance: creating a Dockerfile application from a public repository with `init` and deleting it again, linking, deploying, and following logs of a real Dockerfile application, syncing its variables in both scopes, deploying a webhook-created pull request preview, running a local process with its variables, and linking a two-target monorepo.

The 4.3.19 source has no changes to any endpoint Coolship uses — the API routes, the deploy and project controllers, the sensitive-data middleware, and the deployment-status enum are identical — so it is expected to behave identically. Other versions are untested.

## What the verification uses

Coolship talks to the API under `/api/v1` with a `coolship/<version>` user agent. The endpoints, per command:

| Command                       | Operations                                                                                                                            |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `link`                        | `GET /projects`, `GET /projects/{uuid}/environments`, `GET /projects/{uuid}/{environment_uuid}`, `GET /applications/{uuid}` as needed |
| `init`                        | `GET /servers`, the `link` reads, optionally `POST /projects`, then `POST /applications/public`; with `--deploy`, the `deploy` calls  |
| `status`                      | resolution, then `GET /applications/{uuid}`                                                                                           |
| `deploy`, `preview`           | `POST /deploy`; `GET /deployments/{deployment_uuid}` while waiting                                                                    |
| `logs`                        | `GET /applications/{uuid}/logs?lines=…&show_timestamps=true`                                                                          |
| `doctor`                      | `GET /version`, then resolution                                                                                                       |
| `domain`, `domain set`        | resolution; `PATCH /applications/{uuid}` with `domains`, `redirect`, `force_domain_override`                                          |
| `login`                       | `GET /version`, `GET /teams/current`                                                                                                  |
| `dev`, `env pull\|diff\|push` | `GET /applications/{uuid}/envs`, `PATCH /applications/{uuid}/envs/bulk`, `DELETE /applications/{uuid}/envs/{env_uuid}`                |

`open`, `config`, and `unlink` make no request beyond resolution, and `config` and `unlink` make none at all.

## Observed server behavior

These observations shape Coolship's client and were not visible from the server's source alone:

* **Runtime log snapshots omit the newline after the final line.** Overlap detection compares lines without terminators, and every emitted chunk ends with a newline.
* **`GET /version` returns plain text** (`4.3.18`) with an HTML content type.
* **Deployment logs are present only when the token can read sensitive data.** Without that ability the field is absent, which Coolship reports as unavailable rather than empty.
* **A deployment response identifies its application by integer id and name**, not by UUID. Coolship verifies the returned deployment UUID against the one it submitted.
* **Creating a regular variable also creates a preview-scope twin.** Preview scope is a separate dimension that every variable workflow selects explicitly.
* **A shown-once variable's value is withheld from the regular row**, but its auto-created preview twin is returned in clear. Coolship never reads around a withheld value; the twin is a server defect that was reported privately to Coolify on 2026-09-10.
* **The variable API accepts `is_buildtime` and `is_runtime`**; bulk creation is `PATCH …/envs/bulk` and deletion is by variable UUID.
* **`POST /deploy` accepts a pull request number** and answers HTTP 200 with a message-only receipt when the pull request has no preview record. No endpoint creates previews.
* **`POST /applications/public` answers 201 with the UUID and domains** and assigns the generated `https://<uuid>.<wildcard>` domain at once. A GitHub URL is stored as `owner/repo`; other hosts keep the full URL. A never-deployed application reports `exited:unhealthy`. A refusal is a 422 with a message and field errors, which Coolship includes in its error.
* **A Cloudflare bot rule in front of an instance can reject some default user agents.** Coolship's `coolship/<version>` agent is accepted.

The limits that follow from the API, independent of version, are listed under [Limits](https://coolship.itrocas.com/docs/platform/limits).

## End-to-end tests

The repository's `scripts/e2e` runs every command against a live instance and prints a PASS/FAIL line per step, confined to one example application; a GitHub Actions workflow runs it weekly and on every published release when the `COOLSHIP_URL` and `COOLSHIP_TOKEN` secrets are present. [ARCHITECTURE.md](https://github.com/joaomnuno/coolship/blob/main/ARCHITECTURE.md) records the verification in detail.

---

# Command reference

Every Coolship command, the options they all share, and the conventions for prompts, output, and exit codes.

URL: https://coolship.itrocas.com/docs/commands

```text
coolship [command] [flags]
```

| Command                                   | Purpose                                                                 |
| ----------------------------------------- | ----------------------------------------------------------------------- |
| [`init`](https://coolship.itrocas.com/docs/commands/init)             | Create a Coolify application for this repository, then link it          |
| [`link`](https://coolship.itrocas.com/docs/commands/link)             | Bind this repository to an existing Coolify application                 |
| [`unlink`](https://coolship.itrocas.com/docs/commands/unlink)         | Remove this repository's binding to its Coolify application             |
| [`status`](https://coolship.itrocas.com/docs/commands/status)         | Inspect the linked application's current status                         |
| [`deploy`](https://coolship.itrocas.com/docs/commands/deploy)         | Deploy the linked application using its configured Coolify source       |
| [`logs`](https://coolship.itrocas.com/docs/commands/logs)             | Read runtime logs from the linked application                           |
| [`open`](https://coolship.itrocas.com/docs/commands/open)             | Open the linked application, or its Coolify page, in a browser          |
| [`doctor`](https://coolship.itrocas.com/docs/commands/doctor)         | Check configuration, credentials, and server access                     |
| [`config`](https://coolship.itrocas.com/docs/commands/config)         | Show the effective configuration for this directory                     |
| [`env`](https://coolship.itrocas.com/docs/commands/env)               | Synchronize a local `.env` file with the linked application's variables |
| [`preview`](https://coolship.itrocas.com/docs/commands/preview)       | Deploy the preview Coolify holds for a pull request                     |
| [`dev`](https://coolship.itrocas.com/docs/commands/dev)               | Run a local command with the application's variables                    |
| [`domain`](https://coolship.itrocas.com/docs/commands/domain)         | Show or change the linked application's domains                         |
| [`login`, `logout`](https://coolship.itrocas.com/docs/commands/login) | Save or remove a Coolify instance and API token on this machine         |

`coolship --version` prints the version, and `coolship <command> --help` prints each command's help.

## Global flags

Every command accepts these. Overrides apply to a single invocation and never rewrite `coolship.toml`.

| Flag                    | Purpose                                                                        |
| ----------------------- | ------------------------------------------------------------------------------ |
| `--cwd DIR`             | Use this working directory without changing the process directory.             |
| `--config PATH`         | Project configuration path, relative to the effective working directory.       |
| `--context NAME`        | Coolify CLI instance name for this invocation.                                 |
| `--coolify-config PATH` | Read credentials from this Coolify CLI configuration file.                     |
| `-e`, `--environment`   | Remote environment name for this invocation.                                   |
| `-t`, `--target NAME`   | Named target in a monorepo configuration (`[apps.<name>]`).                    |
| `--format human\|json`  | Output format; `human` is the default, and `logs --format json` prints NDJSON. |
| `--no-color`            | Disable styled output (`NO_COLOR` does the same).                              |
| `--verbose`             | Print one line per request on stderr and stream build logs.                    |
| `--debug`               | Print every request and response in full on stderr, token masked.              |

`COOLSHIP_VERBOSITY` and the preferences file set the verbosity when neither flag is given; see [Verbosity](https://coolship.itrocas.com/docs/platform/output#verbosity). `-v` is the version.

`status`, `deploy`, `logs`, `open`, `dev`, and `domain` also take the target as a positional argument: `coolship deploy api`.

## Conventions

* **Nothing changes without a look first.** Commands that change something — `init`, `link` over a different binding, `unlink`, `env push`, `domain set` — show the plan and ask for confirmation. When stdin is not a terminal, pass `--yes` (`--replace` for `link`) or the command refuses rather than guessing.
* **Results go to stdout; prompts, progress, and diagnostics go to stderr**, so piping stdout stays useful. `--format json` prints one result object per command (`logs` prints one event per line) and never mixes human progress into stdout.
* **Values that may be secrets are masked** unless `--show-values` is given, and tokens are never printed.
* **Exit codes** are `0` success, `1` the operation failed, `2` invalid input, configuration, or selection, and `130` interrupted. See [Output and exit codes](https://coolship.itrocas.com/docs/platform/output).

Every JSON result that names an application carries the same `target` object:

```json
{
  "target": {
    "target": "default",
    "instance": "home",
    "instance_url": "https://coolify.example.com",
    "project": "Personal",
    "project_uuid": "rxv3lqhdvuprnl433dczvo0s",
    "environment": "production",
    "environment_uuid": "5omkp5uuj0qpet6dy16r6bag",
    "application": "fenix-bot",
    "application_uuid": "mm4c0zpbrzx8z96t0qiw3tff",
    "root": "/home/you/my-app"
  }
}
```

`target` is `default` for the single `[project]` form and the target name otherwise; `root` is the absolute application root. Optional `warnings` arrays carry the same text the human output prints as `Warning:` lines.

---

# init

Create a Coolify application for this repository from its Git remote, then link it exactly as link would.

URL: https://coolship.itrocas.com/docs/commands/init

```text
coolship init [flags]
```

`init` is the first step for a repository that is not on Coolify yet. It reads the `origin` remote and the checked-out branch from Git (SSH forms are normalized to `https://github.com/owner/repo`), detects the build pack from the application root, shows the plan, and creates the application only after you confirm. Nothing is deployed unless you pass `--deploy`.

```text
$ coolship init
Create application my-app on home?
  Repository:  https://github.com/you/my-app (branch main)
  Build pack:  dockerfile, port 80
    Dockerfile: /Dockerfile
  Project:     Personal
  Environment: production
  Server:      Master Ubuntu
  Binding:     /home/you/my-app/coolship.toml
Confirm [y/N]: y
Created application my-app (9f8e7d6c) from https://github.com/you/my-app at main
Build pack: dockerfile, port 80
Dockerfile: /Dockerfile
URL: https://9f8e7d6c.coolify.example.com
Linked project in /home/you/my-app/coolship.toml
```

## Build packs

The application root decides the build pack, in the order Coolify's own form checks: a compose file (`docker-compose.yaml`, `docker-compose.yml`, `compose.yaml`, or `compose.yml`) makes a Compose application, a `Dockerfile` builds itself, an `index.html` with no `package.json` is served as it is, and anything else goes to Railpack, Coolify's default. `--build-pack` overrides the detection; naming `--dockerfile` or `--compose-file` (or `--compose-domain`) without `--build-pack` picks that pack too, so naming the file is enough.

| Build pack      | What Coolify does                                                                                  | Flags that refine it                                                                                                                                                                         |
| --------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `railpack`      | Detects the language, builds an image, runs it on `--port` (default 3000).                         | `--install-command`, `--build-command`, `--start-command` override what it detects. `--static` serves the build output with nginx on port 80 from `--publish-dir` (default `/dist`).         |
| `nixpacks`      | The same through Nixpacks, Railpack's predecessor.                                                 | The same flags.                                                                                                                                                                              |
| `static`        | Serves the files as they are with nginx, no build, on `--port` (default 80).                       | `--publish-dir` serves a subdirectory instead of the root.                                                                                                                                   |
| `dockerfile`    | Builds the Dockerfile and runs the image on `--port` (default 80).                                 | `--dockerfile PATH` names another file, relative to the application root.                                                                                                                    |
| `dockercompose` | Runs the compose file; each service publishes the ports the file gives it, so `--port` is refused. | `--compose-file PATH` names another file. `--compose-domain SERVICE=URL`, repeatable, gives a service its domain; without one, the plan warns and you set the domains in Coolify afterwards. |

Paths are relative to the application root (the monorepo target's directory with `--target`) and are stored with a leading slash, as Coolify does. A named Dockerfile or compose file must exist. For a Dockerfile application, Coolify's own health check is switched off at creation, as Coolify's form does, because the check it would generate needs `curl` or `wget` inside the image; a `HEALTHCHECK` in the Dockerfile is still used.

## Flags

| Flag                                                      | Purpose                                                                                                                                       |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--repo URL`                                              | Public repository URL (default: the origin remote).                                                                                           |
| `--branch NAME`                                           | Branch Coolify deploys (default: the checked-out branch).                                                                                     |
| `--name NAME`                                             | Application name (default: the repository name).                                                                                              |
| `--build-pack NAME`                                       | `railpack`, `nixpacks`, `static`, `dockerfile`, or `dockercompose` (default: detected).                                                       |
| `--port N`                                                | Port the application listens on (default: 3000 for `railpack` and `nixpacks`, 80 for `dockerfile` and `static`; refused for `dockercompose`). |
| `--static`                                                | Serve the `railpack` or `nixpacks` build output as a static site.                                                                             |
| `--publish-dir PATH`                                      | Directory to serve with `--static` or `--build-pack static` (default `/dist` with `--static`).                                                |
| `--install-command`, `--build-command`, `--start-command` | Override what `railpack` or `nixpacks` detect.                                                                                                |
| `--dockerfile PATH`                                       | Dockerfile to build, relative to the application root (default `Dockerfile`).                                                                 |
| `--compose-file PATH`                                     | Compose file to run, relative to the application root (default: the one found).                                                               |
| `--compose-domain SERVICE=URL`                            | Domain for one Compose service; repeat for each service.                                                                                      |
| `--project NAME`                                          | Exact Coolify project name (default: prompt, or the only project).                                                                            |
| `--create-project`                                        | Create the `--project` if it does not exist.                                                                                                  |
| `--server NAME`                                           | Exact server name (default: prompt, or the only usable server).                                                                               |
| `--deploy`                                                | Deploy after creating and wait for it.                                                                                                        |
| `--timeout DURATION`                                      | Maximum time to wait for the deployment with `--deploy` (default `10m0s`).                                                                    |
| `-y`, `--yes`                                             | Create without confirmation.                                                                                                                  |

`--environment` (global) defaults to `production`. Coolify gives a project it creates only a production environment, so `--create-project` with any other `--environment` is refused before anything is created. In a monorepo, `--target NAME` writes an `[apps.NAME]` table with the current directory as its root, which becomes the application's base directory.

> **Check the port**
> Coolify routes traffic to the port in the plan. It defaults to 3000 for a Railpack or Nixpacks build and 80 for a Dockerfile or static site; pass `--port` when your application listens elsewhere. A Compose application has no such port: each service publishes its own.

## Examples

```bash
coolship init                                                    # interactive
coolship init --project Personal --server "Master Ubuntu" --port 8080 --yes
coolship init --project Sandbox --create-project --deploy        # new project, first deployment
coolship init --repo https://github.com/you/my-app --branch main --name my-app
coolship init --static --publish-dir build --yes                  # railpack builds, nginx serves build/
coolship init --dockerfile deploy/Dockerfile --port 8080 --yes
coolship init --compose-domain web=https://app.example.com --compose-domain api=https://api.example.com --yes
```

The project and server are asked for only when there is a choice. Noninteractive use needs `--yes`.

## Behavior and limits

* **A private repository** is created through a GitHub App or a deploy key registered in Coolify; `--source`, `--github-app`, `--deploy-key`, and `--create-deploy-key` choose which (see the README for the full flow).
* **A Compose application's domains are per service.** `init` sets them with `--compose-domain`; afterwards they are managed in Coolify, since [`domain set`](https://coolship.itrocas.com/docs/commands/domain) does not apply to Compose applications. Variables work as for any application.
* **A refinement that does not belong to the pack is refused** before any request, so the plan never carries a setting Coolify would ignore: `--static` and the three commands need `railpack` or `nixpacks`, `--dockerfile` needs `dockerfile`, and `--compose-file`, `--compose-domain` need `dockercompose`.
* **A directory that is already linked is refused** rather than re-pointed; use `link` to change a binding.
* **Warnings arrive before the confirmation**, not after the application exists, so a Compose application with no `--compose-domain` can still be answered with no.
* The application is created without an instant deployment; `--deploy` then submits and observes the first deployment exactly like [`deploy`](https://coolship.itrocas.com/docs/commands/deploy).
* A refusal from the server carries its explanation — the message and field errors — so a repository Coolify cannot reach or a rejected value is reported as such rather than as a bare status code.
* The binding is written and verified through the same step `link` uses.

## JSON

```json
{
  "plan": {
    "path": "/home/you/my-app/coolship.toml",
    "target": "default",
    "root": "/home/you/my-app",
    "repository": "https://github.com/you/my-app",
    "branch": "main",
    "build_pack": "dockerfile",
    "port": 80,
    "name": "my-app",
    "instance": "home",
    "project": "Personal",
    "environment": "production",
    "server": "Master Ubuntu"
  },
  "target": { "…": "the resolved target, see the command reference" },
  "url": "https://9f8e7d6c.coolify.example.com"
}
```

`plan.static`, `plan.publish_directory`, `plan.dockerfile`, `plan.compose_file`, `plan.compose_domains` (a list of `service` and `domain`), `plan.install_command`, `plan.build_command`, `plan.start_command`, `plan.new_project`, and `plan.deploy` appear when set; `plan.port` is `0` for a Compose application; `deployment` carries a [`deploy`](https://coolship.itrocas.com/docs/commands/deploy#json) result when `--deploy` was given.

## Exit codes

`0` when the application was created and linked; `1` when Coolify refused or the deployment failed; `2` for invalid input, including a flag that does not belong to the build pack, a named Dockerfile or compose file that does not exist, an already linked directory, or noninteractive use without `--yes`; `130` when interrupted.

---

# link

Bind this repository to an existing Coolify application and write coolship.toml.

URL: https://coolship.itrocas.com/docs/commands/link

```text
coolship link [flags]
```

`link` selects a project, environment, and application — interactively, or from explicit selectors — verifies the binding against the server, and writes [`coolship.toml`](https://coolship.itrocas.com/docs/concepts/project-binding). Linking changes only local configuration; it never creates remote resources.

```text
$ coolship link
Select project
> Personal
  Work
```

Each choice is a picker: the arrow keys move, Enter picks, and Esc (or Ctrl-C) cancels without writing anything. Typing filters a list too long for the terminal; on a shorter one, `/` starts the filter. Choices show names only, and a detail such as the UUID appears only when two candidates share a name. When the directory is already linked, the cursor starts on the current project, environment, and application. Once the last choice is made, the picker is erased and the result names what was linked:

```text
Linked project in /home/you/my-app/coolship.toml
Application: fenix-bot (9f8e7d6c)
Environment: production
Project: Personal
Context: home
```

The prompts appear only when a choice is genuinely ambiguous; a single project, environment, or application is selected without asking. When stdin is not a terminal but input is still interactive, the same choices are printed as a numbered list that reads a number, or `q` to cancel. Noninteractive runs never prompt and name the flag to pass instead.

## Flags

| Flag                      | Purpose                                                                                                                                           |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--project NAME`          | Exact Coolify project name.                                                                                                                       |
| `--project-uuid UUID`     | Pin a Coolify project UUID.                                                                                                                       |
| `--environment-uuid UUID` | Pin an environment UUID within the selected project.                                                                                              |
| `--application NAME`      | Exact application name within the selected environment.                                                                                           |
| `--application-uuid UUID` | Pin an application UUID within the selected environment.                                                                                          |
| `--root PATH`             | Local application root, relative to the project configuration directory (default: `.` for `[project]`, the current directory for a named target). |
| `--replace`               | Replace existing changed configuration, including comments and unrelated settings.                                                                |

`-e`, `--environment NAME` (global) selects the environment by name, and `--target NAME` writes an `[apps.NAME]` table instead of `[project]`.

## Examples

```bash
coolship link                                                              # interactive
coolship link --project Personal --environment production --application fenix-bot
coolship link --project-uuid a1b2c3d4 --application-uuid 9f8e7d6c          # pin identity
cd apps/web && coolship link --target web                                  # monorepo target
```

Supplying selectors runs without prompts, which is what CI needs. Names are matched exactly within their selected parent. Use the `--*-uuid` flags to pin identity instead of a name; `link` also writes a pin automatically when a name cannot describe the selection uniquely.

## Behavior

* Replacing a different existing binding requires confirmation, or `--replace` when noninteractive. Replacing regenerates the complete configuration, including comments.
* Re-running `link` with the same result leaves the file, including its comments, untouched.
* In a monorepo, adding a target keeps the others; changing one, or converting between the single and named forms, requires review. See [Monorepos](https://coolship.itrocas.com/docs/guides/monorepos).
* A directory outside the Git worktree of an existing configuration never picks it up: discovery stops at the worktree root.

## JSON

```json
{
  "path": "/home/you/my-app/coolship.toml",
  "target": {
    "target": "default",
    "instance": "home",
    "instance_url": "https://coolify.example.com",
    "project": "Personal",
    "project_uuid": "rxv3lqhdvuprnl433dczvo0s",
    "environment": "production",
    "environment_uuid": "5omkp5uuj0qpet6dy16r6bag",
    "application": "fenix-bot",
    "application_uuid": "mm4c0zpbrzx8z96t0qiw3tff",
    "root": "/home/you/my-app"
  }
}
```

## Exit codes

`0` when the binding was written or was already identical; `2` when a selector matches nothing or more than one resource, when a pin no longer exists, or when a changed binding needs review noninteractively without `--replace`; `1` when the server cannot be reached; `130` when the prompt is cancelled.

---

# unlink

Delete coolship.toml. Nothing on the Coolify server changes.

URL: https://coolship.itrocas.com/docs/commands/unlink

```text
coolship unlink [flags]
```

`unlink` deletes the discovered `coolship.toml`. Deletion asks for confirmation, or requires `--yes` when input is noninteractive, and refuses if the file changed since it was read.

```text
$ coolship unlink
Delete /home/you/my-app/coolship.toml (Personal / production / fenix-bot)? [y/N]: y
Unlinked /home/you/my-app/coolship.toml
```

## Flags

| Flag          | Purpose                      |
| ------------- | ---------------------------- |
| `-y`, `--yes` | Delete without confirmation. |

## Examples

```bash
coolship unlink
coolship unlink --yes
coolship --cwd ../other-app unlink --yes
```

In a monorepo the whole file is deleted, including every named target; to drop one target, edit the file.

## JSON

```json
{ "path": "/home/you/my-app/coolship.toml" }
```

## Exit codes

`0` when the file was deleted; `2` when there is no configuration to delete, when the file changed since it was read, or when `--yes` is missing noninteractively; `130` when the prompt is cancelled.

---

# status

Inspect the linked application's current status and URL.

URL: https://coolship.itrocas.com/docs/commands/status

```text
coolship status [target] [flags]
```

`status` resolves the binding once and reports the application's identity, its current status as the server states it, and its URL.

```text
$ coolship status
Application: coolship-example (mm4c0zpbrzx8z96t0qiw3tff)
Environment: production
Project: coolship-example
Context: home
Status: running:healthy
URL: https://coolship.example.com
```

Unfamiliar status strings are preserved rather than interpreted. A never-deployed application, for example, reports `exited:unhealthy` on Coolify 4.3.18.

## Examples

```bash
coolship status
coolship status api                # named target in a monorepo
coolship status --format json | jq -r .status
```

## JSON

```json
{
  "target": {
    "target": "default",
    "instance": "home",
    "instance_url": "https://coolify.example.com",
    "project": "coolship-example",
    "project_uuid": "rxv3lqhdvuprnl433dczvo0s",
    "environment": "production",
    "environment_uuid": "5omkp5uuj0qpet6dy16r6bag",
    "application": "coolship-example",
    "application_uuid": "mm4c0zpbrzx8z96t0qiw3tff",
    "root": "/home/you/my-app"
  },
  "status": "running:healthy",
  "url": "https://coolship.example.com"
}
```

`url` is omitted when the application has no domain.

## Exit codes

`0` on a successful read regardless of the application's status; `1` when the server cannot be reached or refuses the request; `2` when the directory is not linked or the binding no longer resolves; `130` when an interrupt stops the read, including the read of the last deployment.

---

# deploy

Deploy the linked application using the source and branch already configured in Coolify, and observe exactly that deployment.

URL: https://coolship.itrocas.com/docs/commands/deploy

```text
coolship deploy [target] [flags]
```

`deploy` submits one deployment of the linked application and, by default, waits for it to finish. It does not upload your worktree or push local commits; Coolify deploys from its configured source and branch. [Deployments](https://coolship.itrocas.com/docs/concepts/deployments) explains the model.

In a terminal, the deployment is a checklist: the target, then the deployment and its stages — build, rolling update, container, cleanup — ticking as Coolify's build log markers arrive, with a spinner and elapsed time on whatever is open. The build log stays collapsed at normal verbosity; it is printed in full if the deployment fails. With `--verbose` or `--debug` no checklist is drawn: the plain stage and status lines print instead, with each finished stage's duration and the time since the deployment was queued, and the build log streams unless `--no-logs` or the `build_logs` preference collapses it.

```text
$ coolship deploy
→ coolship-example
→ production

✓ Deployed                      0:52
  ✓ build                       0:41
  ✓ rolling update              0:08
  ✓ container                   0:06
  ✓ cleanup                     0:00
Deployment: 03dusayin5rleswixblvdqba
Application: coolship-example (mm4c0zpbrzx8z96t0qiw3tff)
Status: finished
https://coolship.example.com
```

Piped, in CI, or with `--format json`, the same run prints status lines, the build log, and one line per stage transition on stderr as they arrive:

```text
$ coolship deploy 2>&1 | cat
Deployment 03dusayin5rleswixblvdqba: queued
Deployment 03dusayin5rleswixblvdqba: in_progress
Starting deployment of joaomnuno/example-coolify-project:main to Master Ubuntu.
Building docker image started.
Stage build: started
Building docker image completed.
Stage build: done
Rolling update started.
Stage rolling update: started
New container started.
Stage container: started
Attempt 2 of 10 | Healthcheck status: "healthy"
New container is healthy.
Stage container: done
Removing old containers.
Stage cleanup: started
Stage cleanup: done
Rolling update completed.
Stage rolling update: done
Deployment 03dusayin5rleswixblvdqba: finished
Deployment: 03dusayin5rleswixblvdqba
Application: coolship-example (mm4c0zpbrzx8z96t0qiw3tff)
Status: finished
https://coolship.example.com
```

Progress goes to stderr; the final result goes to stdout. The last line is where to look: the application's URL when the deployment finished and the application has a domain, otherwise the deployment's page in Coolify — with `--no-wait`, when `--timeout` elapses, and for a preview, whose application URL is the production one. A failed deployment prints the build log gathered so far after the checklist, then names its page on stderr, where the retry lives.

## Flags

| Flag                 | Purpose                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `--no-wait`          | Return after submission without observing completion.                                    |
| `--force`            | Force Coolify to rebuild without cache.                                                  |
| `--timeout DURATION` | Maximum time to wait for deployment completion (default `10m0s`).                        |
| `--logs`             | Stream the build log live above the checklist (terminal only).                           |
| `--no-logs`          | Keep the build log collapsed; it prints in full if the deployment fails (terminal only). |

`--logs` and `--no-logs` cannot be combined. Without either, the `build_logs` key of the [preferences file](https://coolship.itrocas.com/docs/commands/config) decides; without that, the [verbosity](https://coolship.itrocas.com/docs/platform/output#verbosity) does: collapsed at normal, streamed with `--verbose` or `--debug`. Piped output and `--format json` always stream the log.

## Examples

```bash
coolship deploy                  # wait for the deployment to finish
coolship deploy --logs           # and stream the build log above the checklist
coolship deploy --no-wait        # return the queued deployment UUID
coolship deploy --force          # rebuild without cache
coolship deploy --timeout 20m
coolship deploy api              # named target in a monorepo
```

## Behavior

* Coolship observes exactly the UUID the submission returned, never "the latest deployment", which could belong to someone else.
* States are interpreted as `queued`, `in_progress`, `finished`, `failed`, and `cancelled-by-user`. An unknown state is shown as-is and waits for the timeout instead of being guessed.
* Stages come from fixed lines in Coolify's build log (`Building docker image started.`, `Rolling update started.`, `New container is healthy.`, `Removing old containers.`, and their completions and failures), so they need a token that may read the build log. Without one, progress continues without stages or log, and the command says so.
* An observation that stops before the server's verdict — `--timeout` elapsing, an interrupt, a lost connection — ends the checklist with `✗ Observation stopped`, not `Deployment failed`; the deployment continues on the server. The build log stays collapsed in that case, since nothing failed: only a deployment the server reports as failed or cancelled prints it.
* Interrupting the command stops local waiting only. The remote deployment continues, and the UUID is reported so you can pick it back up.
* An uncertain submission — the request was sent but no answer arrived — is never replayed automatically, because the API defines no idempotency key. Coolship reports what it knows.

## JSON

```json
{
  "target": { "…": "the resolved target, see the command reference" },
  "deployment_uuid": "03dusayin5rleswixblvdqba",
  "status": "finished",
  "url": "https://coolship.example.com",
  "url_kind": "application"
}
```

`url` is the last line of the human output and `url_kind` says what it is: `application` for the application's URL, `deployment` for its page in Coolify (`<instance>/project/<project>/environment/<environment>/application/<application>/deployment/<uuid>`). With `--no-wait`, `status` is `queued` and the URL is the deployment page. A failed or cancelled deployment is reported with its state, the deployment page, and a nonzero exit. In JSON mode no progress reaches stdout.

## Exit codes

`0` when the deployment finished (or, with `--no-wait`, was queued); `1` when it failed, was cancelled, timed out, or could not be submitted; `2` when the directory is not linked or the target is ambiguous; `130` when interrupted while waiting — the deployment UUID is still printed.

---

# logs

Read runtime logs from the linked application, once or by following snapshots.

URL: https://coolship.itrocas.com/docs/commands/logs

```text
coolship logs [target] [flags]
```

`logs` reads the application's runtime logs — the container output — not the build log, which [`deploy`](https://coolship.itrocas.com/docs/commands/deploy) streams while it waits.

```text
$ coolship logs --lines 3
2026-09-10T11:49:33.442427921Z 127.0.0.1 - - [10/Sep/2026:11:49:33 +0000] "GET / HTTP/1.1" 200 182 "-" "Wget" "-"
2026-09-10T11:49:43.474192232Z 127.0.0.1 - - [10/Sep/2026:11:49:43 +0000] "GET / HTTP/1.1" 200 182 "-" "Wget" "-"
2026-09-10T11:49:53.513347418Z 127.0.0.1 - - [10/Sep/2026:11:49:53 +0000] "GET / HTTP/1.1" 200 182 "-" "Wget" "-"
```

Lines are printed exactly as the server returns them, with its timestamps, and never styled.

## Flags

| Flag              | Purpose                                                             |
| ----------------- | ------------------------------------------------------------------- |
| `-f`, `--follow`  | Poll new log snapshots until interrupted.                           |
| `-n`, `--lines N` | Maximum number of lines in each requested snapshot (default `100`). |

## Examples

```bash
coolship logs
coolship logs --lines 500
coolship logs --follow
coolship logs web -f              # named target in a monorepo
coolship logs --format json | jq -r .logs
```

## Following

The endpoint has no cursor, so `--follow` polls bounded snapshots and compares overlapping lines to emit only what is new. Repeated identical lines are handled correctly, but log rotation, a container restart, or a gap larger than `--lines` can still produce gaps or duplicates. Coolship reports such a reset as a warning on stderr rather than pretending the stream is lossless. Persistent polling failures are surfaced, and the command stops promptly on Ctrl-C.

Selecting a container is not supported: the server ignores the parameter Coolify CLI sends for it and returns the first container.

## JSON

`--format json` prints newline-delimited events, one object per line, both for a single snapshot and when following:

```json
{"type":"logs","logs":"2026-09-10T11:49:33.442427921Z 127.0.0.1 - - [10/Sep/2026:11:49:33 +0000] \"GET / HTTP/1.1\" 200 182 \"-\" \"Wget\" \"-\"\n"}
```

Each event has a `type`; log events carry the chunk in `logs`, and warnings such as a detected reset carry `message`. Every chunk ends with a newline.

## Exit codes

`0` when the snapshot was read; `1` when the logs cannot be read, or when following stops because a poll failed; `2` when the directory is not linked or `--lines` is not positive; `130` when `--follow` is interrupted, which is how following normally ends: `Interrupted` is printed on stderr, however many snapshots were read before it.

---

# open

Open the linked application's URL, or its Coolify page, in the default browser, and always print it.

URL: https://coolship.itrocas.com/docs/commands/open

```text
coolship open [target] [flags]
```

`open` resolves the application's public URL — or, with `--dashboard`, its page in Coolify — prints it on stdout, and launches the default browser. When stdin is not a terminal, or with `--print`, nothing is launched, so the command composes with other tools.

```text
$ coolship open --print
https://coolship.example.com
```

## Flags

| Flag          | Purpose                                                           |
| ------------- | ----------------------------------------------------------------- |
| `--dashboard` | Open the application's page in Coolify instead of its public URL. |
| `--print`     | Print the URL without launching a browser.                        |

## Examples

```bash
coolship open
coolship open --dashboard
coolship open --print | pbcopy
coolship open api --dashboard
```

The dashboard URL is `<instance>/project/<project uuid>/environment/<environment uuid>/application/<application uuid>`. Only web URLs are ever handed to the browser opener. Preview deployment URLs are not readable through the API, so `open` does not offer them.

## JSON

```json
{
  "target": { "…": "the resolved target, see the command reference" },
  "kind": "application",
  "url": "https://coolship.example.com"
}
```

`kind` is `application` or `dashboard`.

## Exit codes

`0` when the URL was resolved (whether or not a browser was launched); `1` when the server cannot be reached, or when the browser cannot be launched (the URL is still printed); `2` when the directory is not linked, or when the application has no domain to open (the error suggests `--dashboard`).

---

# doctor

Run every step a command performs and report each one, with a failing exit status when a check fails.

URL: https://coolship.itrocas.com/docs/commands/doctor

```text
coolship doctor [flags]
```

`doctor` runs the preparation every other command performs, one step at a time, and reports all of them: project configuration, Git boundary, binding, credentials, context, server reachability and version, and whether the binding resolves to an application. Local checks continue past failures; remote checks stop at the first.

```text
$ coolship doctor
[ok]   Project configuration: /home/you/my-app/coolship.toml
[ok]   Git repository: /home/you/my-app
[ok]   Binding: Personal / production / fenix-bot in /home/you/my-app
[ok]   Credentials: /home/you/.config/coolify/config.json (1 instance, default home)
[ok]   Context: home at https://coolify.example.com
[ok]   Server: Coolify 4.3.18
[ok]   Application: fenix-bot (9f8e7d6c) is running:healthy
```

Each check is marked `ok`, `warning`, `failed`, or `skipped`. Exit status is `1` when any check fails; warnings do not fail the command.

## Examples

```bash
coolship doctor
coolship doctor --context work
COOLSHIP_URL=https://coolify.example.com COOLSHIP_TOKEN=… coolship doctor
```

Running `doctor` in CI with the environment pair confirms that the credentials, the committed binding, and the server agree before a deployment.

## JSON

```json
{
  "checks": [
    { "name": "Project configuration", "status": "ok", "detail": "/home/you/my-app/coolship.toml" },
    { "name": "Git repository", "status": "ok", "detail": "/home/you/my-app" },
    { "name": "Binding", "status": "ok", "detail": "Personal / production / fenix-bot in /home/you/my-app" },
    { "name": "Credentials", "status": "ok", "detail": "/home/you/.config/coolify/config.json (1 instance, default home)" },
    { "name": "Context", "status": "ok", "detail": "home at https://coolify.example.com" },
    { "name": "Server", "status": "ok", "detail": "Coolify 4.3.18" },
    { "name": "Application", "status": "ok", "detail": "fenix-bot (9f8e7d6c) is running:healthy" }
  ],
  "failed": false
}
```

## Exit codes

`0` when no check failed; `1` when any check failed, in human and JSON mode alike.

---

# config

Show the effective configuration for this directory after overrides, reading files only.

URL: https://coolship.itrocas.com/docs/commands/config

```text
coolship config [flags]
```

`config` shows the discovered configuration file, the selected target, the binding, which credentials would be used, and the preferences file, after applying any overrides. It reads files only: no request is made to Coolify and no token is shown.

```text
$ coolship config --context home
Configuration:    /home/you/my-app/coolship.toml
Git root:         /home/you/my-app
Target:           default
Application root: /home/you/my-app
Context:          home
Project:          Personal
Environment:      production
Application:      fenix-bot
Credentials:      /home/you/.config/coolify/config.json
Instance:         home at https://coolify.example.com
Preferences:      /home/you/.config/coolship/preferences.toml (absent)
Override context: home
```

## Examples

```bash
coolship config
coolship config --target api
coolship config --environment staging --format json
```

Use it to see what `--cwd`, `--config`, `--context`, `--environment`, and `--target` resolve to, or which of the committed context and the `COOLSHIP_URL`/`COOLSHIP_TOKEN` pair applies.

## Preferences

Preferences are one developer's tastes on one machine, as opposed to the configuration in `coolship.toml`, which the team shares and commits. They live in a TOML file Coolship reads but never creates:

* Unix and macOS: `~/.config/coolship/preferences.toml`, or `$XDG_CONFIG_HOME/coolship/preferences.toml` when `XDG_CONFIG_HOME` is an absolute path
* Windows: `%AppData%\coolship\preferences.toml`
* Anywhere: the file `COOLSHIP_PREFERENCES` names, for tests and CI

```toml
verbosity = "normal"   # normal | verbose | debug
build_logs = true      # true streams build logs, false collapses them; absent follows the verbosity
color = "auto"         # auto | always | never
```

`verbosity` is the default when neither `--verbose`, `--debug`, nor `COOLSHIP_VERBOSITY` is given (see [verbosity](https://coolship.itrocas.com/docs/platform/output#verbosity)), and `build_logs` decides the build log when neither `--logs` nor `--no-logs` is given. `color` is read and checked today and takes effect in a later version. Credentials never go in this file: the Coolify URL and token stay in [Coolify CLI's file](https://coolship.itrocas.com/docs/concepts/credentials), so one login serves both tools.

The `Preferences` line shows the path, then `(absent)` when there is no file or the keys it sets, such as `(verbosity verbose, build logs off, color auto)`. An unknown key, a value outside its list, or a file that cannot be read is warned about once at the start of every command and the file is ignored; the line then says `(ignored: …)` with the reason. A broken file never stops a deploy.

## JSON

```json
{
  "config_path": "/home/you/my-app/coolship.toml",
  "config_root": "/home/you/my-app",
  "git_root": "/home/you/my-app",
  "target": "default",
  "app_root": "/home/you/my-app",
  "binding": {
    "context": "home",
    "project": "Personal",
    "environment": "production",
    "application": "fenix-bot",
    "root": "."
  },
  "credential_source": "file",
  "credential_path": "/home/you/.config/coolify/config.json",
  "instance": "home",
  "instance_url": "https://coolify.example.com",
  "overrides": { "context": "home" },
  "preferences": {
    "path": "/home/you/.config/coolship/preferences.toml",
    "present": true,
    "verbosity": "verbose",
    "build_logs": false
  }
}
```

`binding` is the selected table from `coolship.toml`, including any UUID pins and `dev` setting. `credential_source` is `file` when the Coolify CLI configuration supplies the credentials and reflects the environment pair when `COOLSHIP_URL` and `COOLSHIP_TOKEN` do; `git_root`, `credential_path`, `instance`, `instance_url`, `overrides`, and `warnings` are omitted when empty. `preferences` carries `path`, `present`, the keys the file sets (`verbosity`, `build_logs`, `color`; an absent key is omitted), and `error` with the reason when a present file is ignored.

## Exit codes

`0` when the configuration could be resolved; `2` when the directory is not linked, the file is invalid, the target is ambiguous, or the credentials cannot be selected.

---

# env

Pull, compare, and push environment variables between a local dotenv file and one scope of the linked application's variables.

URL: https://coolship.itrocas.com/docs/commands/env

```text
coolship env pull [flags]
coolship env diff [flags]
coolship env push [flags]
```

Coolify keeps two scopes per application: the variables regular deployments see, and a separate copy used by preview deployments. The `env` commands act on the regular scope unless `--preview` is given. Values are masked in output unless `--show-values` is given. Shared references such as `{{team.NAME}}` are synced as references, never as the values they resolve to. [Environment variables](https://coolship.itrocas.com/docs/concepts/variables) explains the rules; the [workflow guide](https://coolship.itrocas.com/docs/guides/environment-variables) shows them in sequence.

## Shared flags

| Flag          | Purpose                                                         |
| ------------- | --------------------------------------------------------------- |
| `--file PATH` | Dotenv file, relative to the application root (default `.env`). |
| `--preview`   | Act on the preview-deployment scope instead of the regular one. |

## env pull

Write the remote variables into the local file, creating it with private permissions. Keys that exist only locally are kept, comments and ordering are preserved, and values Coolify withholds are noted as comments rather than invented.

```text
$ coolship env pull
Wrote 4 regular variable(s) to /home/you/my-app/.env
```

```json
{
  "target": { "…": "the resolved target" },
  "scope": "regular",
  "file": "/home/you/my-app/.env",
  "written": ["DATABASE_URL", "PORT"],
  "kept": ["LOCAL_ONLY"],
  "withheld": ["API_SECRET"]
}
```

`written`, `kept`, `withheld`, and `warnings` are omitted when empty.

## env diff

Compare the local file with the remote variables, from the local file's point of view: what `push` would create, update, or (with `--prune`) delete.

| Flag            | Purpose                                        |
| --------------- | ---------------------------------------------- |
| `--show-values` | Print values instead of masking them.          |
| `--exit-code`   | Exit with status 1 when there are differences. |

```text
$ coolship env diff
Comparing /home/you/my-app/.env with regular variables of fenix-bot
+ NEW_KEY=********  (local only; push creates it)
~ PORT: local ********, remote ********
- OLD_KEY=********  (remote only; push --prune deletes it)
? API_SECRET  (remote value withheld; cannot compare)
3 unchanged
```

When nothing differs, the second line is `No differences (3 unchanged)` instead.

```json
{
  "target": { "…": "the resolved target" },
  "scope": "regular",
  "file": "/home/you/my-app/.env",
  "added": [{ "key": "NEW_KEY" }],
  "changed": [{ "key": "PORT" }],
  "removed": [{ "key": "OLD_KEY" }],
  "withheld": ["API_SECRET"],
  "unchanged": 3
}
```

`added` keys exist only locally, `changed` keys differ, `removed` keys exist only remotely, and `withheld` keys have a remote value Coolify hides; `unchanged` counts the rest. Values are masked unless `--show-values` is given: the text output shows `********` (or `(empty)`) in their place, and the JSON entries carry only `key` until `--show-values` adds `local` and `remote`. `--exit-code` makes a clean comparison exit `0` and any difference exit `1`, which is useful in CI.

## env push

Create and update remote variables from the local file, in one bulk request. Keys that exist only remotely are left in place unless `--prune` is given. A key whose remote value is withheld is overwritten only with `--force`. The plan is shown and confirmed first, or requires `--yes` when noninteractive. Changes take effect on the next deployment.

| Flag          | Purpose                                                 |
| ------------- | ------------------------------------------------------- |
| `--prune`     | Delete remote variables that are not in the local file. |
| `--force`     | Overwrite remote values Coolify withholds.              |
| `-y`, `--yes` | Push without confirmation.                              |

```json
{
  "plan": {
    "target": { "…": "the resolved target" },
    "scope": "regular",
    "file": "/home/you/my-app/.env",
    "create": [{ "key": "NEW_KEY", "local": "value" }],
    "update": [{ "key": "PORT", "local": "3000", "remote": "8080" }],
    "delete": [{ "key": "OLD_KEY", "remote": "value" }],
    "skipped": ["API_SECRET"]
  }
}
```

`skipped` lists withheld keys that were not forced. `push` has no `--show-values`; its JSON carries the values it pushed and deleted as they are, so treat that output as you would the file itself. `push` preserves each variable's literal, multiline, and shown-once flags, which the server would otherwise reset. Coolify creates a preview-scope twin of every regular variable `push` creates.

## Examples

```bash
coolship env pull                 # remote → .env, keeping local-only keys and comments
coolship env diff                 # what push would change, values masked
coolship env diff --show-values
coolship env diff --exit-code     # CI check: 1 when .env drifted from Coolify
coolship env push                 # create and update; asks first
coolship env push --prune --yes   # also delete remote-only keys, without asking
coolship env pull --preview --file .env.preview
```

## Exit codes

`0` on success (for `diff --exit-code`, when there are no differences); `1` when the request fails, or for `diff --exit-code` when there are differences; `2` when the directory is not linked, the file cannot be parsed, or `push` needs `--yes` noninteractively; `130` when the confirmation is cancelled.

---

# preview

Deploy the preview deployment Coolify already holds for a pull request, and observe it like deploy does.

URL: https://coolship.itrocas.com/docs/commands/preview

```text
coolship preview [flags]
```

`preview` submits a deployment of the pull-request preview Coolify holds for the linked application and observes it exactly like [`deploy`](https://coolship.itrocas.com/docs/commands/deploy). The pull request number comes from `--pr`, or from `GITHUB_REF` when running in a GitHub Actions `pull_request` workflow.

Coolify must already know the pull request: preview deployments must be enabled for the application, and the pull request added by Coolify's GitHub webhook or in its UI. This command cannot create a preview — the API offers no way to — and reports the server's answer when it does not know the pull request. See [Preview deployments](https://coolship.itrocas.com/docs/concepts/previews).

## Flags

| Flag                 | Purpose                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `--pr N`             | Pull request number (default: from `GITHUB_REF`).                                        |
| `--no-wait`          | Return after submission without observing completion.                                    |
| `--force`            | Force Coolify to rebuild without cache.                                                  |
| `--timeout DURATION` | Maximum time to wait for deployment completion (default `10m0s`).                        |
| `--logs`             | Stream the build log live above the checklist (terminal only).                           |
| `--no-logs`          | Keep the build log collapsed; it prints in full if the deployment fails (terminal only). |

In a terminal the deployment is shown as the same stage checklist as [`deploy`](https://coolship.itrocas.com/docs/commands/deploy), build log collapsed. `--logs` and `--no-logs` cannot be combined; without either, the `build_logs` preference decides; without that, the [verbosity](https://coolship.itrocas.com/docs/platform/output#verbosity) does: collapsed at normal, streamed with `--verbose` or `--debug`. Piped output and `--format json` are unchanged.

## Examples

```bash
coolship preview --pr 42
coolship preview                       # in a GitHub Actions pull_request job, reads GITHUB_REF
coolship preview --pr 42 --no-wait
```

When the server does not know the pull request:

```text
$ coolship preview --pr 999999
Error: server did not confirm a deployment for application mm4c0zpbrzx8z96t0qiw3tff: Pull request 999999 not found for this resource. (Coolify must already know the pull request: enable preview deployments and add it through its webhook or the UI)
```

## JSON

```json
{
  "target": { "…": "the resolved target, see the command reference" },
  "deployment_uuid": "gk2p8m1qz4vwx9c7n5b3ldrs",
  "pull_request": 42,
  "status": "finished",
  "url": "https://coolify.example.com/project/o8m4q2xk1vzr7b5n3c9jlwts/environment/x2n7vk4q9m1zrb8c5j3lwtp6/application/mm4c0zpbrzx8z96t0qiw3tff/deployment/gk2p8m1qz4vwx9c7n5b3ldrs",
  "url_kind": "deployment"
}
```

The shape is `deploy`'s with the `pull_request` number added. The application's URL is the production one, so a preview always ends with its deployment page in Coolify (`url_kind` is `deployment`), in human output too.

## Exit codes

`0` when the preview deployment finished (or was queued with `--no-wait`); `1` when Coolify does not know the pull request, or the deployment failed, was cancelled, or timed out; `2` when no pull request number is available or the directory is not linked; `130` when interrupted while waiting.

---

# dev

Run a local command in the application root with the linked application's runtime variables injected over your environment.

URL: https://coolship.itrocas.com/docs/commands/dev

```text
coolship dev [target] [-- command...] [flags]
```

`dev` runs a local process so that it sees what it would see on Coolify, without pulling a `.env` file first. A command after `--` runs directly. Without one, the binding's `dev` setting runs through your shell:

```toml title="coolship.toml"
[project]
dev = "npm run dev"
```

The application's **runtime** variables are injected over the inherited environment. Unlike `env pull`, shared references such as `{{team.API_KEY}}` are injected as the values they resolve to. Values Coolify withholds are reported and left to your own environment. The command's exit status becomes Coolship's, and Ctrl-C is forwarded to it.

## Flags

| Flag        | Purpose                                                         |
| ----------- | --------------------------------------------------------------- |
| `--preview` | Inject the preview-deployment scope instead of the regular one. |

## Examples

```bash
coolship dev -- npm run dev
coolship dev -- go run .
coolship dev api -- go run .            # named target in a monorepo
coolship dev                            # runs the binding's dev setting
coolship dev --preview -- npm test
```

## Behavior

* The process runs in the application root (`root` in the binding), whatever directory you invoked Coolship from.
* The injected pairs are layered over your environment: a variable set both locally and on Coolify takes Coolify's value.
* On cancellation the child receives an interrupt and is killed after a grace period if it does not exit.
* `dev` is the only command that resolves shared references; `env pull` keeps them as references so a push can never flatten them. See [Environment variables](https://coolship.itrocas.com/docs/concepts/variables).

## Output

`dev` prints nothing of its own on stdout; the child's streams are yours. Withheld variables are reported on stderr before the process starts. `--format json` does not change the child's output.

## Exit codes

The child's exit status. `2` when the directory is not linked or there is neither a command nor a `dev` setting; `1` when the variables cannot be read; `130` when interrupted before the child started.

---

# domain

Show the linked application's domains, or replace them with domain set.

URL: https://coolship.itrocas.com/docs/commands/domain

```text
coolship domain [target] [flags]
coolship domain set DOMAIN [DOMAIN...] [flags]
```

Coolify generates a domain from the application UUID until you set your own. `domain` shows the current list, one per line; `domain set` replaces it.

```text
$ coolship domain
https://coolship.example.com
```

## domain set

A bare host such as `app.example.com` means `https://app.example.com`. The change is shown and confirmed first, or requires `--yes` when noninteractive. Coolship refuses a domain Coolify sees in use elsewhere on the server unless `--force`, reads the application back to confirm what the server kept, and reminds you that the proxy learns the new domain on the next deployment.

| Flag                | Purpose                                                       |
| ------------------- | ------------------------------------------------------------- |
| `--redirect POLICY` | Redirect policy: `www`, `non-www`, or `both`.                 |
| `--force`           | Bypass Coolify's check that a domain is unused on the server. |
| `-y`, `--yes`       | Change without confirmation.                                  |

Docker Compose applications take per-service domains, which this command does not set; the server's refusal explains it.

## Examples

```bash
coolship domain
coolship domain api                                          # named target in a monorepo
coolship domain set app.example.com                          # bare host means https://
coolship domain set https://app.example.com https://www.example.com --redirect non-www
coolship domain set app.example.com --yes
```

## JSON

`domain`:

```json
{
  "target": { "…": "the resolved target, see the command reference" },
  "domains": ["https://coolship.example.com"],
  "generated": false
}
```

`generated` is `true` while the application still uses Coolify's automatic `<uuid>.<wildcard>` domain.

`domain set`:

```json
{
  "plan": {
    "target": { "…": "the resolved target" },
    "current": ["https://9f8e7d6c.coolify.example.com"],
    "domains": ["https://app.example.com", "https://www.example.com"],
    "redirect": "non-www"
  },
  "warnings": ["The proxy learns the new domains on the next deployment."]
}
```

## Exit codes

`0` on success; `1` when Coolify refuses the change (including a domain in use elsewhere without `--force`) or the server cannot be reached; `2` when the directory is not linked, a domain is malformed, or `--yes` is missing noninteractively; `130` when the confirmation is cancelled.

---

# login and logout

Save a Coolify instance URL and API token in the Coolify CLI configuration after verifying them, or remove a saved instance.

URL: https://coolship.itrocas.com/docs/commands/login

```text
coolship login [flags]
coolship logout NAME [flags]
```

`login` saves a Coolify instance URL and API token in the Coolify CLI configuration, which [`coolify-cli`](https://github.com/coollabsio/coolify-cli) shares, after verifying them against the server. Interactively, it asks for the URL, a context name, and the token, which is not echoed:

```text
$ coolship login
Coolify URL: https://coolify.example.com
Context name [coolify]: home
API token:
Logged in to home (https://coolify.example.com) as team Personal on Coolify 4.3.18, now the default
Saved to /home/you/.config/coolify/config.json
```

Noninteractively, pass `--url` and `--name` and pipe the token on stdin with `--token-stdin`. The token is never accepted as a flag, so it stays out of shell history and process listings. Create a token in Coolify under *Keys & Tokens* with the read, write, and deploy abilities.

## Flags

| Flag            | Purpose                                                   |
| --------------- | --------------------------------------------------------- |
| `--url URL`     | Coolify instance URL, e.g. `https://coolify.example.com`. |
| `--name NAME`   | Context name (default: derived from the URL's host).      |
| `--token-stdin` | Read the API token from stdin.                            |
| `--default`     | Make this the default context.                            |

`--coolify-config PATH` (global) writes to an explicit file instead of the default one.

## Examples

```bash
coolship login
echo "$TOKEN" | coolship login --url https://coolify.example.com --name ci --token-stdin
coolship login --url https://coolify.example.com --name work --default
coolship logout work
```

The first context saved becomes the default. A context with the same name is replaced, and the result says so.

## logout

`logout NAME` removes a context from the Coolify CLI configuration. The token remains valid on the server until you revoke it in Coolify.

## JSON

`login`:

```json
{
  "name": "home",
  "url": "https://coolify.example.com",
  "path": "/home/you/.config/coolify/config.json",
  "default": true,
  "server": "4.3.18",
  "team": "Personal",
  "replaced": false
}
```

`logout`:

```json
{
  "name": "work",
  "path": "/home/you/.config/coolify/config.json"
}
```

The token never appears in either.

## Exit codes

`0` when the context was saved or removed; `1` when the server rejects the token or cannot be reached; `2` when the URL is invalid, `--token-stdin` is missing noninteractively, or the context to remove does not exist.

---

# Deploy from CI

Use Coolship in GitHub Actions with COOLSHIP_URL and COOLSHIP_TOKEN, deploy on push, and deploy pull-request previews.

URL: https://coolship.itrocas.com/docs/guides/ci

In CI there is no interactive login and usually no Coolify CLI configuration file. Coolship takes its credentials from two environment variables instead, and reads the binding from the committed `coolship.toml`.

## Credentials

Add two repository secrets under *Settings → Secrets and variables → Actions*: `COOLSHIP_URL`, the instance URL, and `COOLSHIP_TOKEN`, a token with the read, write, and deploy abilities. Both are required together, and the pair cannot be combined with `--context` or `--coolify-config`. It overrides any context committed in `coolship.toml` for that invocation, and Coolship says so in a warning. See [Credentials](https://coolship.itrocas.com/docs/concepts/credentials).

## Deploy on push

```yaml title=".github/workflows/deploy.yml"
name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-production
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      COOLSHIP_URL: ${{ secrets.COOLSHIP_URL }}
      COOLSHIP_TOKEN: ${{ secrets.COOLSHIP_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      - name: Install Coolship
        run: |
          curl -fsSL https://raw.githubusercontent.com/joaomnuno/coolship/main/scripts/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Check the binding and the server
        run: coolship doctor

      - name: Deploy
        run: coolship deploy --timeout 20m
```

Notes:

* The installer puts `coolship` in `$HOME/.local/bin`; adding that directory to `GITHUB_PATH` makes it available to later steps. Pin a release with `COOLSHIP_VERSION=0.3.0` in the step's `env` if you want reproducible runs.
* Coolify deploys from its configured source and branch. The checkout step is there for `coolship.toml`; nothing from the runner's worktree is uploaded. Push before you deploy.
* `deploy` waits for the exact deployment it submitted and fails the job when it fails, is cancelled, or times out. The build log streams into the job output when the token may read it.
* `concurrency` keeps two pushes from racing. Coolship never retries an uncertain submission, so a cancelled job leaves at most one deployment queued, and its UUID is in the log.
* `CI` in the environment turns styling off, so logs stay plain.

## Preview a pull request

Enable *Preview Deployments* on the application in Coolify and let its GitHub webhook register pull requests; the API cannot create previews, so this step is not optional. See [Preview deployments](https://coolship.itrocas.com/docs/concepts/previews).

```yaml title=".github/workflows/preview.yml"
name: Preview

on:
  pull_request:

jobs:
  preview:
    runs-on: ubuntu-latest
    env:
      COOLSHIP_URL: ${{ secrets.COOLSHIP_URL }}
      COOLSHIP_TOKEN: ${{ secrets.COOLSHIP_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      - name: Install Coolship
        run: |
          curl -fsSL https://raw.githubusercontent.com/joaomnuno/coolship/main/scripts/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      # The pull request number is read from GITHUB_REF; --pr overrides it.
      - name: Deploy the preview
        run: coolship preview
```

When Coolify does not yet know the pull request — the webhook has not fired, or previews are disabled — the job fails with the server's explanation and what to do about it.

## Linking in CI

Commit `coolship.toml` so CI does not need to link. If a job must link a fresh checkout, supply every selector so no prompt is needed:

```bash
coolship link --project Personal --environment production --application fenix-bot
```

Names are matched exactly within their parent; use `--project-uuid`, `--environment-uuid`, or `--application-uuid` to pin identity. `link` only writes local configuration, never remote resources.

## Keeping variables honest

`coolship env diff --exit-code` exits `1` when a committed `.env.example`-style file (given with `--file`) has drifted from Coolify, with values masked, so a job can fail before a deployment ships with a missing variable. See the [environment-variable workflow](https://coolship.itrocas.com/docs/guides/environment-variables).

---

# Monorepos

Bind several Coolify applications in one repository with named targets, selected by directory or by name.

URL: https://coolship.itrocas.com/docs/guides/monorepos

A repository with several applications uses named `[apps.<name>]` targets in `coolship.toml` instead of the single `[project]` table. Each target has its own binding and its own root.

```toml title="coolship.toml"
version = 1

[apps.web]
context = "home"
project = "Personal"
environment = "production"
application = "frontend"
root = "apps/web"

[apps.api]
context = "home"
project = "Personal"
environment = "production"
application = "backend"
root = "apps/api"
```

## Link each target

Link each target from its own directory, which becomes its root:

```bash
cd apps/web && coolship link --target web
cd ../api  && coolship link --target api
```

`link --target NAME` proposes the working directory relative to the configuration root as the root, so linking from `apps/web` records `apps/web`. Adding a target keeps the existing ones and needs no confirmation. `--root` overrides the proposed root.

For an application that is not on Coolify yet, `init --target NAME` creates it with the current directory as its base directory and writes the same table.

## Select a target

Commands pick the target whose root most specifically contains the current directory, so `cd apps/api && coolship deploy` deploys the API. From anywhere, name it instead:

```bash
coolship deploy api
coolship logs web --follow
coolship status --target api
```

`status`, `deploy`, `logs`, `open`, `dev`, and `domain` take the name as a positional argument; every command takes `--target`. Two targets with the same root, or a directory outside every root, require an explicit name rather than a guess, and the error lists the choices.

`coolship config` shows which target a directory selects.

## Rules

* A file uses `[project]` **or** `[apps.<name>]`, never both.
* Target names match `[A-Za-z0-9][A-Za-z0-9_-]*`; `default` is reserved for the single form.
* Changing a target's binding, or converting between the two forms — which drops the other form's bindings — asks for confirmation or requires `--replace`, and the prompt says what would be dropped.
* Roots are validated against the configuration root, including symlinks, exactly as in the single form.
* Targets can point at different contexts, projects, or environments; each is resolved independently.

`dev` honors a per-target `dev` setting:

```toml
[apps.web]
# …
dev = "npm run dev"
```

This layout was verified live by linking a two-target monorepo against Coolify 4.3.18.

---

# Environment-variable workflow

Pull the application's variables into .env, edit locally, review the diff, push, and run locally with the real values.

URL: https://coolship.itrocas.com/docs/guides/environment-variables

This guide strings the [`env`](https://coolship.itrocas.com/docs/commands/env) and [`dev`](https://coolship.itrocas.com/docs/commands/dev) commands into a day-to-day sequence. The rules they follow are described under [Environment variables](https://coolship.itrocas.com/docs/concepts/variables).

## Pull

```bash
coolship env pull
```

The remote regular-scope variables are written into `.env` in the application root, created with private permissions. Keys that exist only locally are kept, comments and ordering are preserved, and a value Coolify withholds — a shown-once secret, or one your token may not read — is noted as a comment rather than written empty.

Do not commit the pulled file; it holds real values. Coolship never writes them anywhere else.

## Edit and review

Edit `.env` as usual, then look at what a push would do:

```bash
coolship env diff
coolship env diff --show-values      # only when you need to see them
```

Added keys would be created, changed keys updated, and remote-only keys deleted only if you later pass `--prune`. A `?` marks a key whose remote value is withheld; `push` overwrites it only with `--force`. A shared reference such as `{{team.API_KEY}}` compares as the reference, so the diff never suggests replacing it with a secret.

## Push

```bash
coolship env push
coolship env push --prune            # also delete remote-only keys
```

The plan is shown and confirmed before anything changes (`--yes` when noninteractive). Creates and updates go in one bulk request; each variable's literal, multiline, and shown-once flags are preserved. Changes take effect on the next deployment, so follow with `coolship deploy` when you want them live.

Coolify creates a preview-scope twin of every regular variable a push creates.

## Preview scope

Preview deployments read a separate copy of the variables. Every command acts on one scope at a time and never touches the other:

```bash
coolship env pull --preview --file .env.preview
coolship env diff --preview --file .env.preview
coolship env push --preview --file .env.preview
```

## Run locally without a file

`dev` injects the application's runtime variables over your environment for one process, so you do not need to pull at all:

```bash
coolship dev -- npm run dev
coolship dev --preview -- npm test
```

Unlike `pull`, `dev` resolves shared references to their values, because a local process needs them. Withheld values are reported and left to your own environment. The command's exit status becomes Coolship's.

## In CI

Keep a committed, value-free file (for example `.env.example`) and fail a job when it drifts from Coolify:

```bash
coolship env diff --file .env.example --exit-code
```

Values stay masked in the output, and the exit status is `1` when the two differ.

---

# Limits

What Coolship does not do, mostly because the Coolify API does not allow it, and how it behaves at those edges.

URL: https://coolship.itrocas.com/docs/platform/limits

Coolship is verified against Coolify 4.3.18 (see [Server compatibility](https://coolship.itrocas.com/docs/concepts/server-compatibility)). The limits below hold independently of the version; each one is reported rather than papered over.

## Deployments

* **Deployment states** are interpreted as `queued`, `in_progress`, `finished`, `failed`, and `cancelled-by-user`. An unknown state is shown as-is and waits for the timeout instead of being guessed as terminal.
* **An uncertain deployment submission is never retried automatically**, since the API defines no idempotency key. Coolship reports what it knows so you can recover manually.
* **Interrupting `deploy` does not cancel the remote deployment.** Local waiting stops and the UUID is reported.
* **Build logs can be withheld** by token ability and team role. Withheld data is reported as unavailable, never as empty data.
* **`deploy` does not upload your worktree** or push commits; Coolify deploys from its configured source and branch.

## Logs

* **`logs --follow` polls snapshots and compares overlapping lines.** The endpoint has no cursor, so log rotation, a container restart, or a gap larger than `--lines` can cause gaps or duplicates. Coolship reports the reset rather than pretending the stream is lossless.
* **Selecting a container is not supported**, because the server ignores the parameter the Coolify CLI sends for it and returns the first container.

## Previews

* **Coolship cannot create a preview.** The API only deploys a pull request Coolify already holds; previews are created by Coolify's GitHub webhook or in its UI. `preview` repeats the server's refusal with what to do about it.
* **Preview URLs are not readable through the API**, so `open` does not offer them.

## Creating applications

* **A private repository needs a source Coolify already holds.** `init` creates it through a GitHub App registered in Coolify or a deploy key (which `--create-deploy-key` can generate); it cannot install a GitHub App for you.
* **Compose domains are per service.** `init` sets them once with `--compose-domain`; `domain set` does not apply to a Compose application, so later changes are made in Coolify.
* **`init` cannot create an application from a Docker image**, only from a repository.

## Variables

* **Secret values can be withheld** (shown-once variables, or a token that may not read sensitive data). They are never invented, and `push` overwrites a withheld value only with `--force`.
* **Shared references are never flattened** by `pull`, `diff`, or `push`; only `dev` resolves them.
* **Changes made by `push` take effect on the next deployment.**

## Resolution

* **Pagination is not assumed.** A `Link` header advertising a next page is refused rather than treated as a complete candidate list.
* **Names are matched exactly** within their parent; an ambiguous or missing selection is an error, never a guess. A pinned UUID that disappears is reported rather than replaced.

## Scope

Coolship is not a replacement interface for every Coolify API resource. Server, private-key, and team administration belong in a general-purpose tool such as [`coolify-cli`](https://github.com/coollabsio/coolify-cli). The install script supports Linux and macOS; on Windows, build from source or use WSL.

---

# Output and exit codes

Which stream carries what, when output is styled, what --format json prints, and what each exit code means.

URL: https://coolship.itrocas.com/docs/platform/output

## Streams

Results go to stdout; prompts, progress, and diagnostics go to stderr, so piping stdout stays useful. `coolship open --print | pbcopy` copies only the URL, and `coolship deploy > result.txt` keeps the status lines and build log on the terminal while the final result lands in the file.

## JSON

`--format json` prints one result object per command. The shapes are documented on each command page, and every result that names an application carries the same `target` object (see the [command reference](https://coolship.itrocas.com/docs/commands)). `logs --format json` prints newline-delimited events, one object per line, for a single snapshot and when following. JSON mode never mixes human progress into stdout, and JSON output is never styled.

Values that may be secrets are masked in JSON as well, unless `--show-values` is given. The token never appears in any output; at debug verbosity only its last four characters are shown.

## Styling

Human output is colored only when the stream it goes to is a terminal, and each stream is decided on its own, so `coolship status | cat` prints plain text while a prompt on the terminal is still styled. Styled elements are `doctor` markers, deployment statuses, `Warning:` and `Error:` prefixes, prompt questions, `env diff` markers, and key/value labels, using plain ANSI sequences.

Set [`NO_COLOR`](https://no-color.org) or pass `--no-color` to turn styling off everywhere; `TERM=dumb` and `CI` do the same. JSON output and the server's logs are never styled.

## Verbosity

A run shows one of three levels. **Normal** is the default; for now, results name applications and deployments with their UUIDs at every level. **Verbose** adds one line on stderr for every request Coolship sends to Coolify: the method, the URL, the status (or `no response`), the time taken, and `(retry N)` for a retried read. **Debug** adds each request's and response's headers and bodies under that line, curl-style, with the token masked to its last four characters and control characters removed.

```text
$ coolship status --verbose
GET https://coolify.example.com/api/v1/applications/mm4c0zpbrzx8z96t0qiw3tff 200 OK 84ms
…
```

The level comes from the first of these that is set:

1. `--debug`, then `--verbose` (both long-only; `-v` is the version);
2. `COOLSHIP_VERBOSITY=normal|verbose|debug`, for CI; any other value is invalid input;
3. the `verbosity` key of the [preferences file](https://coolship.itrocas.com/docs/commands/config#preferences);
4. normal.

Verbose and debug output goes to stderr only, so stdout and `--format json` are the same at every level. Above normal, `deploy`, `start`, `restart`, and `preview` stream the build log unless `--no-logs` or the `build_logs` preference says otherwise (then it is held back and printed in full only if the deployment fails), and no spinner or stage checklist is drawn, so the request lines are never overwritten; the plain stage and status lines print instead, each finished stage with its duration and each status line with the time since the deployment was queued. Debug output shows request and response bodies in full, with only the token masked, so it includes the variable values `env set` and `env push` send and `env pull` returns, and any other secret a request or response carries; keep it out of shared CI logs.

## Prompts

Commands that change something show their plan and ask for confirmation on stderr. When stdin is not a terminal there is no prompt: pass `--yes` (`--replace` for `link`) or the command refuses. `link` and `init` prompt for a choice only when it is genuinely ambiguous; `q` cancels a selection.

## Exit codes

| Code  | Meaning                                     |
| ----- | ------------------------------------------- |
| `0`   | Success.                                    |
| `1`   | The operation failed.                       |
| `2`   | Invalid input, configuration, or selection. |
| `130` | Interrupted.                                |

Errors are printed once, as a single `Error:` line on stderr, and classified into these codes at the process boundary. Some commands give a code a specific meaning: `doctor` exits `1` when any check fails, `env diff --exit-code` exits `1` when there are differences, and `dev` exits with the child process's status.

---

# Docs for AI assistants

This documentation is published as Markdown too, with an llms.txt index and a Markdown rendition of every page, so assistants and agents can read it directly.

URL: https://coolship.itrocas.com/docs/platform/ai

Every page of this site is generated from Markdown, and the same Markdown is served next to the HTML.

## llms.txt

[`/llms.txt`](https://coolship.itrocas.com/llms.txt) is an index in the [llms.txt](https://llmstxt.org) format: a title, a summary, and every page grouped by section with a one-line description and an absolute URL. Point an assistant at it when you want it to choose which pages to read.

[`/llms-full.txt`](https://coolship.itrocas.com/llms-full.txt) is the whole documentation in one Markdown file, in sidebar order, for assistants that would rather read everything at once.

## One page as Markdown

Append `.md` to any page URL to get that page as Markdown, with its title, description, and URL at the top:

```text
https://coolship.itrocas.com/docs/commands/deploy.md
https://coolship.itrocas.com/docs/index.md
```

A request for the page URL itself that prefers Markdown gets the same text, so tools that send `Accept: text/markdown` need no special URL:

```bash
curl -H 'Accept: text/markdown' https://coolship.itrocas.com/docs/commands/deploy
```

Callouts, cards, and steps are rendered as plain Markdown; code blocks and tables are unchanged, and links are absolute.

## Page actions

Below the title of every page:

* **Copy as Markdown** puts the page's Markdown on the clipboard.
* **View as Markdown** opens the `.md` rendition of the page.
* **Open in Claude** and **Open in ChatGPT** start a conversation with a prompt that asks the assistant to read the page's Markdown before answering.
* **Edit on GitHub** opens the source file.

## For agents using the CLI

The CLI's own `--help` text is the source of the [command reference](https://coolship.itrocas.com/docs/commands), and `--format json` gives every command a stable, documented shape; see [Output and exit codes](https://coolship.itrocas.com/docs/platform/output). Mutations confirm interactively and require `--yes` (or `--replace` for `link`) when there is no terminal, so an agent must pass those flags explicitly rather than expect a prompt.
