Install on a Linux host
The supported deployment of self-hosted ShakerScan Enterprise, Docker Compose on one Linux host you operate, from requirements through install, start at boot, firewall, backup and restore, and upgrade.
The supported production posture is Docker Compose on one Linux host you operate, whether a cloud VM, a VPS or an on-premises virtual machine, on x86_64 (the images are linux/amd64; there is no ARM build). This guide was executed end to end on clean Ubuntu 24.04 hosts with the one-command installer, including a reboot and a backup; Ubuntu 26.04 is validated in continuous operation. Other distributions are not supported yet. Kubernetes and ECS layouts exist as evaluations and are not the supported target.
1. Requirements
| Item | Minimum | Recommended |
|---|---|---|
| Operating system | Ubuntu 24.04 LTS or Ubuntu 26.04, x86_64, cgroup v2 | Ubuntu 24.04 LTS |
| Docker | Docker Engine 24+ with the Compose plugin 2.20+ (docker compose) | Docker 27+ / Compose 2.30+ |
| CPU and memory | 4 vCPU, 16 GB | 8 vCPU, 32 GB for several concurrent scans (WORKER_REPLICAS, 4 GiB per worker) |
| Disk | 40 GB SSD | 100 GB or more; evidence grows with retained scans |
| Network in | TCP 443 from your users; TCP 80 and 443 from the internet if you use ACME certificates | Restrict 443 to corporate ranges; ACME still needs 80 or 443 reachable by the CA |
| Network out | Docker Hub (engine images), the vendor registry (gateway image), GitHub (the public engine release catalog and installer, attestation checks) and the Sigstore services (signature checks) at install and update time, the targets you are authorized to scan, the ACME CA | The same; nothing about your installation is sent anywhere at run time |
| Hostname | A DNS name pointing at the host. Caddy obtains and renews a Let's Encrypt certificate automatically | A corporate hostname; use your own certificate through Caddy if ACME is not allowed |
| Operator account | A non-root user in the docker group | The same |
| Verification tools | cosign and the GitHub CLI (gh), used by the installer to check the image signature and build attestations | The same |
| From the vendor | The Enterprise image reference (a digest) and a pull token, the delivery kit it carries, and a license for your installation id | |
| From the public release | The installed engine runtime directory produced by the public installer (step 2 below) |
2. Install
Run these as root on the host, with the image reference, pull token and licence file from your delivery note. The installer checks the host first and lists everything missing before it changes anything.
# 1. Sign in to the registry with your pull token and extract the delivery kit from the image.
echo "$PULL_TOKEN" | docker login ghcr.io -u <pull user> --password-stdin
IMAGE=ghcr.io/andriyze/shakerscan-enterprise@sha256:<digest>
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/out" "$IMAGE" python -m shakerscan_saas.enterprise.cli kit --output /out/shakerscan-kit
# 2. Install. GH_TOKEN lets the attestation checks run without GitHub's anonymous rate limit.
GH_TOKEN="$PULL_TOKEN" shakerscan-kit/self-hosted/install.sh --image "$IMAGE" \
--public-url https://scanner.example.com --installation-id acme-prod \
--organization 'Acme Security' --license acme-prod-license.jwt --admin admin@example.com
In about five minutes on a reference host it: verifies the host (Docker and Compose versions,
memory, disk, ports 80 and 443, DNS for your hostname, registry access, and your licence's
signature, installation id and validity); installs the engine runtime at the newest release this
gateway has reviewed, from the installer at the exact commit its certification receipt names,
and checks the installed image lock against the published one; verifies the gateway image's
signature and SBOM and every engine image's attestation; renders the deployment under
/srv/shakerscan-enterprise; installs your licence; starts the stack and waits for it to be
healthy; creates the first administrator; and installs three systemd units (the stack at boot, a
daily backup, and the engine updater behind the console's update button). It finishes by
checking that the gateway is using your licence and prints where everything is:
Sign in: https://scanner.example.com/_enterprise/login as admin@example.com
Initial password: /etc/shakerscan/initial-admin-password
Engine: 2.5.4 (update it later from the console's Deployment card)
Backups: daily to /var/backups/shakerscan, passphrase in /etc/shakerscan/backup-passphrase
Then open the sign-in page, sign in, enroll an authenticator app (required for administrators), change the initial password, and configure single sign-on (Okta guide). Copy the backup passphrase and the backups off the host.
Options, all optional: --engine-version, --max-concurrent-scans (at most your licensed
slots), --worker-replicas, --private-network-targets allow|refuse, --tls-certificate and
--tls-key for a certificate from your own CA, --outbound-proxy, --hunt off (Hunt is on
for operators by default), --admin-password-file, and the directories (--engine-dir,
--deployment, --kit-dir, --backup-dir). --preflight-only runs the checks and stops.
Running the installer again resumes an interrupted install; it never touches a deployment it did
not create.
docker pull "$IMAGE"
shakerscan-kit/self-hosted/verify.sh "$IMAGE" /srv/shakerscan-public-release
SHAKERSCAN_HOME=/srv/shakerscan-public-release SHAKERSCAN_START=0 sh -c 'curl -fsSL https://install.shakerscan.com | sh'
chmod -R a+rX /srv/shakerscan-public-release && chmod 600 /srv/shakerscan-public-release/.env
shakerscan-kit/self-hosted/prepare.sh "$IMAGE" /srv/shakerscan-public-release \
/srv/shakerscan-enterprise https://scanner.example.com acme-prod 'Acme Security'
cp license.jwt /srv/shakerscan-enterprise/secrets/license.jwt
sed -i 's|^SHAKERSCAN_ENTERPRISE_LICENSE_MODE=.*|SHAKERSCAN_ENTERPRISE_LICENSE_MODE=signed|' /srv/shakerscan-enterprise/enterprise.env
echo 'SHAKERSCAN_ENTERPRISE_LICENSE_FILE=/run/enterprise-secrets/license.jwt' >> /srv/shakerscan-enterprise/enterprise.env
chown 10001:10001 /srv/shakerscan-enterprise/secrets/* && chmod 0440 /srv/shakerscan-enterprise/secrets/*
cd /srv/shakerscan-enterprise && docker compose -f compose.json up -d --wait
docker compose -f compose.json exec enterprise python -m shakerscan_saas.enterprise.cli bootstrap --username admin@example.com
docker compose -f compose.json exec enterprise python -m shakerscan_saas.enterprise.cli check
plus the systemd units in self-hosted/systemd/ with your paths.
3. Start at boot
The installer installs and enables this unit (and the backup and updater timers) for you; it is shown here for a manual install.
# /etc/systemd/system/shakerscan-enterprise.service
[Unit]
Description=ShakerScan Enterprise (Docker Compose)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/shakerscan-enterprise
ExecStart=/usr/bin/docker compose -f /srv/shakerscan-enterprise/compose.json up -d --remove-orphans
ExecStop=/usr/bin/docker compose -f /srv/shakerscan-enterprise/compose.json down
TimeoutStartSec=600
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now shakerscan-enterprise.service
After a reboot the unit is active, all containers come up healthy within about 30 seconds, the certificate is reused from its volume, and identities and scans are intact.
4. Firewall
Allow inbound TCP 443 (and 80 for ACME's HTTP challenge and the redirect) and your SSH port; deny
everything else. The engine, database and cache publish no ports; only Caddy does. With ufw:
sudo ufw allow 22/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw enable
On a cloud VM apply the same rules in the security group. Docker manages its own iptables
chains; do not expect ufw alone to block published container ports.
Outbound, the host needs Docker Hub and the vendor registry for image pulls, GitHub (the public
engine release catalog and installer) and the Sigstore services (cosign verification) at install
and update time, Let's Encrypt unless prepare was given your certificate, your identity
provider if SSO is configured, and the scan targets; the security overview lists
the exact destinations. Nothing is fetched at scan time. prepare --outbound-proxy http://proxy:3128 routes the ACME client and the gateway's identity-provider calls through a
corporate proxy; scan traffic to targets is never proxied, and image pulls use the Docker
daemon's own proxy settings.
5. Backup and restore
self-hosted/backup.sh and self-hosted/restore.sh in the kit work for an unprivileged
operator: the results directory and the secrets belong to the gateway's user after the first
start, so the scripts read and write them through a helper container with no network. Run
backups from cron or a systemd timer; keep the backup directory encrypted at rest and off the
host.
# /etc/systemd/system/shakerscan-backup.service
[Service]
Type=oneshot
Environment=BACKUP_PASSPHRASE_FILE=/etc/shakerscan/backup-passphrase
ExecStart=/opt/shakerscan-kit/self-hosted/backup.sh /srv/shakerscan-enterprise /var/backups/shakerscan/%i
# /etc/systemd/system/shakerscan-backup.timer
[Timer]
OnCalendar=daily
restore.sh refuses to run over existing identity state, a non-empty results directory or an
engine database that already holds scans (--replace-database overrides the last check
deliberately). A full drill, backup while running, docker compose down -v, restore, start,
brings back the enrolled authenticator, the signed license, completed scans, their findings and
evidence. Note that down -v also removes Caddy's certificate volume, so the certificate is
re-issued on the next start; Let's Encrypt allows five duplicate certificates per week per name.
Keep the enterprise-tls-data volume if you plan repeated restores.
Restore onto another host or directory
The backup carries the engine runtime's .env (the datastore passwords the volumes were
initialized with) and its digest lock as engine.env and engine-lock.env. On the new host, as
the operator user:
# 1. the same engine version, installed without starting it
SHAKERSCAN_HOME=/srv/shakerscan-public-release SHAKERSCAN_INSTALL_VERSION=<engine version from the backup manifest> SHAKERSCAN_START=0 \
sh -c 'curl -fsSL https://install.shakerscan.com | sh'
cp <backup>/engine.env /srv/shakerscan-public-release/.env
chmod -R a+rX /srv/shakerscan-public-release && chmod 600 /srv/shakerscan-public-release/.env
# 2. the deployment files, re-rendered at the new path (restore.sh extracts them and stops here)
BACKUP_PASSPHRASE_FILE=... self-hosted/restore.sh /srv/shakerscan-enterprise <backup> || true
self-hosted/upgrade.sh <enterprise-image> /srv/shakerscan-public-release /srv/shakerscan-enterprise
# 3. the data
BACKUP_PASSPHRASE_FILE=... self-hosted/restore.sh /srv/shakerscan-enterprise <backup>
docker compose -f /srv/shakerscan-enterprise/compose.json up -d
The restored gateway keeps the original public URL; point DNS at the new host (or re-prepare with a new URL) before people use it.
6. Upgrade
Two things can change: the public engine release and the Enterprise gateway image. Upgrade the
engine runtime in place with the public installer (it keeps the .env the deployment was
initialized with and refreshes the digest lock), then re-render with the gateway image you were
given (the current one if only the engine changed), pull, and restart:
SHAKERSCAN_HOME=/srv/shakerscan-public-release SHAKERSCAN_START=0 sh -c 'curl -fsSL https://install.shakerscan.com | sh'
chmod -R a+rX /srv/shakerscan-public-release && chmod 600 /srv/shakerscan-public-release/.env
self-hosted/upgrade.sh ghcr.io/andriyze/shakerscan-enterprise@sha256:<digest> /srv/shakerscan-public-release /srv/shakerscan-enterprise
docker compose -f /srv/shakerscan-enterprise/compose.json pull
docker compose -f /srv/shakerscan-enterprise/compose.json up -d --remove-orphans
Back up before upgrading. upgrade.sh re-renders compose.json, the Caddyfile and
deployment.json for the new release with the same project and volume names, so engine data
and identity state persist; it does not touch enterprise.env, secrets/ or results/. It
refuses an engine downgrade, a major engine version change and a release that changes the volume
layout, and it warns when the target engine release has not yet been reviewed for the gateway.
The previous rendering is kept under previous/<timestamp>/; rollback is moving those files
back and running the same up command. A release directory installed afresh carries different
datastore passwords than the running volumes; upgrade refuses it with a message naming the
variable. In a measured upgrade with real data, PostgreSQL, Redis and Caddy were not recreated
and the stack was healthy again in under a minute with scans, findings, evidence, identities and
the license intact.
7. Things worth knowing
- A replica stopped by hand stays down. Docker applies
restart: unless-stoppedonly when a container exits on its own; afterdocker killordocker stop, rundocker compose -f compose.json up -dto bring the worker back. A worker that crashes is restarted automatically. Either way the scan it was executing endsfailedwith a message that replay was withheld because requests may already have been sent; re-run it. - Rotating your own certificate: replace
secrets/tls/cert.pem(mode 0444) andsecrets/tls/key.pem(0400, owner 10001) and rundocker compose -f compose.json restart https; Caddy serves the new pair within seconds. prepareneeds the runtime directory as the installer produced it (.envplusrelease-image-lock.env); an engine source checkout or a bare Compose file is refused.- The runtime files must be world-readable. A
db/init.sqlwith mode 0600 is silently skipped by PostgreSQL's first start, the volume is then marked initialized without a schema, and the API crash-loops. Fix:chmod -R a+rXthe runtime directory,docker compose down -v, start again. - A secret the gateway cannot read (wrong owner or mode) stops it with a message naming the
chown 10001:10001to apply.