Your first app

Four rules, one command, one push.

You have a host. Now let's put an app on it. This chapter covers what the app must do, how to tell Yard about it, and how to see that it worked.

The four rules#

An app on Yard follows four rules. Most apps already follow them.

  1. It has a Containerfile or a Dockerfile, at the repo root or in ops/. (Or a compose.yml, for apps with more than one container. That's its own chapter.)

  2. It serves HTTP on port 8000, on all interfaces: 0.0.0.0, not 127.0.0.1. Caddy reaches the container over the network, and 127.0.0.1 inside a container means "only me".

  3. It reads its settings from environment variables. You set them with yard env set.

  4. It keeps its files in /data. Everything else in the container is thrown away on the next deploy. /data is a directory on the host, and it stays.

A Stario app made with uvx stario-create new follows all four rules already, so you can skip to the next section. For anything else, here's a minimal Containerfile, a Python app as an example:

FROM python:3.14-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir .
CMD ["python", "-m", "myapp", "--host", "0.0.0.0", "--port", "8000"]

Nothing about it is Yard-specific. If podman build and podman run -p 8000:8000 work on your laptop, the app will work on Yard.

Create the app#

Go to the app's checkout and run:

cd my-app
yard app new --public my-app.example.com

Yard reads the repo URL and the branch from the checkout, so there's nothing to type. It writes a small record on the host: the repo, the branch to follow, and the trigger (push). The name comes from the repo, so github.com/you/my-app becomes the app my-app.

--public adds a bind: Caddy now sends https://my-app.example.com to the container my-app-app-1. Yes, "app" twice. Compose builds the name from the project (my-app), the service (app), and the instance number (1). You'll see that <name>-app-1 pattern everywhere in Yard.

Make sure my-app.example.com points at the VM first. (If you added the wildcard record in Set up a host, it already does.)

Push#

git push

That's the deploy. Or, if there's nothing new to push, ask for one directly:

yard app deploy

The first deploy builds the image from scratch, so it takes as long as your build takes. Later deploys reuse Podman's build cache.

See it#

yard status
name               state   cpu    mem    data   rev      bind                                  deploy
yard-app-1         up 6h   0.1%   60MB   128MB  9c2c7e1  yard.example.com → yard-app-1         ok 43s · 2h
yard-caddy-1       up 6h   0.3%   24MB
my-app-app-1       up 1m   0.4%   48MB   0B     a1b2c3d  my-app.example.com → my-app-app-1     ok 31s · 1m
host: 31% of 4 cores · 294MB of 7.7GB · 1.3GB of 40GB

One row per container. rev is the commit it runs. deploy says how the last deploy went, how long it took, and how long ago it was.

Give Caddy a few seconds to get the certificate, then open https://my-app.example.com.

When it doesn't work#

Three commands cover almost every first-deploy problem:

yard app logs --deploy    # the build and the start: did the deploy work?
yard app logs             # the app's own output: did the app start?
yard status --full        # the host: DNS, certificate, Caddy

The usual suspects, in order of likelihood:

  • The build failed. The deploy log shows the build output. Fix it, push again.

  • The app listens on 127.0.0.1. The container runs, but yard status says no answer. Listen on 0.0.0.0.

  • The app listens on another port. Same symptom. Use 8000, or bind the real port: yard bind set my-app.example.com my-app-app-1:3000.

  • DNS doesn't point at the VM yet. yard status --full shows the dns and certificate rows.

Settings and secrets#

Your app probably needs a few environment variables:

yard env set DATABASE_PATH=/data/app.sqlite3 SECRET_KEY=…
yard app deploy

The values live in one file on the host, readable only by the yard user. They never appear in an API response or a log. A new value reaches the container on the next deploy, which is why the second line is there. Day to day has more.

By the way#

  • No push? The check still notices. A new app record is enough: on the next check, at most five minutes away, Yard deploys the branch tip. yard app deploy is just faster.

  • A branch can be an app too. Check out a branch called redesign and run yard app new --public redesign.example.com. The app is named my-app-redesign, follows that branch, and lives next to my-app with its own data. That's a preview environment in one command.

  • Inside the container, Yard also sets PROJECT_NAME, PROJECT_DATA_DIR=/data, SHA (the commit), and PORT=8000. Use SHA to show the running version somewhere.

  • Not inside a checkout? yard app new --repo https://github.com/you/my-app --ref main does the same thing.