Documentation

Install your Docker updater.

PullPilot is a self-hosted Docker Compose updater for homelabs — status, logs and deployment modes from a single UI. Here is how to install it, lay out your stacks and lock it down.

Quick install

There is no .env to write. PullPilot ships with working defaults — one container, a SQLite file and your Compose folder — and asks for a username and a password in the browser on first run.

bash
$ sudo mkdir -p /srv/docker-stacks
$ mkdir -p ~/pullpilot && cd ~/pullpilot
$ curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/KN990x/PullPilot/main/docker-compose.yml
$ docker compose up -d

Then open http://your-server-ip:8000 (or the host port from PULLPILOT_PORT). That is the whole installation.

First run

The browser shows a setup wizard: pick a username and a password, type the password twice, and you are in. Credentials are stored hashed (scrypt) in PullPilot's own database, and the session signing secret is generated and persisted for you — there is nothing to configure and no secret to put in a file.

Between the first start and completing the wizard, anyone who can reach the instance can claim it. Complete it right away, and do not publish the port before you have.

After startup

Each project is a subfolder under your stacks root with a docker-compose.yml (or docker-compose.yaml) inside. Keep PullPilot's own compose folder outside that tree when you can.

/srv/docker-stacks/          # default STACKS_PATH
├── plex/
│   └── docker-compose.yml
├── uptime-kuma/
│   └── docker-compose.yml
└── ...

Folders named pullpilot, pullpilot-ui, docker-updater and data are ignored under the stacks root.

Using a different stacks location

Create the folder on the host, then add an .env next to docker-compose.yml pointing at it. The path must be identical on the host and inside the container, because it is bind-mounted one-to-one. After any .env change, run docker compose up -d.

.env
STACKS_PATH=/absolute/path/to/stacks

Usage guide

Dashboard

A card per project — status, per-project update, and Full stop and Exclude toggles.

Update All

Scans non-excluded projects, runs git pull where applicable, recreates containers; summary in History. It answers immediately and works in the background.

Exclude

Means never update this automatically: skipped by Update All, its own update button disabled, and a scheduled task pointing at it will not run. Only removing the toggle brings it back.

Schedule

Cron or one-off tasks, per project or for everything at once. Nothing is scheduled until you create it — the form just suggests 04:00.

What happens when an update fails

Before pulling, PullPilot records which local image each service is currently using. It then pulls, recreates the stack, and waits up to 60 seconds for the containers to come up healthy. If the deploy or the healthcheck fails it puts those image tags back, reverts the compose file when the stack is a Git clone, and brings the stack up again either way — a failed update does not leave it down.

Two limits worth knowing: services built from a Dockerfile (build:) are rebuilt rather than reverted, and an image that had never been pulled on this host has no previous version to go back to.

Updates run in the background. Both Update All and a single-project update answer immediately and do the work behind the request, so a reverse proxy with a short read timeout will not report a working deploy as failed. A second update of the same stack is refused while the first runs.

Environment variables

All four are optional. Copy .env.example to .env next to docker-compose.yml only if one of these needs changing, then run docker compose up -d.

VariableDefaultDescription
STACKS_PATH/srv/docker-stacksStacks folder: one subfolder per project, each with its docker-compose.yml. Bind-mounted at the same absolute path on host and container.
PULLPILOT_PORT8000Published host port for the UI.
TZUTCContainer timezone — the one scheduled tasks run on.
PUBLIC_URL(unset)Only behind a reverse proxy. An https:// value marks the session cookie Secure and makes the login rate limit read X-Forwarded-For.

Everything that used to be configurable — the session secret, the cookie's SameSite, CORS origins, command timeouts, the login rate limit — is now either generated automatically or a constant with a sensible value. If you had DOCKER_ROOT_PATH or PROJECTS_ROOT in an older .env, both still work as aliases of STACKS_PATH.

Security notes

Treat PullPilot like root access — the Docker socket is mounted. Never expose port 8000 to the public internet without TLS, a strong password, and ideally an extra auth layer (Authelia, Authentik…).

Updates and scheduled jobs only run under the resolved stacks root. Database paths outside that tree are rejected.

Credentials live hashed in the database and are created through the setup wizard. There is no environment variable that can create, replace or bypass them. Login is rate-limited to 15 attempts per 5 minutes per IP.

Behind a TLS-terminating proxy, set PUBLIC_URL to the address you actually use, for example https://pullpilot.example.com. Leave it unset on a plain LAN install. The account button in the header changes username and password — doing so signs out every other device.

Run one Uvicorn worker per instance. The signing secret is shared across workers — it lives in a file inside the data volume — but the scheduler, the login rate limit and the update state are per process, so more than one worker means duplicated scheduled updates.

The published image is ghcr.io/kn990x/pullpilot:latest. If you still pin ghcr.io/kernel-nomad/pullpilot, update your compose file — that path is no longer maintained.