> ## Documentation Index
> Fetch the complete documentation index at: https://docs.airshipci.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-hosting Airship

> Run the control plane on your own infrastructure: one container, one Postgres database, and a GitHub App of your own.

Airship's control plane is one container plus Postgres. If "whoever
administers the hosted service can see our job metadata" is not an acceptable
answer, run it yourself: the same code, your infrastructure, your sealing key.

```mermaid theme={null}
flowchart LR
    B["browsers"] --> W["dashboard<br/>app.example.com"]
    GHW["GitHub webhooks"] --> A["api × 2+<br/>api.example.com"]
    R["runner machines<br/>(cache credentials)"] --> A
    W --> A
    A --> DB[("Postgres")]
    A --> GH["api.github.com"]
    A --> C["your customers' clouds"]
```

## Two different clouds

These pages are about where **Airship itself** runs. Where **runner machines**
run is a separate choice, made by connecting a credential — see [Connecting a
cloud](https://docs.airshipci.com/clouds/overview.md). The two are independent: a control plane on Cloud
Run can launch EC2 instances, and the reverse.

> [!NOTE]
>
> The control plane needs **no cloud permissions of its own**. It launches
> machines with the credentials you connect in the dashboard, never with the
> identity of the platform it runs on.

## What you need

| | |
| - | - |
| The API container | Serves the HTTP API, receives webhooks, launches and tears down machines |
| Postgres **17 or newer** | 18 recommended. The single point of truth |
| The dashboard | A Next.js app, served as a static-ish site or a Node server |
| A GitHub App of your own | [Creating it](https://docs.airshipci.com/self-hosting/github-app.md) |
| Two hostnames on one site | `app.example.com` and `api.example.com`, so the session cookie is sent |
| TLS on the API | `PUBLIC_URL` must be https — runner machines fetch the cache proxy and its credentials from it |

## The shape of a deployment

**1. Create the GitHub App**

Callback URL, webhook URL, permissions and event subscriptions. See
[GitHub App](https://docs.airshipci.com/self-hosting/github-app.md).

**2. Provision Postgres and run the migration**

The image ships a `migrate` entrypoint. Run it once per release, before
the new API starts.

```bash theme={null}
docker run --rm --entrypoint migrate \
  -e DATABASE_URL=postgres://... airship-api up
```

**3. Generate a credentials key and keep it safe**

```bash theme={null}
openssl rand -base64 32
```

This key is the only thing that can open the sealed cloud credentials.
**Losing it loses every connected cloud.** Back it up as you would a root
key. [Configuration](https://docs.airshipci.com/self-hosting/configuration.md#credentials-key)

**4. Run the API, and the dashboard pointed at it**

Several API replicas are fine: launchers claim runners with row locks, and
the reconciler elects a leader through a database advisory lock.

**5. Install your app and connect a cloud**

From then on it is the ordinary [Quickstart](https://docs.airshipci.com/quickstart.md).

## Running it

```bash theme={null}
docker build -t airship-api apps/api
docker run --rm --entrypoint migrate -e DATABASE_URL=... airship-api up
docker run -p 8080:8080 \
  -e DATABASE_URL=... \
  -e PUBLIC_URL=https://api.example.com \
  -e DASH_URL=https://app.example.com \
  -e CORS_ORIGINS=https://app.example.com \
  -e CREDENTIALS_KEY=... \
  airship-api
```

API documentation is served at `/docs` on the API itself, and `/healthz`
answers for your load balancer.

> [!WARNING]
>
> The image embeds the [cache proxy](https://docs.airshipci.com/performance/caching.md) for `linux/amd64`
> and `linux/arm64`. A build of the API alone leaves it out, and cache-enabled
> runners then run their jobs without acceleration — recorded as
> `cache.unavailable` on **Events** rather than failing.

## Where to run it

> **[Deployment guides](https://docs.airshipci.com/self-hosting/deploying.md)**
>
> Container platform plus managed Postgres, or a single server with Docker
> Compose and automatic TLS.

> **[Configuration reference](https://docs.airshipci.com/self-hosting/configuration.md)**
>
> Every environment variable, what is required, and what must never be set
> in production.

## What it does not need

| | |
| - | - |
| Cloud permissions | None. It uses the credentials you connect |
| Inbound access to runners | None. Runners connect out |
| Persistent disk | None. State is in Postgres |
| A queue or a cache | None. Postgres only |
| Sticky sessions | No. Sessions are rows, not memory |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.