box
docs /Databases & services

Databases & services

You don't write a manifest to run postgres. The registry publishes runnable apps — prebuilt images with a real entrypoint, a data volume, and a health gate — and ply run takes them by name:

ply run postgres@17 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=todos
ply run redis@8

First run fetches the image (a few KiB — the service binaries come from shared packages, so ten services share one base); after that it starts in tens of milliseconds. Data persists in a ply-managed volume across restarts.

#How names resolve

ply run <name>[@version] looks the name up in the registry's apps namespace (--source points anywhere else, any [sources] spec including file:///). The newest matching version wins — postgres@17 means "the latest published 17.x". That's deliberate: interactive runs want current; locked, repeatable resolution is what ply build + ply.lock (and a stack's lock) are for.

Runnable apps live beside the package catalog, not inside it: ply/redis is the inert library package your manifests depend on, apps/redis is the runnable service. Asking ply run for a library tells you so instead of starting nothing.

#The env contract

Services speak the same env vars as their Docker official images — every tutorial and muscle memory transfers:

service vars
postgres POSTGRES_PASSWORD (set → scram auth; unset → trust, dev only), POSTGRES_USER (default postgres), POSTGRES_DB (created on first boot), PGPORT (default 5432), POSTGRES_LISTEN (default *; use 127.0.0.1 to keep a rootless instance off the LAN)
redis REDIS_PASSWORD (→ requirepass), REDIS_PORT (default 6379), REDIS_BIND (default all; 127.0.0.1 for local-only), REDIS_ARGS (extra redis-server arguments)

Like Docker's images, init-time vars (POSTGRES_PASSWORD, POSTGRES_DB) apply on first boot only — an existing data volume keeps its password. To start over, remove the app's volume (under ~/.local/share/ply/volumes/<app>/ rootless, /var/lib/ply/volumes/ rootful).

#Ports, rootless

Rootless instances share the host network, so postgres binds the host directly. If something already owns 5432 (a system postgres is the classic), move the service: -e PGPORT=5442 — and connect to 5442. A host process on the same port silently shadows your container otherwise; this is a rootless fact of life, not a ply quirk.

#Production

The same image is the production story on a droplet: give it a [restart] policy ride under systemd, publish it internal: so only your apps reach it, and gate dependants with --after:

ply systemd postgres-17.10.0-linux-x64.img -e POSTGRES_PASSWORD=… --publish internal:5432 \
  | sudo tee /etc/systemd/system/ply-postgres.service
ply run api.img --after postgres      # waits for health, learns the address

#The escape hatch: docker://

Anything the registry doesn't publish, Docker Hub has:

ply run docker://mongo:7 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=dev

The image is pulled once, converted to a ply image, and cached. It's a fat image — self-contained, sharing nothing — which is why the native services are the primary path and docker:// is the fallback. See Importing from Docker.