Podman and containers

Why every app gets a container, and how the containers find each other.

Every app on Yard runs in a container. This chapter explains what that buys you, how the containers find each other, and the few Podman details that shape everything else.

Why a container at all#

A container is a process with its own filesystem. The app inside it sees its own Python, its own packages, and its own /tmp, and nothing of the other apps. That gives you three things.

Each app brings its own world. One app can run Python 3.12 and another 3.14. A Node app can live next to a Go binary. Nothing is installed on the host for an app, so nothing on the host can break an app.

A deploy is a swap. The new version is a new image. Yard starts a container from it and throws the old one away. There is no “upgrade in place”, and no leftover file from last month’s version.

The recipe is a file. A Containerfile says exactly how the image is made. You can read it, build it on your laptop, and get the same result.

The cost is that a container forgets. Every file it writes is gone at the next deploy, except the files in /data. That one rule is the price of the three gifts above, and it is a fair price. Settings and data explains /data.

Why Podman#

Docker would work too. Yard uses Podman for one reason above the others: it runs without root and without a daemon that runs as root. Every container on a Yard host belongs to the plain yard user. The host explains what that protects.

Podman also speaks Docker’s API on a socket. So the tools you know, docker-compose among them, talk to Podman as if it were Docker. Yard leans on that. It never reimplements Compose. It runs the real one.

One network, many names#

Every app container joins one Podman network called yard. Caddy is on it too. Podman runs a small DNS server on that network, so a container can reach another by its name: Caddy reaches notes-app-1:8000 without knowing any address.

That is the heart of the design. Addresses change at every deploy. Names do not. Compose names a container <project>-<service>-<n>, so the app notes with its service app runs as notes-app-1, today and after the next hundred deploys. Caddy dials the name, and the swap needs no change in Caddy.

Yard’s own compose file also gives each app container a short alias, the app name. So another container can reach notes:8000 as well. A compose file of your own sets its aliases itself.

Compose on top of Podman#

Each app is a Compose project with the app’s name. A project is a set of containers, networks, and volumes that start and stop together. For most apps, that set is one container.

Yard keeps two compose files of its own for the apps that do not bring one. The first, app.compose.yml, is the base that every such app gets: the service app, the network yard, the env file, and /data. The second, build.compose.yml, adds the build for an app that Yard builds from a repo. An app with a compose file of its own runs that file, as it is. Your own compose file shows both files in full, and the contract for yours.

Compose runs inside the deploy container, not on the host and not inside Yard. That container mounts the yard user’s Podman socket, and docker-compose talks to Podman through it. Deploys explains why a deploy gets a container of its own.

Images, tagged by commit#

An image that Yard builds is named after the app and the commit:

localhost/notes:9c2c7e1a2b3c            the repo's own Containerfile
localhost/notes:9c2c7e1a2b3c-3f9a1c2e   a Containerfile that Yard wrote

localhost/ means “built here, never pushed”. The twelve characters are the commit, the way Docker shortens them. The eight after the dash are the stamp of Yard’s templates. The stamp changes when a Yard release changes a template, so the same commit gets a new image on the new template.

Because the tag names the commit, a deploy of a commit that already has an image needs no build. The deploy log says == reuse, and Compose starts the image that is there. That is what makes a new setting, or a deploy by hand, take seconds.

An image app runs the publisher’s image by its digest, for example docker.io/vaultwarden/server:1.37.4@sha256:…. A restart can never change what runs, even when the tag moves upstream.

The Docker format, for the healthcheck#

Podman’s default image format drops the HEALTHCHECK line without a word. Yard builds every image in the Docker format, which keeps it. Healthchecks has the details.

Restarts and reboots#

Each app container has restart: always. When the process inside it exits, Podman starts it again, and yard status counts the restarts. A number that keeps growing means the app crashes. Its log says why.

After a reboot of the VM, podman-restart.service starts every container that has that policy. Caddy resumes its last config, Yard starts, and the apps come back without a deploy.

Cleaning up#

Every deploy that builds leaves an image behind. That is useful for a while and then it fills the disk. So once a day, ten minutes after Yard starts and then every 24 hours, Yard prunes:

  1. It removes its own localhost/ images that no container uses. Each app keeps its running image and its three newest images, for a quick rollback by hand. An image older than seven days goes even among those three.

  2. It runs Podman’s own image prune for untagged build layers older than seven days. The newer ones stay, because they are the build cache that keeps the next deploy fast.

Images pulled from a registry with a tag stay until you remove them yourself. yard prune --dry-run shows what would go, and YARD_PRUNE_DAYS in Yard’s own env changes the seven days. Day to day has the commands.

Looking around by hand#

Everything Yard does with Podman, you can do over SSH as the yard user:

sudo -u yard podman ps -a                     # every container, deploy containers too
sudo -u yard podman logs deploy-notes         # the last deploy of notes
sudo -u yard podman inspect deploy-notes      # its labels: the record of that deploy
sudo -u yard podman exec -it notes-app-1 sh   # a shell inside the app
sudo -u yard podman images                    # every image on the host

Yard does not mind. It reads Podman’s state each time it needs it, so it never disagrees with what you see.

By the way#

  • --remove-orphans is on. Delete a service from your compose file, and the next deploy removes its container.

  • Yard reserves two kinds of names: yard-* belongs to the host, and caddy belongs to the proxy. An app cannot take either.