Content
<div align="center">
<h1><img src="product/dashboard/public/logo/banner.png" alt="Cubeship — Your infrastructure. Ready to ship." width="960"></h1>
**A PaaS you run on your own server.** `docker push`, and it is live —
with HTTPS, a database beside it, and a second machine when one stops
being enough.
[Install](#install) · [First five minutes](#first-five-minutes) ·
[Deploy an app](#deploy-an-app) · [Databases](#databases-and-storage) ·
[Backups](#backups) · [More machines](#more-machines) · [MCP](#mcp) ·
[Changelog](CHANGELOG.md)
<a href="https://github.com/sponsors/lucasaarch"><img src=".github/assets/sponsor.svg" alt="Sponsor lucasaarch" width="114" height="32"></a>
</div>
---
## Contents
- [What you get](#what-you-get)
- [Install](#install)
- [What it needs](#what-it-needs)
- [Installing your own build](#installing-your-own-build)
- [First five minutes](#first-five-minutes)
- [Claim the instance](#claim-the-instance)
- [Give it a domain](#give-it-a-domain)
- [Deploy an app](#deploy-an-app)
- [Push an image](#push-an-image)
- [Run an image from somewhere else](#run-an-image-from-somewhere-else)
- [Build from a Git repository](#build-from-a-git-repository)
- [Give it a name on the internet](#give-it-a-name-on-the-internet)
- [Reach it from another app](#reach-it-from-another-app)
- [Databases and storage](#databases-and-storage)
- [Backups](#backups)
- [More machines](#more-machines)
- [Limits and autoscaling](#limits-and-autoscaling)
- [The CLI](#the-cli)
- [MCP](#mcp)
- [Upgrading](#upgrading)
- [Uninstalling](#uninstalling)
- [What reaches the internet](#what-reaches-the-internet)
- [Everything else](#everything-else)
- [License](#license)
## What you get
One command on a fresh VPS, and the box is running:
| | |
| --- | --- |
| **A dashboard** | at your instance's address, over HTTPS |
| **A registry** | `docker push` to it and the app deploys |
| **Zero-downtime deploys** | the new container has to look healthy before the old one goes |
| **Certificates** | Let's Encrypt, renewed for you, nothing to configure |
| **Databases** | Postgres, MySQL, MariaDB, Redis, MongoDB — one click, wired into the app's environment |
| **Object storage** | a MinIO on the box, or your own S3 bucket |
| **Backups** | every database dumped on a schedule into a bucket off the machine, and restored from the same screen |
| **Builds** | from a Dockerfile, or from a repository with no Dockerfile at all |
| **More machines** | add a second server and apps spread across both |
| **Charts** | what every container is using, and what the machine underneath is doing |
| **A shell** | a terminal inside any app's container, or as root on any server — in the browser or from the CLI, with no SSH port open |
| **An API, a CLI and MCP** | everything the dashboard does, scriptable — and an endpoint an agent drives directly |
| **Templates** | recipes for apps and the databases they need, published by the community at [cubeship.dev/templates](https://cubeship.dev/templates) |
Everything Cubeship runs is a container, the daemon included: Postgres,
the registry, the proxy, the builder and every app of yours are its
siblings, with administrative services on a separate network from apps.
Nothing else is installed on the host.
## Install
On the server, as root:
```bash
curl -fsSL https://cubeship.dev/install.sh | sh
```
It installs Docker if the box has not got it, pulls two images, starts
the daemon, and prints the address to open and the token to claim it
with.
It is [one file](install.sh) — read it first if you would rather not
pipe a script into a shell, which is a reasonable thing to prefer.
**It installs an exact release**, not a moving tag: it asks which
release is newest and pins that number, so running the same command
again tomorrow gives you the same thing. To choose:
```bash
curl -fsSL .../install.sh | sh -s -- --version 0.1.0
```
A version with a `-rc.1` on it is a release candidate. They are
published like any other release and are never what you get by default.
### What it needs
- **Linux**, x86-64 or arm64. Debian and Ubuntu are what the installer
knows how to put Docker on; on anything else, install Docker first and
the script will use it.
- **Ports 80 and 443** free, for the proxy and for certificates.
- **Port 3000** free on loopback, for recovery through an SSH tunnel.
The installer refuses rather than fighting whatever is already there.
- Root, because it installs Docker and writes to `/var/lib/cubeship`.
### Installing your own build
To run your own code instead of a published release, put the repository
on the server and build there. The build runs inside Docker, so the
server needs no Go and no Node:
```bash
git clone https://github.com/cubeshipd/cubeship && cd cubeship
sudo ./install.sh --local
```
## First five minutes
### Claim the instance
Open the HTTPS address the installer printed and create the first
account. It asks for the **setup token**, which the installer printed
and which is also in `/var/lib/cubeship/setup-token`.
If the domain or certificate is unavailable, run this on your computer:
```bash
ssh -L 3000:127.0.0.1:3000 root@<your-server>
```
Keep the tunnel open and visit `http://localhost:3000`. The recovery port
is bound to the server's loopback interface.
That token is the point: without it, whoever reaches the page first is
the admin of your machine. With it, claiming the instance takes access
to the host, which is what it always meant to require. The file is
deleted the moment setup succeeds.
The first account is an admin, and it is the only account setup ever
makes. Everyone after is invited from **Account → Users**.
### Give it a domain
Point a name at the server's IP and set it in **Settings**. From then on
the instance answers there over HTTPS, and so does everything you deploy.
**You do not need one to start.** With no domain, Cubeship gives itself
an [sslip.io](https://sslip.io) address — a name that resolves to your
IP without anything being registered anywhere — so certificates and app
names work on a box you set up five minutes ago.
## Deploy an app
Apps live in an environment inside a project: `myproject/production/api`
is the whole of an app's name, and it is also its path in the registry.
Create one from the dashboard, or:
```bash
cubeship app create api --project myproject
```
An app is created **empty** and deploys anyway. It has no name on the
internet until you give it one, which is a normal thing to be: a worker
or a queue consumer has no business answering the internet.
### Push an image
The push *is* the deploy. Nothing else to press:
```bash
docker login registry.example.com # your API key as the password
docker push registry.example.com/myproject/production/api:latest
```
`cubeship app get api` prints the exact path to push to.
### Run an image from somewhere else
Docker Hub, GHCR, DigitalOcean, ECR — anything you already publish to.
Nothing tells Cubeship when you push there, so a deploy is something you
ask for:
```bash
cubeship app create api --project myproject --source external --image nginx
cubeship app deploy api --tag 1.27
```
The image is given without a tag: which tag runs is a deploy's argument,
so an app pinned to one could never be told to run another.
This is the one that needs nothing configured at all: no domain, no
certificate, no registry. It works the minute the installer finishes.
### Build from a Git repository
Cubeship builds it on the box, from a Dockerfile you wrote or — with no
Dockerfile at all — by reading the code and working the build out.
Connect a GitHub account in **Git providers** and a push deploys it.
### Give it a name on the internet
Add a domain to the app and point a DNS record at the server. Cubeship
gets the certificate. Under an sslip.io address it already resolves, so
the name works the moment you add it.
An app can answer at several names, and **each name carries its own
port** — one image exposing an API and an admin panel is two names.
### Reach it from another app
Inside the instance every app answers at
`cubeship-<project>-<environment>-<app>`, on the port it listens on. It
is on the app's **Network** tab, and it is the address to put in another
app's environment: it survives deploys, spreads over every copy of a
scaled app, and means the same thing from every machine in the cluster —
so a worker with no domain at all is still reachable.
The public name cannot do that job from inside the box. The request
leaves for a DNS record pointing back at the same machine, and a host
that does not route its own traffic back in answers nothing, which reads
as the other app being down.
## Databases and storage
A database belongs to the **instance**, not to a project: on one box the
usual shape is a single Postgres serving several small apps, and those
apps are routinely in different projects.
```bash
cubeship db create pg --engine postgres
cubeship db attach pg --app myproject/production/api
```
The app receives `DATABASE_URL` and its parts in its own environment
from its next deploy — nothing to copy, and no credential passing
through your hands. A second database on one app takes a prefix.
Object storage works the same way: a MinIO Cubeship runs, or an S3
bucket you already have. An attached app gets `S3_ENDPOINT`,
`S3_BUCKET`, the keys and the rest.
## Backups
Postgres, MySQL, MariaDB and MongoDB are dumped the way their own tools
do it, on a schedule or on demand, from the **Backups** tab of the
database's settings.
- **A time of day, a timezone, and how many to keep.** Retention counts
only the dumps that worked: a week of failures never pushes out the
last good one.
- **Into a bucket, off the machine.** Link an object store and the dump
streams straight into it without landing on this box's disk — a
database larger than the disk under it is still a backup. With nothing
linked it is written locally, which is a copy on the same disk as the
database, and every screen showing one says so.
- **Restored from the same screen**, into the database it came from,
after typing that database's name.
- **Kept when the database is deleted**, which is the moment they
matter. **Backups** under Platform says what every database is covered
by, worst first, and holds the dumps whose database is gone.
Redis is not in the list, on purpose: it is a cache and a queue on a box
this size, and there is no good answer to what a nightly copy of one
would be restored *to*.
**The instance backs itself up the same way** — its own Postgres, with
every account, project, app, credential and attachment in it, plus the
certificate store and the project pictures — under **Settings →
Backups**. There is no button to put that one back, because the thing it
would replace is the database the button runs on: restoring an instance
is a fresh install, the daemon stopped, the dump loaded and the files
put in place, and the screen says so.
## More machines
Add a server, and the instance becomes a cluster:
```bash
cubeship server add eu-1
```
It prints the command to run on the new box, address and credential
already in it. The new machine **dials home** and nothing dials it: it
publishes no port to the internet, serves no dashboard and needs no
certificate of its own.
The machines share a private network, encrypted, and container names
mean the same thing on every one of them. That network is the part that
needs them to reach **each other**: Cubeship opens the three cluster
ports on each machine, restricted to the other machines' addresses. So a
box behind NAT can report in and be told what to run, and cannot be on
that network — which is what an app on it would need to reach a database
on another machine.
**Every name still arrives at your instance**, whichever machine runs
the app. One DNS record, one certificate store, and moving an app
between machines touches neither.
```bash
cubeship app place api --on eu-1 # move it
cubeship app place api --replicas 4 # four copies, spread
cubeship app place api --everywhere # and on every machine that joins
```
Scaling takes effect at once, in both directions.
**A shell on any of them, with no SSH.** An admin opens a root terminal on
a machine from **Servers**, and anyone with the grant opens one inside an
app's container from its **Shell** tab. A worker connects the session back
to the control plane, so it still opens no port:
```bash
cubeship server shell eu-1 # root on the machine
cubeship app shell api # inside the app's container
```
## Limits and autoscaling
Cap what one copy of an app — or a database, or a store — may take:
```bash
cubeship app limits api --cpu 1 --memory 512Mi
```
**Changing a limit restarts nothing.** It is the one part of a running
container Docker can change, so raising an app's memory is a request
rather than a redeploy.
Or hand the replica count over entirely:
```bash
cubeship app autoscale api --min 2 --max 8 --cpu 70
```
It works from the average CPU across the app's copies — the number on
the app's own chart, where 100 is one core. A maximum is required:
without one, a loop of requests is a loop of replicas.
## The CLI
`cubeship` talks to the instance's API from your machine.
```bash
cubeship login https://cubeship.example.com <your-api-key>
cubeship app list
cubeship app logs api
cubeship app shell api
cubeship app env set api DATABASE_POOL=10
```
Make an API key under **Account** in the dashboard. Every noun has
`--help`, and every command it can run is something the dashboard can
too — they are the same API.
**Installing it.** It is one static binary, attached to every
[release](https://github.com/cubeshipd/cubeship/releases) for macOS and
Linux, Intel and ARM:
```bash
curl -fsSL https://github.com/cubeshipd/cubeship/releases/latest/download/cubeship_darwin_arm64.tar.gz | tar -xz
sudo mv cubeship /usr/local/bin/
```
Swap `darwin_arm64` for `darwin_amd64`, `linux_amd64` or `linux_arm64`.
That name carries no version, so the command keeps working after the next
release; the same tarball is also attached under its version — which is
what to ask for when you want a specific one — and `checksums.txt` is
beside them.
There is no `go install`: this module is named `cubeship` rather than the
path it lives at, and a downloaded binary covers the same ground.
## MCP
`https://your-instance/mcp` is the same API for an agent, authenticated
with the same key. Point Claude Code, Cursor or whatever you run at it:
```json
{
"mcpServers": {
"cubeship": {
"type": "http",
"url": "https://cubeship.example.com/mcp",
"headers": { "Authorization": "Bearer <your-api-key>" }
}
}
}
```
From there it can create a project, deploy an app, read the logs and how
the last deploy ended, attach a database and set the variables — the
work of the dashboard, without the dashboard. It is stateless on
purpose: every call carries the key, so nothing an agent opened can be
picked up by another.
Two lines are deliberately not crossed: **no tool reads a secret, and no
tool sets a container's ceiling.** Those stay with a person.
## Upgrading
Run the installer again. It pulls the newest release and replaces the
containers; nothing under the data directory is touched.
```bash
curl -fsSL https://cubeship.dev/install.sh | sh
```
The dashboard shows you what changed the next time you open it. What is
in each release is in the [changelog](CHANGELOG.md).
## Uninstalling
```bash
sudo ./uninstall.sh # the containers; your data is kept
sudo ./uninstall.sh --purge # and the data, permanently
```
The default is deliberately not the destructive one: removing the
software is not the same as asking to lose your database. Installing
again brings the same instance back.
## What reaches the internet
**There is no telemetry.** Nothing reports what you deploy, how much you
deploy, or that this instance exists. There is no analytics in the
dashboard and no account with anybody.
Three things leave the box, and all three are yours to look at:
- **Installing and upgrading** pulls two images from `ghcr.io`, and asks
GitHub which release is newest so it can pin an exact one.
- **The running daemon asks GitHub which releases exist** — when an admin
opens the update screen, and when the automatic update timer comes
round. It is a plain `GET` on the public releases API with no
credential and nothing about your instance in it
([the whole of it](product/internal/update/releases.go)). An instance that
cannot reach GitHub simply never offers an update and says so, rather
than claiming to be current.
- **Let's Encrypt**, for certificates — and only once you have given the
instance a domain.
After that it does what you ask it to: pull the images you named, clone
the repositories you connected, write the DNS records you configured.
A build with no version stamped on it — which is what `make dev` is —
never checks for a release at all.
## Everything else
- **API reference** — `https://your-instance/docs`, and the OpenAPI
document at `/openapi.json`.
- **Working on Cubeship itself** — [CONTRIBUTING.md](CONTRIBUTING.md).
- **Found a security problem?** [SECURITY.md](SECURITY.md) — report it
privately rather than in an issue.
## License
[Apache-2.0](LICENSE). Use it, run it, modify it, run your company on it,
sell what you build on top of it — the same terms
[Coolify](https://github.com/coollabsio/coolify) is under, and a patent
grant comes with it.
The name is the one thing that is not in the grant — Apache-2.0 section 6
excludes trademarks, deliberately — and
[TRADEMARK.md](TRADEMARK.md) says what that means: a public fork gets its
own name.
Connection Info
You Might Also Like
No recommendations available