Getting started

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.

SectionGetting started
UpdatedSeptember 27, 2026
EditionSelf-hosted Enterprise, public beta

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

ItemMinimumRecommended
Operating systemUbuntu 24.04 LTS or Ubuntu 26.04, x86_64, cgroup v2Ubuntu 24.04 LTS
DockerDocker Engine 24+ with the Compose plugin 2.20+ (docker compose)Docker 27+ / Compose 2.30+
CPU and memory4 vCPU, 16 GB8 vCPU, 32 GB for several concurrent scans (WORKER_REPLICAS, 4 GiB per worker)
Disk40 GB SSD100 GB or more; evidence grows with retained scans
Network inTCP 443 from your users; TCP 80 and 443 from the internet if you use ACME certificatesRestrict 443 to corporate ranges; ACME still needs 80 or 443 reachable by the CA
Network outDocker 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 CAThe same; nothing about your installation is sent anywhere at run time
HostnameA DNS name pointing at the host. Caddy obtains and renews a Let's Encrypt certificate automaticallyA corporate hostname; use your own certificate through Caddy if ACME is not allowed
Operator accountA non-root user in the docker groupThe same
Verification toolscosign and the GitHub CLI (gh), used by the installer to check the image signature and build attestationsThe same
From the vendorThe Enterprise image reference (a digest) and a pull token, the delivery kit it carries, and a license for your installation id
From the public releaseThe 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.

<details><summary>What the installer does, step by step (manual install)</summary>
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.

</details>

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-stopped only when a container exits on its own; after docker kill or docker stop, run docker compose -f compose.json up -d to bring the worker back. A worker that crashes is restarted automatically. Either way the scan it was executing ends failed with 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) and secrets/tls/key.pem (0400, owner 10001) and run docker compose -f compose.json restart https; Caddy serves the new pair within seconds.
  • prepare needs the runtime directory as the installer produced it (.env plus release-image-lock.env); an engine source checkout or a bare Compose file is refused.
  • The runtime files must be world-readable. A db/init.sql with 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+rX the 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:10001 to apply.