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
- Configuration, for the environment file.
- Operations, for the first administrator, updates and backups.