Get started

Deploy a repo

koyracloud builds your app into a container image, pushes it to a built-in registry, and runs it on the swarm. You describe how to build and start it in a .paas/app.yaml manifest — or bring your own Dockerfile.

Want to run your own koyracloud? It's open source — the full self-host walkthrough (Docker Swarm, Traefik, NFS, DNS) is in the self-host tutorial on GitHub.

Quickstart

  1. Add .paas/app.yaml to your repo (see below) and push.
  2. Sign in, click New App, paste the repo URL and branch.
  3. Open Secrets and set anything your app needs (e.g. SECRET_KEY).
  4. Hit Deploy and watch the live log. Your app comes up at <name>-<token>.apps.example.com.

Prefer to start from something runnable? The examples on GitHub have complete manifests for a FastAPI+React app and a static site.

The manifest

A complete example for a FastAPI + Vite app:

.paas/app.yaml
name: lens-inventory
runtime: python+node
subdomain: lens.apps.example.com   # default host (optional)
port: 8000
build:
  - pip install -r requirements.txt
  - bash -c "cd web && npm ci && npm run build"
predeploy:
  - alembic upgrade head
start: uvicorn app.main:app --host 0.0.0.0 --port 8000
persist:
  - data
healthcheck: /health
env:
  CORS_ORIGINS: https://lens.apps.example.com
secrets:
  - SECRET_KEY

Manifest fields

