Running dpndncY
Backups, upgrades, monitoring and hardening for an installation that is already up. The commands assume Docker Compose in /opt/dpndncy (as set up by deploy/install.sh); for Kubernetes, run the same steps with kubectl exec and your volume snapshot tooling.
#What to protect
All state is in the volume mounted at /app/data:
| Contains | Why it matters |
|---|---|
dpndncY.db (with -wal and -shm files) |
Everything: tenants, users, projects, scans, findings, settings, audit log |
licenses/ |
The installed licence |
history/, sast/, caches |
Scan artifacts. Large, and partly rebuildable |
Also keep your .env (Compose) or Helm values and Secret somewhere safe. APP_SECRET is in it: it encrypts the credentials stored in the database, so a restored database is only useful together with the same value.
#Backups
#Online backup of the database (no downtime)
SQLite can copy itself consistently while the service runs. This writes a copy inside the volume:
cd /opt/dpndncy
docker compose exec dpndncY node -e "
const fs = require('fs');
fs.mkdirSync('/app/data/backups', { recursive: true });
const name = '/app/data/backups/dpndncy-' + new Date().toISOString().replace(/[:.]/g, '-') + '.db';
new (require('better-sqlite3'))(process.env.DB_PATH || '/app/data/dpndncY.db')
.backup(name).then(() => console.log('wrote', name));
"
docker compose cp dpndncY:/app/data/backups ./backups # take it off the machineDo not copy dpndncY.db itself while the service is running; without its -wal file the copy can be stale or inconsistent.
#Whole-volume backup (a few seconds of downtime)
docker compose stop dpndncY
docker run --rm -v dpndncy_dpndncy-data:/data -v "$PWD":/backup alpine \
tar czf /backup/dpndncy-data-$(date +%F).tgz -C /data .
docker compose start dpndncY(docker volume ls shows the exact volume name; Compose prefixes it with the project folder name.)
Run one of these on a schedule, keep copies on other storage, and test a restore at least once.
#Restore
docker compose stop dpndncY
docker run --rm -v dpndncy_dpndncy-data:/data -v "$PWD":/backup alpine \
sh -c "rm -rf /data/* && tar xzf /backup/dpndncy-data-2026-11-06.tgz -C /data"
docker compose start dpndncYTo restore only the database from an online backup, stop the service, copy the file over /app/data/dpndncY.db, delete dpndncY.db-wal and dpndncY.db-shm, and start it.
#Upgrading
- Read the changelog for the versions between yours and the target.
- Take a backup.
- Change the image tag (
DPNDNCY_IMAGEin.env, orimage.tagin Helm) to the new version. docker compose pull && docker compose up -d(orhelm upgrade).- Check
/healthreports the new version and sign in.
The database is migrated automatically at start-up. A migrated database is not guaranteed to work with an older version, so to roll back, restore the backup from step 2 together with the old image tag.
Browser sessions end when the server restarts; API tokens do not. Schedule upgrades outside working hours.
#Health and monitoring
| What | How |
|---|---|
| Is it up | GET /health on the API address returns {"status":"ok","version":...,"uptime":...}. Use it for an uptime monitor; the container healthcheck and the Helm probes use it |
| Is it healthy | Settings, System health shows live resource use, including event-loop lag, and the scan queue. Rising lag means the machine is too small for the load |
| Logs | JSON lines on standard output: docker compose logs -f dpndncY, or kubectl logs. Ship them with your usual log agent |
| Who did what | Settings, Activity (the operator) shows the audit log: sign-ins, setting changes, policy changes, scans |
#Security checklist
- HTTPS on both hostnames. Never run the evaluation HTTP mode for real data.
ENABLE_BOOTSTRAP_SUPERADMIN=falseafter the first sign-in.- Two-step verification enforced for each tenant (Settings, Sign-in security), or single sign-on.
- A dedicated
analystservice user for pipelines, one API token per pipeline system, short lifetimes where practical, and revoke tokens when a person or system leaves. APP_SECRETgenerated randomly and stored in a secrets manager, not in a repository.- The image tag pinned to a version, and upgraded on purpose.
- The API hostname restricted by your firewall to the networks your runners and agents come from, if they are not on the public internet. Everything except
/healthand/releasesneeds a token, but there is no reason to expose it more widely than required. - Backups encrypted and stored off the machine.
- Outbound access limited to what is needed:
api.osv.dev,nvd.nist.gov,api.github.com, the package registries your projects use. Air-gapped sites need none (Running dpndncY without internet access (air-gapped)).
#Rotating secrets
dpndncY has three kinds of secret, and they rotate differently.
| Secret | What it protects | How it rotates |
|---|---|---|
| Signing key | Sessions and API tokens | Automatically, on a schedule, without signing anyone out |
APP_SECRET |
Credentials stored in the database (git tokens, integration configurations, Maven repository password) | With a tool, by hand, when you decide |
| Credentials you hold (operator password, API tokens, SMTP, NVD, GitHub, OIDC) | Access to dpndncY and to other systems | By hand, in the console or the other system |
#A schedule to adopt
| What | How often | How |
|---|---|---|
| Signing key | Every 90 days | Automatic. Set JWT_SECRET_ROTATION_DAYS=90 (the default in .env.example and the Helm chart) |
| API tokens | Every 90 to 365 days | Set API_TOKEN_MAX_DAYS so tokens expire; create the new token before the old one ends and update the CI secret |
APP_SECRET |
Yearly, and at once if it may have leaked | scripts/rotate-app-secret.js, below |
| Operator and user passwords | Per your policy | In the console. Keep ENABLE_BOOTSTRAP_SUPERADMIN=false, or a restart resets the operator password |
| SMTP, NVD, GitHub, OIDC secrets | Per the issuer's policy | Change the value and restart |
| TLS certificates | Automatic | Caddy renews them; with Kubernetes, cert-manager does |
#The signing key
The signing key lives in a key ring in the database. A rotation adds a new key and retires the old one, which keeps verifying existing tokens until its window (JWT_SECRET_VERIFY_WINDOW_DAYS, default 400 days) ends and it is deleted. Nobody is signed out. The window is long so that a 365-day API token outlives a rotation.
Check it (owner role):
curl -H "Authorization: Bearer $TOKEN" https://api.dpndncy.example.com/api/admin/auth-secretsThe answer shows the active version, its age, the next rotation and any configuration warning. Rotate now:
curl -X POST -H "Authorization: Bearer $TOKEN" https://api.dpndncy.example.com/api/admin/auth-secrets/rotateIf a key may have leaked, rotate and end every older session and API token at once:
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"verifyWindowDays":0,"revokeOlder":true}' \
https://api.dpndncy.example.com/api/admin/auth-secrets/rotateEveryone is signed out and every API token stops working, so pipelines need new tokens. Each rotation is written to the audit log.
#APP_SECRET
APP_SECRET encrypts credentials at rest. It does not sign anything, so replacing it signs nobody out, but changing the value without re-encrypting would make the stored credentials unreadable. Use the tool:
Stop dpndncY. The database must not be written while the tool runs.
Dry run, to see what it would change:
docker compose run --rm --no-deps --entrypoint node dpndncY \ scripts/rotate-app-secret.js --db /app/data/dpndncY.db --old env:APP_SECRET --new generateThe service already has the data volume and the current
APP_SECRET, so the command needs nothing else. On Kubernetes, run the same command in a Job that mounts the data volume and the Secret.Apply. The tool writes a backup next to the database, re-encrypts, and then reads every value back with the new secret. It stops without changing anything if a value cannot be read with the old secret.
docker compose run --rm --no-deps --entrypoint node dpndncY \ scripts/rotate-app-secret.js --db /app/data/dpndncY.db --old env:APP_SECRET --new generate --applyWith
--new generatethe new secret is printed once at the end; store it before you do anything else.Set
APP_SECRETto the new value (.env, Secret, or secrets manager) and start dpndncY.If anything looks wrong, restore the backup the tool printed and put the old
APP_SECRETback.
An installation that has no stored git tokens, integration configurations or Maven passwords has nothing to re-encrypt, and the tool says so.
#Licence
Settings, License. A new installation runs a 30-day trial. A licence is applied by pasting a key, entering an activation code, activating online, or importing a file (for air-gapped sites). After a licence expires there is a 7-day grace period, then the installation is read-only until it is renewed.
#Sizing and capacity
One container uses the memory the scans need; 4 GB is a floor, 8 GB comfortable. Scan time grows with repository size and the number of ecosystems. Concurrent scans queue rather than fail. If System health shows a growing queue or lag, give the machine more CPU and memory. There is no horizontal scaling: the database is a single SQLite file, so size up the one machine instead.