Skip to content

Migrating to the script-driven deployment

If your pentestpad-docker checkout has no init.sh, manage.sh, or update.sh in the repo root, it is on the legacy layout. Migrating is in-place and non-destructive: your database, uploaded reports, TLS certificates, OAuth signing keys, and existing .env values are preserved. You run through this once and every future update becomes a single command.

  • Zero-downtime updates. ./manage.sh update starts a new app container next to the old one, health-checks it, and only cuts over once it responds. No docker compose down, no dropped requests.
  • A stable APP_KEY. It is pinned in .env from the first ./init.sh run, so sessions and encrypted data survive updates instead of rotating each time the app container restarts.
  • Config drift you can actually see. Shipped configs live as *.dist templates alongside your customised files. Every update prints a WARNING line for each file that diverges, so you can merge upstream changes deliberately rather than notice weeks later that a fix was missed.
  • One command to validate the install. ./manage.sh doctor checks env keys, TLS cert pair and expiry, compose config, registry auth (host and watcher-container), and watcher heartbeat. It exits non-zero on failure, so cron and monitoring pick up drift before you do.
  • Registry auth shared with the host. ~/.docker/config.json is bind-mounted into the update-watcher container. A single docker login on the host propagates immediately — no repeated logins, no half-updated stack.

Any in-app “Update PentestPad” trigger goes through the same path via the update-watcher service, so operators who prefer the shell and admins who prefer the UI end up on the same rollout code.

  • Back up .env and docker/nginx/certs/. These are the only unversioned files that matter if you have to roll back manually.
  • Snapshot your named volumes (postgres_data, pentestpad_data, template_builder, redis_data, clamav_db) if your host supports it.
  • Confirm registry access. docker pull repository.pentestpad.com/pentestpad/app:latest should succeed on the host before you start.

From your existing pentestpad-docker checkout:

Terminal window
git fetch origin
git checkout main
git pull

Any customisations to docker/nginx/nginx.conf, docker/nginx/proxy.conf, docker/php/php-fpm.conf, or docker/php/php.ini are preserved — the new layout ships *.dist templates alongside them rather than overwriting the active files.

Terminal window
./init.sh

init.sh is fully idempotent. On an existing install it detects the state, keeps every value already in .env, and skips the fresh-install prompts. It also:

  • Adds three keys the current docker-compose.yml requires — INSTALL_DIR (absolute path of the repo, mounted back into the watcher), HOST_DOCKER_CONFIG (usually $HOME/.docker, bind-mounted so registry auth is shared), and APP_KEY (recovered from the running app container if it is up, freshly generated and pinned otherwise).
  • Copies any missing *.dist file to its active name. Existing files are never overwritten.
  • Retires the old host-level pentestpad-update-watcher systemd unit if it is present — the watcher is now a compose service.
  • Validates access to repository.pentestpad.com and prompts for docker login if the token is missing or stale.

Compare anything you had edited against the new template:

Terminal window
diff docker/nginx/nginx.conf.dist docker/nginx/nginx.conf
diff docker/nginx/proxy.conf.dist docker/nginx/proxy.conf
diff docker/php/php-fpm.conf.dist docker/php/php-fpm.conf
diff docker/php/php.ini.dist docker/php/php.ini

./manage.sh update prints a WARNING line for each file that diverges — that is the ongoing signal, not a bug. Merge upstream changes into your active file when appropriate.

Terminal window
./manage.sh start

This is docker compose up -d under the hood. Compose only recreates containers whose definition changed: the update-watcher starts fresh, everything else restarts onto the current compose config.

Terminal window
./manage.sh doctor

Exits non-zero on any failure. Then:

Terminal window
./manage.sh info

for a live state summary — version, access URL, per-service health, TLS expiry, watcher heartbeat, signal queue.

The everyday commands map one-to-one. Anything not listed is still available via docker compose ... directly.

Legacy Current
docker compose up -d ./manage.sh start
docker compose stop ./manage.sh stop
docker compose restart ./manage.sh restart
docker compose ps ./manage.sh status (alias ps)
docker compose logs -f app ./manage.sh logs (logs <service> for others)
docker compose exec app bash ./manage.sh shell (shell <service> for others)
docker compose down && docker pull … && docker compose up -d ./manage.sh update
Reading .version inside the container ./manage.sh version

Two commands have no legacy equivalent: ./manage.sh info and ./manage.sh doctor, covered above.

./manage.sh update runs ./update.sh, which pulls new images, starts a new app container beside the old one, health-checks it, and only cuts over once it responds. No request is dropped mid-update.

Updating PentestPad...
Current version: v1.7.0
From github.com:PentestPad/pentestpad-docker
d306b4d..bc8426e main -> origin/main
Fast-forward
docker-compose.yml | 3 +++
Checking for config changes...
ok all config files match their .dist template
[+] Pulling 6/6
✔ app Pulled
✔ template-builder Pulled
...
Starting new app container (current one keeps serving)...
Waiting for the new app container to become healthy (up to 120s)...
New app container is healthy - retiring the old one...
Updated: v1.7.0 -> v1.8.0

If a customised config has diverged from the shipped .dist, you will see:

Checking for config changes...
WARNING: ./docker/nginx/nginx.conf differs from updated default.
Review: diff ./docker/nginx/nginx.conf.dist ./docker/nginx/nginx.conf

These are advisory. The update proceeds; review the diff afterwards.

Sample of a failed run — the two lines that matter are the ones that are not ok:

Running diagnostics...
ok docker CLI available
ok docker compose plugin present
ok .env exists
...
FAIL cannot pull from repository.pentestpad.com - run: docker login repository.pentestpad.com
warn TLS cert expires in 12 days (renew soon)
ok update-watcher heartbeat is fresh (7s)
Result: 1 error(s), 1 warning(s)

Safe to wire into cron because it exits non-zero on any FAIL:

Terminal window
* * * * * cd /opt/pentestpad-docker && ./manage.sh doctor >> /var/log/pentestpad-doctor.log 2>&1

no basic auth credentials during an update

Section titled “no basic auth credentials during an update”

The watcher container has its own docker client — registry auth is a client-side concern and is not carried by the mounted docker socket. Three things to check, in order:

  1. HOST_DOCKER_CONFIG is set in .env. Re-run ./init.sh if not; it captures the value automatically.
  2. The path it points at contains a config.json with an auth entry for repository.pentestpad.com. A docker login repository.pentestpad.com on the host, as the same user who ran ./init.sh, fixes this.
  3. The config does not use credsStore or credHelpers. Those store credentials outside config.json in a system keyring or external binary the watcher container cannot reach. ./manage.sh doctor flags this explicitly.

Compose refuses to start the watcher without this. Re-run ./init.sh.

Check ./manage.sh logs update-watcher. The usual cause is a docker-compose.override.yml that changes the compose project name — the watcher expects to find the running stack under the same project as itself.

If you need to return to the previous layout:

Terminal window
git checkout <previous-commit>
docker compose stop
docker compose up -d

Data volumes are untouched throughout, so a rollback is a compose-config change rather than a data restore. The one caveat is APP_KEY: if init.sh had to generate a fresh one — because the app container was down at the time of step 2 — sessions issued under the new key will not decode on the old layout. Log users out and back in after rolling back if this applies.