namerequiredStack + service identity. Alphanumeric, -, _.
runtimerequiredpython, node, python+node, static, dockerfile, or go (two-stage golang:1.23 → distroless build; no shell or python3 in the runner, so healthcheck/predeploy aren't supported — koyracloud rejects the combination).
dockerfileoptionalPath to your repo's own Dockerfile (or set runtime: dockerfile for ./Dockerfile). koyracloud builds it as-is; build/start are ignored.
rootoptionalBuild-context subdirectory for a monorepo app — e.g. marketing/site. The Docker context (and dockerfile path) are scoped to it; the rest of the repo can build separately. Blank = repo root.
startoptionalThe command that starts your server (becomes the container command). Must bind 0.0.0.0 on port. Not needed for static / dockerfile.
static_diroptionalFor runtime: static: directory to serve. Auto-detected (dist/build/public/out/_site or repo root) if omitted.
spaoptionalFor runtime: static: true always falls back to index.html (SPA); false serves real 404s. Omit to auto-detect — a 404.html in the build (Next.js exports ship one) → real 404s, else SPA fallback.
headersoptionalFor runtime: static: extra response headers (string map) sent on every response. X-Content-Type-Options: nosniff and X-Frame-Options: SAMEORIGIN are sent by default unless overridden here.
portrequiredContainer port Traefik routes to.
buildoptionalImage build steps (become RUN layers). Cached by Docker's layer cache; unchanged deps aren't reinstalled.
predeployoptionalCommands run on every start before the app — e.g. migrations. Must be idempotent.
subdomainoptionalDefault host. Falls back to <name>-<token>.apps.example.com (a random token keeps names from colliding). Manage more in the Domains tab.
persistoptionalDirectories that survive redeploys (NFS-backed volumes, mounted into the container).
cpuoptionalCPU limit for the web service, e.g. "0.5". Falls back to the instance default (capped so one app can't starve a node).
memoryoptionalMemory limit for the web service, e.g. 256M. Falls back to the instance default.
healthcheckoptionalHTTP path probed for liveness, e.g. /health. The probe execs python3 inside your container — with your own Dockerfile, an image without python3 builds and starts fine, then swarm kills it when the probe fails (Alpine: RUN apk add --no-cache python3, or omit the field).
envoptionalNon-secret environment defaults baked into the deploy.
secretsoptionalNames of secrets to inject at deploy. Set their values in the UI — never commit them.
redisoptionaltrue provisions a scoped Redis and injects REDIS_URL. Namespace keys + channels as <name>: (see below).
workersoptionalAlways-on background processes off the same image — [{name, start, replicas?, cpu?, memory?}]. No HTTP port.
cronoptionalScheduled jobs — [{name, schedule, command}]. 5-field cron, UTC, run to completion.
notifyoptionalDeploy-failure webhook — {on_failure: <https url>}. On a failed deploy koyracloud POSTs JSON {app, deploy_id, status, error, log_tail} to it (best-effort). The old service keeps running.

Runtimes & build

Each deploy builds a per-app image — from a Dockerfile koyracloud generates (base python:3.12 + node:22, your build steps as layers) or your repo's own. It's built on local disk (off NFS, layer-cached), pushed to the internal registry, and the container runs from the image — so the app reads no code over NFS and can run on any node.

Build-time env (NEXT_PUBLIC_*, VITE_*) is passed as build args, so client bundles bake the right values. Secrets are injected at run time only. Single-container model: serve your built frontend from your backend (e.g. FastAPI StaticFiles) so API and SPA share one origin and port.

Bring your own Dockerfile

Already containerized? Set runtime: dockerfile and koyracloud builds your image as-is and runs it as a managed service — you still get domains, env/secrets, logs and rollback.

.paas/app.yaml
name: my-app
runtime: dockerfile        # or: dockerfile: docker/Dockerfile
port: 8000
healthcheck: /health
secrets:
  - DATABASE_URL

One gotcha: healthcheck: execs python3 inside your container. A slim/alpine image without it builds and starts fine — then swarm kills it when the probe fails. Install it (RUN apk add --no-cache python3) or drop the healthcheck field.

Push-to-deploy

Turn on Auto-deploy in the app's Settings and add a GitHub webhook to your repo (the Settings tab shows the URL + secret). Choose the event it sends: push deploys on every push; workflow_rundeploys only after a GitHub Actions run finishes successfully — so repos with CI deploy after it passes.

Static sites (Netlify-style)

For frontend-only / static sites, koyracloud serves the files itself — no server command needed. Use runtime: static:

.paas/app.yaml
name: my-site
runtime: static
# optional: build a frontend first, then serve the output
build:
  - bash -c "npm ci && npm run build"
static_dir: dist        # auto-detected if omitted

Zero-config: if a repo has index.html (at the root or in dist/build/public/out/_site) and nomanifest at all, koyracloud auto-detects it as a static site and serves it — just connect the repo and deploy. SPA client-side routes fall back to index.html, and the analytics beacon is injected automatically.

Custom domains

Every app gets <name>-<token>.apps.example.com automatically. To attach your own domain, open the app's Domains tab and add it. If Cloudflare for SaaS is configured, koyracloud registers it as a custom hostname and shows the two CNAME records to add at your registrar — the Cloudflare edge then mints and auto-renews TLS (Vercel-style, no nameserver move):

DNS (your registrar)
Type   Host                    Value
CNAME  yourdomain              origin.<your-saas-zone>
CNAME  _acme-challenge.yourdomain   <shown in the Domains tab>

The Domains tab shows each domain's cert status (Pending → Active). Set any domain as primary; all attached domains route to the same app. (Without Cloudflare for SaaS, point an A record at the host and Traefik mints a Let's Encrypt cert instead.)

Secrets & env

List secret names in the manifest; set their values in the app's Secrets tab. They're encrypted at rest (Fernet) and injected as environment variables at deploy time. Non-sensitive config goes in env:.

Analytics & uptime

Every app gets built-in, cookieless analytics(pageviews, unique visitors, top pages/referrers) and an uptime monitor (koyracloud probes your app and tracks up/down + 24h %). Both are on the app's tabs and header.

Static sites get the analytics beacon injected automatically. For dynamic apps, paste the one-line snippet from the Analytics tab. Analytics is opt-out per app. Set an email in Settings → Email alerts to be notified on deploy success/failure and down/recovered (when the instance has email configured).

Persistence

Directories under persist: live on the NFS-backed volume and survive redeploys and reschedules. A SQLite database at ./data/app.db, for example, persists across deploys when data is listed.

Pinning stateful apps

By default an app isn't pinned to any node — Swarm can run or reschedule it anywhere. That's fine as long as its state lives under persist:, but an app that keeps state on the node's local disk instead would have that data orphaned by a reschedule.

Turn on Pin to node in the app's Settings tab to keep it on the one node it's already running on. It takes effect on the app's next deploy — it won't move or restart a container that's already running, so redeploy the app to enforce it immediately.

Workers, cron & Redis

An app can run background work from the same repo and image as its web process — declared in the manifest, no extra service to wire up:

.paas/app.yaml
redis: true                      # scoped Redis + injected REDIS_URL

workers:                         # always-on, no HTTP port
  - name: events
    start: python -m app.worker
    replicas: 1                  # optional (default 1); cpu/memory optional

cron:                            # 5-field schedules, UTC
  - name: nightly
    schedule: "0 2 * * *"
    command: python -m app.jobs.nightly
  • Workers are extra services off the one image — same env, secrets and REDIS_URL, no router, no healthcheck. They don't run the web's predeploy. Status + logs live on the app's Background tab.
  • Cron jobs run to completion on schedule (UTC) from the app's live image, with per-run status, logs and a Run now button. No catch-up: a job overdue after downtime fires once, not per missed slot.
  • Redis is one shared instance, isolated per app by an ACL user: you may only touch keys and pub/sub channels prefixed <name>:(e.g. my-app:jobs) — other names are rejected. It runs noeviction, so it back-pressures instead of silently dropping a queue. A typical pattern: the web app RPUSHes to my-app:jobs and a worker BLPOPs it.

How it works

repo (.paas/app.yaml — or your own Dockerfile)
   │  control plane clones @ commit → LOCAL build dir (off NFS)
   ▼
docker build   →  per-app image  (layer-cached)
   ▼
docker push    →  internal registry koyracloud runs
   ▼
docker stack deploy  →  Swarm service (Traefik labels, secrets, persist)
   ▼
Swarm pulls + runs on ANY node  ·  predeploy (migrate) · start
   ▼
https://<host>  ·  TLS at the edge, or minted by Traefik

The control plane is a single service on the swarm manager that renders a Docker stack from your manifest and drives the cluster. Each deploy builds your app into an image on local disk and runs the container from it — the app never reads code over NFS, so any node can pull, run and reschedule it. NFS is used only for persisted data. A deploy is marked live only after the service actually converges — every replica running (and healthy) on the new image; otherwise it fails with the real task error instead of pretending.