This chapter is the whole trip, start to finish. We make a small app, put it in a repo, and give it to Yard. Then we change it, configure it, break it on purpose, and fix it. At the end, the app runs at https://hello.example.com, and you have written no Dockerfile, no pipeline file, and no server configuration.
Want to run software that someone else wrote, such as a password manager or a status page? That needs no repo and no code: Self-host a service is your chapter.
You can read this chapter before you have a host. It shows the shape of the work, and every later chapter fills in one part. When you want to follow along for real, Set up a host gets a VM ready, and you come back here.
1. An app on your laptop#
Start with a new Stario app:
uvx stario-create new hellocd hellojust devOpen http://127.0.0.1:8000, and the page says hello. That folder is all that Yard will need. Four of its files matter more than the rest, so remember them:
hello/ pyproject.toml lists stario as a dependency uv.lock the exact version of every package app/main.py the entry point: python -m app.main app/config.py every setting, and where files go: $PROJECT_DATA_DIR, or ./dataAny app can go on Yard. We start with Stario because Yard knows it best. Other languages shows a Go, a Node, a Python, and an http-nu app at the end.
2. A repo#
Yard deploys from a repo on GitHub or Cursor Origin, so the app needs one:
git add -Agit commit -m "First version"gh repo create you/hello --private --source . --pushOn GitHub, add the new repo to your Yard app’s installation (Settings → Applications → Installed GitHub Apps → Configure). That is how Yard reads a private repo and hears about each push. A public repo works without it, but then no push wakes Yard, and the five-minute timer finds each push instead.
3. One command#
Still in the app’s folder, run:
yard app new --public hello.example.comThe name hello.example.com must point at the VM; the wildcard record from Set up a host already does. Yard reads the repo URL and the branch from your checkout, so there is nothing else to type. It creates the app, deploys it at once, and streams the deploy log while it works:
name hellorepo https://github.com/you/helloref maintrigger pushcreated 2026-10-27T15:00:00Zruns auto (not deployed yet)Deploying hello now…== sync https://github.com/you/hello main== runs yard:python-uv (containerfile; stario: pyproject.toml declares stario)== build localhost/hello:4f1c2a9b7d3e-88dff6cd…built 2026-10-27T15:01:12Z== start localhost/hello:4f1c2a9b7d3e-88dff6cd== started localhost/hello:4f1c2a9b7d3e-88dff6cdok localhost/hello:4f1c2a9b7d3e-88dff6cdGive Caddy a few seconds to get the certificate, and then open https://hello.example.com. The page that said hello on your laptop now says hello on the internet.
4. What Yard just did#
Read the == runs line again, because it is the interesting one. The repo has no Dockerfile, so Yard looked at the repo instead. It saw stario in pyproject.toml and knew what kind of app this is. Then it wrote a Containerfile from its yard:python-uv template, and ran the image with its own compose file:
The image installs the packages exactly as
uv.locklists them, withuv sync --locked.The image starts the app the way you do:
python -m app.main.The container gets
STARIO_HOST=0.0.0.0andSTARIO_PORT=8000, so that Caddy can reach the app.The container gets
STARIO_TRACER=json, so each request becomes one line in the log.The container gets
/data, a directory on the host that survives every deploy.
The image tag says what is inside: 4f1c2a9b7d3e is your commit, and 88dff6cd is the hash of the file Yard wrote. Nothing is hidden. To read the file, run:
yard app cat containerfileThen yard status shows the app next to Yard itself:
name state cpu mem data rev bind deployyard-app-1 up 6h 0.10% 60MB 128MB 9c2c7e1 yard.example.com ok 43s · 2hyard-caddy-1 up 6h 0.30% 24MB 128MB ok 43s · 2hhello-app-1 up 1m 0.40% 48MB 4f1c2a9 hello.example.com ok 38s · 1m5. A change#
Open app/features/home/views.py and change the sentence on the page. Then push it:
git commit -am "Say something new"git pushThat is the whole deploy. GitHub tells Yard about the push, and Yard builds the new commit and swaps the container. Caddy keeps sending visitors to the same container name, so nobody sees an error during the swap. On GitHub, the commit gets a green check that links to the live site. Run yard app logs --deploy -f if you want to watch it happen.
6. A setting#
The app reads its name from the APP_NAME environment variable (see app/config.py). Set it on the host:
yard env set APP_NAME="Hello from Yard"Saved. hello keeps its old settings until the next deploy.Apply now: yard app deploy --app helloSaving never restarts the app, so you choose when the change lands. Run the line it printed:
yard app deployDeploying hello now…== sync https://github.com/you/hello main== runs yard:python-uv (containerfile; stario: pyproject.toml declares stario)== reuse localhost/hello:4f1c2a9b7d3e-88dff6cd== start localhost/hello:4f1c2a9b7d3e-88dff6cd== started localhost/hello:4f1c2a9b7d3e-88dff6cdok localhost/hello:4f1c2a9b7d3e-88dff6cdLook for == reuse: the code did not change, so there is no build. Compose starts a new container from the same image with the new value, and the page title changes.
One setting you never have to make is where the data goes. A container is thrown away at each deploy, so a file inside it is lost. But app/config.py keeps every file in PROJECT_DATA_DIR, and Yard sets that to /data, a directory on the host. When the app gets a database (the stario-litewriter skill in the app adds SQLite through litewriter), its file is app.sqlite3 in /data. On the host, that is /srv/yard/apps/hello/data/app.sqlite3, and every deploy keeps it. On your laptop the same code writes to ./data, which git ignores.
Secrets go the same way as APP_NAME, and never into the repo. When the code reads a setting with no default, such as os.environ["SESSION_SECRET"], the next deploy ends with a hint that names it and the line that sets it.
7. A mistake#
Now break something on purpose. Add a package, but commit only half of the change:
uv add httpxgit commit -m "Add httpx" pyproject.tomlgit pushThe new pyproject.toml asks for httpx, but the old uv.lock does not list it. The build stops:
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.…failed localhost/hello:7b0e3d5c1a29-88dff6cdRead the full log: yard app logs --deploy --app helloThe live site does not notice. The build failed before anything started, so the old container keeps running, and on GitHub the commit gets a red cross. Commit the lock, and push again:
git commit -m "Lock httpx" uv.lockgit pushThat is how Yard handles every mistake. A build that fails never starts. A new container that starts but never gets healthy is stopped, and the previous image comes back. When Yard cannot tell what to build, it stops before the build and says what it found. When Yard refuses lists each message and its fix.
8. Your own Containerfile#
The written file is a good start. When you want more, take it into the repo and change it. The first change worth making is a healthcheck. With one, Yard waits until the new container answers before it lets the old one go, and it brings the old image back when the new one never answers.
yard app cat containerfile > ContainerfileAdd one line at the end of the file:
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s \ CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/')"Then tell Yard to build this file, and push it:
git add Containerfilegit commit -m "Own the Containerfile"yard app set --runs Containerfilegit pushSet --runs before you push. Otherwise the push deploys on auto, and auto refuses to switch from Yard’s file to yours by itself. What an app runs explains that guard.
What you did not do#
Look back over the steps. You did not write a Dockerfile, a CI pipeline, or a Caddy or nginx configuration. You did not buy a certificate, set up a registry, open a firewall port, or log in to the server. Yard did each of those jobs, and each one left a record that you can read:
yard app cat containerfileprints the file that built the image.yard app logs --deployprints the deploy, step by step.yard events --app hellolists every push, check, deploy, and setting change.
Other languages#
Yard reads more than Stario. The steps are the same four: commit, push, yard app new, and open the address. Only the files differ, and so does the == runs line that says what Yard found.
A Go service needs a go.mod and one package main, at the root or in cmd/:
api/ go.mod go 1.26 main.go package main == runs yard:go (containerfile; go: go.mod)Yard builds one static binary with go build and runs it on a distroless image.
A Node app needs a package.json with a start script:
shop/ package.json "build": "vite build", "start": "node server.js" package-lock.json == runs yard:node-npm (containerfile; node: package.json start script)Yard runs npm ci, then npm run build because there is a build script, and starts with npm start.
A plain Python app needs uv.lock or requirements.txt, and one entry point such as app.py:
notes/ requirements.txt flask app.py == runs yard:python-pip (containerfile; python: requirements.txt)Yard installs requirements.txt into a virtual environment and runs python app.py.
A cross.stream app needs only serve.nu:
board/ serve.nu {|req| ... } == runs yard:http-nu (containerfile; http-nu: serve.nu)Yard downloads one http-nu binary, checks its sha256, and keeps the xs store in /data.
Only Stario and http-nu listen on the right address without help. The Go, Node, and plain Python apps must listen on 0.0.0.0, on the port in the PORT variable, which Yard sets to 8000. Flask’s app.run() listens on 127.0.0.1:5000 by default, so it needs host="0.0.0.0" and the port.
A Rust app — or anything else — brings its own Dockerfile:
api/ Dockerfile FROM rust:1-slim … Cargo.toml src/main.rs == runs Dockerfile (containerfile; found Dockerfile)Yard builds it as it is.
An app does not need code of its own. A public image or a compose file at a URL runs the same way, and Yard follows it as it follows a branch:
yard app new --runs docker.io/library/postgres:17 -e POSTGRES_PASSWORD=…yard app new --runs https://raw.githubusercontent.com/Bobowski/yard/main/examples/uptime-kuma.compose.yml --public status.example.comWhere to go next#
The whole idea explains the parts that you just used, and what Yard leaves out.
Set up a host gets a VM ready, if you only read along.
What an app runs shows every file that Yard reads, and every message it can stop with.
Day to day covers logs, settings, domains, rollbacks, and cleanup.