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.
Quickstart
- Add .paas/app.yaml to your repo (see below) and push.
- Sign in, click New App, paste the repo URL and branch.
- Open Secrets and set anything your app needs (e.g. SECRET_KEY).
- 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:
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
| name | required | Stack + service identity. Alphanumeric, -, _. |
| runtime | required | python, 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). |
| dockerfile | optional | Path to your repo's own Dockerfile (or set runtime: dockerfile for ./Dockerfile). koyracloud builds it as-is; build/start are ignored. |
| root | optional | Build-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. |
| start | optional | The command that starts your server (becomes the container command). Must bind 0.0.0.0 on port. Not needed for static / dockerfile. |
| static_dir | optional | For runtime: static: directory to serve. Auto-detected (dist/build/public/out/_site or repo root) if omitted. |
| spa | optional | For 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. |
| headers | optional | For 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. |
| port | required | Container port Traefik routes to. |
| build | optional | Image build steps (become RUN layers). Cached by Docker's layer cache; unchanged deps aren't reinstalled. |
| predeploy | optional | Commands run on every start before the app — e.g. migrations. Must be idempotent. |
| subdomain | optional | Default host. Falls back to <name>-<token>.apps.example.com (a random token keeps names from colliding). Manage more in the Domains tab. |
| persist | optional | Directories that survive redeploys (NFS-backed volumes, mounted into the container). |
| cpu | optional | CPU limit for the web service, e.g. "0.5". Falls back to the instance default (capped so one app can't starve a node). |
| memory | optional | Memory limit for the web service, e.g. 256M. Falls back to the instance default. |
| healthcheck | optional | HTTP 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). |
| env | optional | Non-secret environment defaults baked into the deploy. |
| secrets | optional | Names of secrets to inject at deploy. Set their values in the UI — never commit them. |
| redis | optional | true provisions a scoped Redis and injects REDIS_URL. Namespace keys + channels as <name>: (see below). |
| workers | optional | Always-on background processes off the same image — [{name, start, replicas?, cpu?, memory?}]. No HTTP port. |
| cron | optional | Scheduled jobs — [{name, schedule, command}]. 5-field cron, UTC, run to completion. |
| notify | optional | Deploy-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.
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:
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):
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:
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.