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.
What you get on the new layout
Section titled “What you get on the new layout”- Zero-downtime updates.
./manage.sh updatestarts a new app container next to the old one, health-checks it, and only cuts over once it responds. Nodocker compose down, no dropped requests. - A stable
APP_KEY. It is pinned in.envfrom the first./init.shrun, 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
*.disttemplates alongside your customised files. Every update prints aWARNINGline 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 doctorchecks 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.jsonis bind-mounted into the update-watcher container. A singledocker loginon 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.
Before you start
Section titled “Before you start”- Back up
.envanddocker/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:latestshould succeed on the host before you start.
Migration steps
Section titled “Migration steps”1. Pull the new layout
Section titled “1. Pull the new layout”From your existing pentestpad-docker checkout:
git fetch origingit checkout maingit pullAny 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.
2. Run ./init.sh
Section titled “2. Run ./init.sh”./init.shinit.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.ymlrequires —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), andAPP_KEY(recovered from the running app container if it is up, freshly generated and pinned otherwise). - Copies any missing
*.distfile to its active name. Existing files are never overwritten. - Retires the old host-level
pentestpad-update-watchersystemd unit if it is present — the watcher is now a compose service. - Validates access to
repository.pentestpad.comand prompts fordocker loginif the token is missing or stale.
3. Reconcile customised config files
Section titled “3. Reconcile customised config files”Compare anything you had edited against the new template:
diff docker/nginx/nginx.conf.dist docker/nginx/nginx.confdiff docker/nginx/proxy.conf.dist docker/nginx/proxy.confdiff docker/php/php-fpm.conf.dist docker/php/php-fpm.confdiff 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.
4. Bring the stack up
Section titled “4. Bring the stack up”./manage.sh startThis 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.
5. Verify
Section titled “5. Verify”./manage.sh doctorExits non-zero on any failure. Then:
./manage.sh infofor a live state summary — version, access URL, per-service health, TLS expiry, watcher heartbeat, signal queue.
Command translation
Section titled “Command translation”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.
What a successful update looks like
Section titled “What a successful update looks like”./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.0From github.com:PentestPad/pentestpad-docker d306b4d..bc8426e main -> origin/mainFast-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.0If 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.confThese are advisory. The update proceeds; review the diff afterwards.
What doctor catches
Section titled “What doctor catches”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:
* * * * * cd /opt/pentestpad-docker && ./manage.sh doctor >> /var/log/pentestpad-doctor.log 2>&1Troubleshooting
Section titled “Troubleshooting”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:
HOST_DOCKER_CONFIGis set in.env. Re-run./init.shif not; it captures the value automatically.- The path it points at contains a
config.jsonwith an auth entry forrepository.pentestpad.com. Adocker login repository.pentestpad.comon the host, as the same user who ran./init.sh, fixes this. - The config does not use
credsStoreorcredHelpers. Those store credentials outsideconfig.jsonin a system keyring or external binary the watcher container cannot reach../manage.sh doctorflags this explicitly.
HOST_DOCKER_CONFIG is not set in .env
Section titled “HOST_DOCKER_CONFIG is not set in .env”Compose refuses to start the watcher without this. Re-run ./init.sh.
doctor reports a stale watcher heartbeat
Section titled “doctor reports a stale watcher heartbeat”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.
Rollback
Section titled “Rollback”If you need to return to the previous layout:
git checkout <previous-commit>docker compose stopdocker compose up -dData 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.