Saltar al contenido principal

On a server, with Quadlet

This is the recommended way to run Kinkeep. Podman reads systemd unit files, called Quadlets, and turns each container into a service that starts on boot and restarts on failure. Nothing else is needed: no orchestrator, no daemon of your own.

deploy/quadlet/ in the repository holds units for PostgreSQL, the application, and Caddy, which terminates TLS and obtains its certificate on its own. They share one Podman network, and only Caddy publishes ports.

Before you start

  • A server with Podman and systemd, running as root.
  • Ports 80 and 443 open.
  • A domain whose DNS already points at the server. Caddy cannot obtain a certificate before the name resolves, so do this first and let it propagate.

Install

Run these on the server, as root:

# Configuration and reverse proxy
sudo mkdir -p /etc/kinkeep
sudo cp deploy/quadlet/kinkeep.env.example /etc/kinkeep/kinkeep.env
sudo chmod 600 /etc/kinkeep/kinkeep.env
sudo $EDITOR /etc/kinkeep/kinkeep.env
sudo cp deploy/quadlet/Caddyfile /etc/kinkeep/Caddyfile

# Install the units and start
sudo cp deploy/quadlet/*.network deploy/quadlet/*.volume \
deploy/quadlet/*.container /etc/containers/systemd/
sudo systemctl daemon-reload
sudo systemctl start kinkeep-db.service kinkeep.service kinkeep-caddy.service

The application runs its database migrations on start, so there is no separate migration step, and the services come up again on boot.

See Configuration for what goes in kinkeep.env. The one value that matters most here is PHX_HOST: it is both the hostname the application builds URLs with and the site address Caddy requests a certificate for.

The client

Since September 2026 the interface is not part of the application image: it is its own Node server, released as docker.io/kinkeep/web and running beside the API (see decision 0017 in the architecture repository). Install it after the API is up:

# Configuration
sudo cp deploy/quadlet/web.env.example /etc/kinkeep/web.env
sudo chmod 600 /etc/kinkeep/web.env
sudo $EDITOR /etc/kinkeep/web.env

# The unit
sudo cp deploy/quadlet/web.container /etc/containers/systemd/
sudo systemctl daemon-reload
sudo systemctl start web.service

The one value in web.env is API_ORIGIN, the API's address from the client's own container: the name of the application container on the shared podman network, http://kinkeep:4000. It is read per request rather than at build time, because a standalone Next build bakes its rewrites when the image is built.

The interface now answers where the API used to, so the reverse proxy must split the paths instead of passing everything to the application: /api and /media belong to the API, and every other path belongs to the client.

app.example.org {
handle /api/* {
reverse_proxy kinkeep:4000
}
handle /media/* {
reverse_proxy kinkeep:4000
}
handle {
reverse_proxy web:3000
}
}

The client's image updates the same way the application's does, by watching the registry.

:::tip Check the units against your own Podman first Quadlet gained directives over time, and a server on a long term distribution can be several versions behind a current workstation. You can see exactly what your Podman makes of the units, without installing anything, with:

QUADLET_UNIT_DIRS=deploy/quadlet \
/usr/lib/systemd/system-generators/podman-system-generator --dryrun

It prints the podman run command it would generate for each unit, and warns about anything it does not understand. :::

Already running something else on the same host?

The units are self-contained on purpose, so that they work on a server with nothing else on it. Two applications cannot both publish ports 80 and 443, so if this host already serves another site, do not install kinkeep-caddy.container. Instead, join your existing proxy to the kinkeep network and give it a block for the new hostname.

With Caddy, that means adding one line to its unit:

Network=kinkeep.network

and one block to its Caddyfile. With only the API installed, the application answers every path:

app.example.org {
reverse_proxy kinkeep:4000
}

With the client installed too, use the split from The client instead, so /api and /media reach the API and everything else reaches the client.

Validate the configuration before restarting the proxy, because a proxy that fails to start takes every site on the host with it:

podman run --rm -v /etc/caddy/Caddyfile:/etc/caddy/Caddyfile:ro \
docker.io/library/caddy:2 caddy validate --config /etc/caddy/Caddyfile

Everything else, container names, volumes and the network, is prefixed so that it does not collide with another deployment on the same machine.

Next