Setting up a server
Every cornus command talks to a server. This page has one short runbook per arrangement the server can be in — what it needs, the command that starts it, and how to confirm it worked. cornus setup links straight into the section for the arrangement you picked.
These are runbooks, not references. Each one ends with a link to the page that covers its subject exhaustively; follow it when you need the full flag, value, or capability list.
Which arrangement?
The server is one process. What changes between arrangements is where it runs and which runtime it drives (deploy backends).
| You want to | Arrangement | cornus setup scenario |
|---|---|---|
| Try Cornus with the least setup | Local, Docker | local |
| Run without a Docker daemon | Local, containerd or bare | local |
| Run without any daemon at all | Local, bare | local |
| Use Incus instances | Local, Incus | local |
| Deploy into a cluster from your machine | Local, Kubernetes | local |
| Use a beefier build/deploy host | Remote host over SSH | ssh-* |
| Run Cornus in the cluster it deploys to | In the cluster | kube-port-forward, kube-url |
| Keep the host clean | As a container | docker-container, containerd-container, incus-container |
| Use a server someone else runs | Nothing to set up | url |
Two rules apply everywhere:
- The build engine needs privilege. It uses runc, overlayfs, and user namespaces, so run the server as root/privileged, or use
cornus serve --rootless. See privilege posture. - Check before you commit.
cornus daemon preflightruns the same host checkscornus serveruns at startup and exits non-zero on a configuration it would refuse. Every runbook below uses it.
Local server
Cornus runs on your machine. Its data directory holds the registry CAS and the build cache — pass --data-dir (or CORNUS_DATA) to keep them across restarts.
Get the binary first: installation.
Docker
The default, and the least to arrange.
Needs: the Docker socket, /var/run/docker.sock.
cornus daemon preflight # verify the host first
cornus serve --data-dir ~/.local/share/cornusCheck: cornus health prints nothing and exits 0 when the server is up.
More: dockerhost backend.
Podman
Cornus drives Podman through its native libpod API, not Podman's Docker-compatibility endpoints.
Needs: the podman API socket — and cornus must be told which one. Nothing is inferred: with neither variable set, the server refuses to start rather than guessing which daemon you meant.
# Rootless (the usual desktop and CI shape)
systemctl --user enable --now podman.socket
export CORNUS_PODMAN_SOCKET="$XDG_RUNTIME_DIR/podman/podman.sock"
# ...or rootful
sudo systemctl enable --now podman.socket
export CORNUS_PODMAN_SOCKET=/run/podman/podman.sock
CORNUS_DEPLOY_BACKEND=podman cornus daemon preflight
CORNUS_DEPLOY_BACKEND=podman cornus serve --data-dir ~/.local/share/cornusEnabling podman.socket does not turn Podman into a daemon: the unit is socket-activated, so the service starts on demand and exits when idle.
If you would rather not enable a socket unit at all, cornus can run the service itself — this needs only the podman binary on PATH:
CORNUS_DEPLOY_BACKEND=podman CORNUS_PODMAN_SERVICE=1 cornus serveA remote podman is reachable over SSH, using the destination podman system connection already stores:
export CORNUS_PODMAN_SOCKET="ssh://core@host/run/user/1000/podman/podman.sock"Rootless caveat: a rootless workload's network namespace is not routable from the host, so cornus port-forward and cornus tunnel cannot reach it by dialing. Set CORNUS_PODMAN_REMOTE=1 to reach the workload through a per-instance companion instead; that mode also needs CORNUS_AGENT_IMAGE and CORNUS_ADVERTISE_URL. Deploy, logs, and exec work either way, and a rootful podman has none of this.
Check: cornus health.
More: podman backend.
containerd
No dockerd, but still a daemon.
Needs: root, a containerd socket, and the CNI plugins (bridge, portmap, host-local, loopback) in /opt/cni/bin.
sudo CORNUS_DEPLOY_BACKEND=containerd cornus daemon preflight
sudo CORNUS_DEPLOY_BACKEND=containerd cornus serve --data-dir /var/lib/cornusCheck: cornus health.
More: containerd backend.
Bare, with no daemon
Cornus drives an OCI runtime itself and is its own supervisor — no dockerd, no containerd.
Needs: root, an OCI runtime on PATH, and the same CNI plugins. runc is the default; CORNUS_BARE_RUNTIME selects crun, youki, or runsc (gVisor).
sudo CORNUS_DEPLOY_BACKEND=bare cornus daemon preflight
sudo CORNUS_DEPLOY_BACKEND=bare cornus serve --data-dir /var/lib/cornusA missing runtime fails fast at startup with an actionable error, rather than at first deploy.
Run this one under systemd
On bare, cornus is the workload supervisor — it waits on each container's PID 1 and applies the restart policy itself (CORNUS_BARE_SHIM would detach that into per-container shims, but it is off by default). A server you start in a terminal therefore takes workload supervision with it when it exits, and the startup reconcile that reattaches survivors and rebuilds after a host reboot only runs when cornus runs. Restart=on-failure plus WantedBy=multi-user.target are what make workloads outlive a crash and a reboot.
The other backends delegate supervision to their daemon or the cluster, so losing cornus loses the API and not the workloads — a foreground cornus serve stays a reasonable dev loop there.
cornus setup offers a matching cornus.service for this arrangement; take it rather than composing one by hand.
Check: cornus health.
More: bare backend.
Incus
Workloads become Incus application containers.
Needs: incusd 6.3+ (earlier releases have no OCI support), access to its socket, and skopeo + umoci on the daemon host — incusd shells out to them to flatten the image, so they are needed where incusd runs.
CORNUS_DEPLOY_BACKEND=incus cornus daemon preflight
CORNUS_DEPLOY_BACKEND=incus cornus serve --data-dir ~/.local/share/cornusCORNUS_INCUS_SOCKET (default /var/lib/incus/unix.socket) and CORNUS_INCUS_PROJECT (default default) override the target.
Check: cornus health.
More: incus backend.
Kubernetes, from your machine
The server runs locally and deploys into a cluster your kubeconfig reaches — k3s, kind, minikube, or a remote one. No local container runtime is involved.
Needs: a reachable cluster (KUBECONFIG, else ~/.kube/config) and RBAC to manage Deployments and Services in CORNUS_K8S_NAMESPACE (default default).
CORNUS_DEPLOY_BACKEND=kubernetes cornus daemon preflight
CORNUS_ADVERTISE_REGISTRY=192.0.2.10:5000 \
CORNUS_DEPLOY_BACKEND=kubernetes cornus serve --data-dir ~/.local/share/cornusThe nodes pull the image, not you
CORNUS_ADVERTISE_REGISTRY is not optional here. The cluster's nodes pull built images from this server's registry themselves, so an address like 127.0.0.1:5000 resolves on the node to the node — and every deploy fails pulling an image that is sitting on your machine. Set it to an address the nodes can reach.
Cornus is primarily meant to run inside the cluster, where the registry is a service endpoint the nodes reach by construction and this problem does not arise. Prefer in the cluster unless you specifically want the server local.
More: kubernetes backend.
Remote host over SSH
The server runs on another machine; your CLI reaches it through an SSH tunnel that binds no local port. Which runtime it drives is decided there, with CORNUS_DEPLOY_BACKEND — the tunnel itself is backend-agnostic.
The shape is the same for all four:
- Install the cornus binary on the remote host (installation).
- Satisfy that backend's prerequisites (below).
- Verify:
ssh HOST '<env> cornus daemon preflight'. - Run it bound to loopback — the tunnel exits on the host, so its own loopback reaches the server:
ssh HOST '<env> cornus serve --addr 127.0.0.1:5000'. - Configure your side:
cornus setup --scenario ssh-<backend>.
Step 4 deserves a systemd unit rather than a shell. cornus setup generates a correct cornus.service for the backend you chose, including its prerequisites as comments — take it instead of composing one by hand.
Docker
Needs on the host: the Docker socket. Env: none (the default).
ssh HOST 'cornus daemon preflight'
cornus setup --scenario ssh-dockercontainerd
Needs on the host: root, a containerd socket, CNI plugins in /opt/cni/bin. Env: CORNUS_DEPLOY_BACKEND=containerd.
ssh HOST 'sudo CORNUS_DEPLOY_BACKEND=containerd cornus daemon preflight'
cornus setup --scenario ssh-containerdBare
Needs on the host: root, an OCI runtime on PATH, CNI plugins. Env: CORNUS_DEPLOY_BACKEND=bare.
ssh HOST 'sudo CORNUS_DEPLOY_BACKEND=bare cornus daemon preflight'
cornus setup --scenario ssh-bareIncus
Needs on the host: incusd 6.3+, socket access, skopeo and umoci. Env: CORNUS_DEPLOY_BACKEND=incus.
ssh HOST 'CORNUS_DEPLOY_BACKEND=incus cornus daemon preflight'
cornus setup --scenario ssh-incusAs a container on that host
You do not have to install a binary there. On a Docker host the server can run from the published image, reached through the same tunnel — cornus setup --scenario ssh-docker asks "Will the server run as a container on the remote host?" and switches to this shape.
Needs on the host: Docker, and a host directory for the data dir. No cornus binary, no systemd unit.
# Check the binds there first.
ssh HOST 'docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/cornus:/var/lib/cornus:rshared \
ghcr.io/moriyoshi/cornus:latest daemon preflight'
ssh HOST 'docker run -d --name cornus --privileged --restart unless-stopped \
-p 127.0.0.1:5000:5000 \
-e CORNUS_DATA=/var/lib/cornus \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/cornus:/var/lib/cornus:rshared \
ghcr.io/moriyoshi/cornus:latest serve --addr :5000'It publishes to loopback on the remote host, which is exactly where the SSH tunnel exits — nothing is exposed on that host's network. No systemd unit is offered for this shape: there is no binary for one to start, and --restart unless-stopped is what brings the server back after a reboot.
The binds carry the same weight as they do locally; see as a container and running the server in a container.
Registry caveat, all four: if the host's deploy targets cannot pull from the derived registry address, set --registry-host.
More: remote container hosts over SSH.
In the cluster
The arrangement Cornus is built around: the server runs as a StatefulSet in the cluster it deploys to, so its registry is a service endpoint the nodes reach by construction and the build cache survives restarts.
Needs: a cluster and kubectl/helm. Nothing on your machine but the CLI.
helm install cornus oci://ghcr.io/moriyoshi/charts/cornus
kubectl rollout status statefulset/cornus --timeout=300sHelm is the recommended path: the chart is versioned and its image tag tracks the chart version, so one command gives you a server and a manifest that match. A raw manifest works too, but must be pinned to a release tag, never a branch — it installs a privileged StatefulSet with broad RBAC.
Then point the CLI at it:
cornus setup --scenario kube-port-forward # auto port-forward, no exposure needed
cornus setup --scenario kube-url # or reach it at an ingress URLRegistry exposure: a NodePort registry auto-advertises the node address; for ClusterIP or ingress set registry.advertiseHost (or the client's --registry-host). cornus setup writes a matching cornus-values.yaml for you.
More: installation, Helm chart values, working with remote clusters, and the quick start, which walks this whole path on a single-node k3s cluster.
As a container on the host it manages
The server itself runs as a container on the host runtime it manages. On docker the difficulty is entirely in the bind mounts, and a wrong one does not fail at startup — it fails silently at deploy time.
Needs: Docker, and a host directory for the data dir. Nothing else — no Compose, no bundled file.
# Check the binds first, in the image you will serve from.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/cornus:/var/lib/cornus:rshared \
ghcr.io/moriyoshi/cornus:latest daemon preflight
docker run -d --name cornus --privileged --restart unless-stopped \
-p 127.0.0.1:5000:5000 \
-e CORNUS_DATA=/var/lib/cornus \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/cornus:/var/lib/cornus:rshared \
ghcr.io/moriyoshi/cornus:latest serve --addr :5000Run the preflight first: it is far cheaper to fix a bind while nothing is running. Each of the three flags earns its place — the socket bind is what makes this the host's docker rather than none at all, :rshared is what lets a mount cornus makes inside the container reach the host, and --privileged is needed to build in-process and for the kernel 9P mount.
cornus setup --scenario docker-container asks for the host data directory and port and prints this command with your answers filled in.
Check: cornus health.
On a containerd or Incus host
Both are supported and each is configured differently enough to be worth reading before you start:
- containerd uses
ctr runrather thandocker run, and that is load-bearing: cornus resolves its own container by asking the containerd it deploys through, so it only finds itself when that same containerd created the server's container. It also needs--net-hostand a/run/cornusbind, and the preflight refuses to start without either.cornus setup --scenario containerd-container. - Incus needs only the daemon socket — that backend hands incusd no path of its own — plus
--network hostfor a route to the incus bridge. Running the server as an incus instance instead needs neither.cornus setup --scenario incus-container.
More: running the server in a container.
A server someone else operates
Nothing to set up. Ask whoever runs it for the URL and, if it needs one, a credential, then:
cornus setup --scenario urlIf it requires authentication, the wizard walks you through enrolling an SSH key or storing a token. See security and authentication.
After the server is up
cornus health # is it listening?
cornus version # does the CLI reach it through the configured profile?
cornus compose up # deploy somethingIf cornus version fails where cornus health succeeds, the server is fine and the connection profile is not — re-run cornus setup, or see connection config.
See also: cornus setup, deploy backends, cornus serve, security and authentication.