| name | deploy |
| description | Use when deploying anything onto a Zeabur-rented server — a public image like nginx, a website, an app. This skill picks the path; read it before installing anything on the machine. Use when the user says "run nginx on my server", "host my site on the VPS", or "deploy X" after renting. |
Deploy to a Zeabur Server
If what the user wants to deploy is their own source code (a Git repo) rather than an existing image, build it into an image first with the build-image skill — then deploy that imageReference through the paths below like any other image.
Path selection — ask, and recommend k3s
-
The user named a stack (docker / docker compose / 1Panel / 寶塔 / bare systemd…) → respect it, don't second-guess it. Follow the self-managed branch below.
-
The user didn't specify how → ask before installing anything, presenting the common options with the recommendation first. For example:
How should this machine run its services?
- ZeaburOS (k3s, recommended) — Zeabur's suggested setup: k3s + the Zeabur operator; declarative deployments, auto-renewed HTTPS, machine resources visible in the Zeabur dashboard, and Zeabur projects deployable later
- Vanilla k3s — upstream open-source k3s; fully independent machine, zero Zeabur components, Zeabur only provides the server and domain
- docker compose — single-machine container orchestration, one YAML for everything
- 1Panel / 寶塔 — a web control panel, if you want click-to-manage later
- Bare install — apt-install things directly on the OS, the classic way
The machine is yours — any of these works, and other ideas are welcome too.
(Present the question in the user's language. If the agent environment has a structured question tool, use it with these options instead of prose.) "You decide" or picking option 1 → the Kubernetes path, ZeaburOS variant. Option 2 → the Kubernetes path, vanilla variant. Options 3–5 or any other named stack → self-managed branch. Ask once per machine — the choice sticks for later deployments on the same server unless the user changes it. Note the honest distinction: option 1 installs Zeabur's operator and connects the machine to Zeabur's control plane; option 2 leaves zero Zeabur components on the box.
The recommended path gives every deployment the same shape — declarative workloads, an ingress with automated TLS renewal, and room to add more apps to the same machine later — instead of a pile of hand-run commands. That's the pitch; the user still decides.
Kubernetes path (ZeaburOS or vanilla k3s)
On the ZeaburOS variant, read ../../references/zeaburos.md before touching the cluster — it is the operating contract: what the platform owns, what reverts if you change it, and the ingress-controller's routing/TLS rules (hosts route to a single backend and paths are ignored; host collisions are destructive; numeric service ports only).
1. A server with k3s
If there is no server yet, rent one with the server-rent skill. Either variant goes only on a blank machine (fresh rent or fresh reinstall; k3s claims ports and rewrites iptables, so on a machine the user already set up, reinstall first — see server-manage).
ZeaburOS variant — install through the Zeabur API. Despite the mutation's name, this installs k3s plus the Zeabur operator (which manages platform components like cert-manager) and registers the cluster with Zeabur:
OP_ID=$(uuidgen)
zapi "$(jq -n --arg op "$OP_ID" --arg id "<server-id>" '{query: "mutation($id: ObjectID!, $op: String){ installK3s(serverID: $id, operationID: $op) }", variables: {id: $id, op: $op}}')"
Poll until hasK3s is true (installation takes a few minutes; read events if stuck):
zapi '{"query":"query($id: ObjectID!){ server(_id: $id) { hasK3s provisioningStatus events { message time severity } } }","variables":{"id":"<server-id>"}}'
Vanilla k3s variant — install upstream k3s over SSH; no Zeabur components touch the machine:
ssh <sshUsername>@<ip> -p <sshPort> "curl -sfL https://get.k3s.io | sudo sh -"
2. kubectl access
Fetch the kubeconfig over SSH and point it at the public IP:
ssh <sshUsername>@<ip> -p <sshPort> "sudo cat /etc/rancher/k3s/k3s.yaml" > ./kubeconfig
sed -i.bak "s/127\.0\.0\.1/<ip>/" ./kubeconfig && rm -f ./kubeconfig.bak
export KUBECONFIG="$PWD/kubeconfig"
kubectl get nodes
Treat the kubeconfig like a credential: keep it out of git and out of chat output.
3. Deploy the workload
Use the kubernetes-workloads skill for the mechanics (fragments, server-side apply, rollout verification). One mapping note: those skills describe machines that also host Zeabur project services, with environment-<id> namespaces and Zeabur identity labels. A toolkit-only deployment has no Zeabur project — use a plain namespace (default is fine) and plain names (nginx), and skip the Zeabur ID injection entirely. The boundary, mutation loop, and verification discipline still apply as written.
Minimal example (nginx):
kubectl create deployment nginx --image=nginx --port=80
kubectl expose deployment nginx --port=80
4. Expose it
Exposure is its own fork. If the user already named a hostname (or said "no domain"), follow that. Otherwise ask, recommendation first:
How should it be reachable from outside?
- zeabur.app subdomain (recommended) — instantly get
<your-label>.zeabur.app with DNS pointed at this machine, plus auto-issued, auto-renewed HTTPS
- Your own domain — create an A record at your DNS provider pointing to
<server-ip>; the rest of the flow is identical
- Bare IP for now — no domain,
http://<ip> for testing (no HTTPS)
- Not public yet — internal to the cluster/machine only
(Present the question in the user's language.)
HTTPS comes with options 1–2 by default — skip it only if the user explicitly says so. Then, order matters — DNS first, then certificate (Let's Encrypt HTTP-01 must reach the hostname):
-
Get DNS pointing at the machine: option 1 → the subdomain skill (<label>.zeabur.app → this server's IP, wait for ACTIVE); option 2 → the user creates an A record at their DNS provider, then verify with dig +short <host> before proceeding.
-
The ingress controller — per variant. On vanilla k3s, the bundled traefik is there out of the box; use ingressClassName: traefik. On ZeaburOS, Zeabur ships its own ingress-controller — that is exactly why the bundled traefik is disabled (traefik.yaml.skip). It claims the node's 80/443, serves every Ingress resource regardless of ingressClassName (so omit that field), speaks HTTP/3, and also routes Zeabur's own monitoring/log endpoints (*.servers.onzeabur.com) that the dashboard depends on.
Never enable the bundled traefik on ZeaburOS. If traefik grabs 80/443 first, Zeabur's ingress-controller sits Pending forever and the dashboard's server monitoring breaks with it. It may take a few minutes to appear after installK3s finishes — wait for it instead of installing something else:
kubectl get pods -A | grep -i ingress
(If a machine already has traefik enabled from older guidance: restore the traefik.yaml.skip marker, delete the traefik HelmChart/deployment, and Zeabur's controller will schedule as soon as 80/443 free up.)
3. TLS — the variants differ completely.
ZeaburOS: do nothing. The Zeabur ingress-controller has ACME built in (HTTP-01 against Let's Encrypt): the first HTTPS request for an Ingress host triggers issuance (that first handshake is served a self-signed placeholder — poll until the real cert lands), renewal is automatic, HTTP redirects to HTTPS, and HTTP/3 is on. Do NOT use cert-manager for public TLS here — the controller intercepts /.well-known/acme-challenge/* globally, so a cert-manager HTTP-01 solver always sees 404 and the two fight over the same ACME authorization until issuance fails. A preinstalled cert-manager on the cluster belongs to Zeabur's internal components — leave it alone. Bring-your-own-cert and other fine print: see ../../references/zeaburos.md.
Vanilla k3s: install cert-manager and a Let's Encrypt issuer:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
kubectl -n cert-manager wait --for=condition=Available deploy --all --timeout=180s
kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
EOF
- The Ingress. On ZeaburOS, this plain Ingress is the whole job — no
ingressClassName, no annotations, no spec.tls; the controller picks it up and handles TLS by itself:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
spec:
rules:
- host: <label>.zeabur.app
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
On vanilla k3s, add to the same manifest: ingressClassName: traefik under spec, the annotation cert-manager.io/cluster-issuer: letsencrypt, and a tls block (hosts: [<label>.zeabur.app], secretName: <label>-zeabur-app-tls).
- Verify from outside. ZeaburOS: the first HTTPS request can take a minute while the controller finishes issuance — poll until 200:
until [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://<label>.zeabur.app)" = "200" ]; do sleep 10; done
curl -sI "https://<label>.zeabur.app" | head -1
Vanilla k3s: kubectl wait --for=condition=Ready certificate <label>-zeabur-app-tls --timeout=180s, then the same external curl.
Self-managed branch (user chose their own stack)
SSH in and set up exactly what the user asked for (e.g. apt-get install docker.io && docker run -d -p 80:80 nginx). For the hostname, use the subdomain skill the same way; for HTTPS, run certbot on the machine (certbot --nginx or certbot certonly --standalone) — DNS points straight at this server and port 80 is reachable, so HTTP-01 just works, with auto-renewal via the certbot timer.
Either way, finish with verification
Report: what is running, where (namespace/unit), the URL, and cert expiry/renewal story. curl -sI https://<host> from outside the machine is the completion check — an applied manifest or a started container is not yet a deployment.