Deployment

Install on Microsoft Azure

Deploy ScanSuite Teams into your own Azure subscription on Container Apps with Terraform, with installation examples.

ScanSuite Teams can run in your own Azure subscription instead of on a server. A Terraform configuration in the azure folder of the installation repository builds everything with one command and removes it with another. The application runs on Azure Container Apps with managed services around it, so there is no virtual machine to patch.

Scope: static analysis

Container Apps has no Docker daemon, so the scanners that run as containers of their own — dynamic web scanning (DAST) and infrastructure scanning — do not run here. Code (SAST), dependency (SCA), infrastructure-as-code, secrets and AI code analysis all work. For dynamic scans, install on a server: Install ScanSuite Teams.

What gets built

Everything lives in one resource group that the deployment creates. Secrets are generated on the first deploy and kept in Key Vault. The database is reachable only from inside the virtual network, and the artefact storage only from the Container Apps subnet, without storage keys.

PartHow it runs
Web UI and APIA container app with managed TLS; its ingress admits only your address ranges.
Background workersContainer apps: housekeeping and recovery of interrupted scans, the scheduler, Redis for the task queue, and the sandbox for AI proof-of-concept checks, started on demand.
ScansOne Container Apps job execution per scan, 4 vCPU and 8 GiB, gone when the scan ends.
Database migrationA Container Apps job that deploy.sh runs.
DatabaseAzure Database for PostgreSQL 16 (Flexible Server), private, with seven days of backups.
Scan artefactsBlob Storage, reached with a managed identity, earlier versions kept for 30 days.
ImagesYour own Azure Container Registry: the installation never pulls from the internet at run time.
Logs and alertsLog Analytics, with alerts on errors, failed scans and database load.

1. Before you start

  • An Azure subscription where you are Owner, or Contributor together with Role Based Access Control Administrator: the deployment creates managed identities, a custom role and role assignments, so Contributor alone is not enough.
  • The tools, on Linux, macOS, or Windows with WSL or Git Bash: the Azure CLI (az) 2.60 or newer, Terraform 1.6 or newer, and git and bash.
  • From your ScanSuite delivery: the licence file (<name>_<code>.lic; the code is also the image tag) and the registry user name and access token, used to copy the images into your subscription.
  • Your public IPv4 address or ranges, the ones allowed to open the web UI. Ask for IPv4 explicitly, since many connections report an IPv6 address otherwise: curl -4 -s https://ifconfig.me.

2. Deploy

Get the installation files, add your licence, and change to the azure folder:

bash
git clone https://github.com/cepxeo/scansuite.git
cp /path/to/<name>_<code>.lic scansuite/key/
cd scansuite/azure

Sign in to Azure and note the subscription to deploy into:

bash
az login
az account list -o table

Copy the example settings and edit the copy. Section 3 has complete examples.

Shell
cp terraform.tfvars.example terraform.tfvars
SettingWhat to put
subscription_idThe subscription ID from az account list.
locationThe region, such as germanywestcentral, swedencentral or northeurope.
resource_group_nameA new resource group name. The deployment creates it, so it must not exist yet.
web_allowed_cidrsThe address ranges allowed to open the UI, such as ["203.0.113.10/32"].
image_tagYour licence code, the <code> in <name>_<code>.lic.
alert_emailOptional. Where alerts go.

Give the registry credentials to the shell — they are used only to copy the images and never saved — and run the deployment:

bash
export DOCKERHUB_USERNAME='<registry user name>'
export DOCKERHUB_TOKEN='<registry access token>'
./deploy.sh

A first run takes 30 to 40 minutes, most of it Azure building the Container Apps environment. The script checks your tools, sign-in and licence, and whether the region lets your subscription create PostgreSQL, before creating anything; builds the network, database, Key Vault, storage, registry, the Container Apps environment and Redis; copies the ScanSuite images into your registry; starts the web UI and workers and runs the database migration. When it ends with Done, the installation is running and the url output is its address. terraform output prints the outputs again at any time.

If Docker on the same machine already holds the images for your licence code, for example from a server installation, deploy.sh pushes those local copies instead of copying from the registry; remove them first if they may be out of date.

3. Installation examples

Examples A and B are complete terraform.tfvars files; C to E are lines to add to one of them. The registry credentials always come from the shell, as in section 2.

A. A trial installation

The small dev sizes, the UI open to one address:

terraform.tfvars
subscription_id     = "00000000-0000-0000-0000-000000000000"
location            = "swedencentral"
resource_group_name = "rg-scansuite-trial"
environment         = "dev"

image_tag         = "a1b2c3"
web_allowed_cidrs = ["203.0.113.10/32"]
timezone          = "Europe/Berlin"

# Cold start instead of an always-warm UI: the first visit after a quiet
# period waits about a minute.
web_min_replicas = 0

When the trial is over, ./destroy.sh removes the whole resource group.

B. A production installation

A zone-redundant database with a standby, zone-redundant storage, a Premium registry, Key Vault purge protection and a dedicated compute profile for the scans, plus a fixed outbound address and unlimited logs:

terraform.tfvars
subscription_id     = "00000000-0000-0000-0000-000000000000"
location            = "germanywestcentral"
resource_group_name = "rg-scansuite"
environment         = "prod"

image_tag         = "a1b2c3"
web_allowed_cidrs = ["198.51.100.0/24", "203.0.113.10/32"]   # office and VPN
timezone          = "Europe/Berlin"
alert_email       = "secops@acme.example"

postgres_sku       = "GP_Standard_D2ds_v5"
enable_nat_gateway = true
log_daily_quota_gb = -1

Expect several hundred USD a month more than dev. Before anyone else runs the deployment, move the Terraform state to Azure Storage, as backend.tf.example describes.

C. A fixed outbound address

Outbound traffic otherwise leaves from the environment's address, which can change. Where your git server or AI endpoint allows only listed addresses, add a NAT gateway (about 35 USD a month plus traffic); the egress_ip output is then the address to allow:

terraform.tfvars
enable_nat_gateway = true

D. A second installation in the same subscription

Such as a test installation next to production. Deploy it from its own copy of the folder, so it has its own Terraform state (cp -r scansuite/azure scansuite/azure-test), into its own resource group. Globally unique names get a random suffix, so nothing else has to change:

terraform.tfvars
resource_group_name = "rg-scansuite-test"

E. A region where PostgreSQL is not offered to you

Some subscription types, Visual Studio ones among them, cannot create PostgreSQL in every region; deploy.sh stops before building anything and says so. Pick another region:

terraform.tfvars
location = "swedencentral"   # or northeurope, francecentral

4. First sign-in

Open the url output right away. A new installation shows the setup page, where you create the first account and name your team, as on a server — see Install ScanSuite Teams. Whoever opens it first gets that account, which is why web_allowed_cidrs is set before the deploy.

Then configure the AI provider on the System AI card under System Settings → Shared services (or for one team under Teams → AI → AI provider) — see AI providers and cost. For Azure OpenAI, choose the OpenAI provider, set the endpoint to your resource's v1 address, https://<resource>.openai.azure.com/openai/v1, with its API key, and use a deployment name as the model name.

5. Day-to-day operation

  • Change a setting: edit terraform.tfvars and run ./deploy.sh again. Re-running is always safe, and while the images stay the same, running scans carry on.
  • A new release: put the new .lic in ../key/ and remove the old one, set image_tag to the new code, run git pull, then ./deploy.sh. The migration cancels scans still running, so update when none is.
  • Interrupted scans recover by themselves: a scan whose container Azure stopped shows no progress for 90 minutes and is then started again, continuing its AI analysis from the last saved stage.
  • Logs: open the Log Analytics workspace in the resource group, choose Logs, and query ContainerAppConsoleLogs_CL by ContainerAppName_s (such as scansuite-web) or ContainerJobName_s (scansuite-sast for scans).
  • Save money when nobody uses it: web_min_replicas = 0, and stop the database with az postgres flexible-server stop; Azure starts it again after seven days.
  • Keep the Terraform state safe: terraform.tfstate holds the generated passwords and your licence. Keep it private, back it up after every deploy, and move it to Azure Storage before a second person or a pipeline deploys.

Rough monthly costs in USD for the default dev settings; check your region in the Azure pricing calculator:

ItemApproximate cost
Always-on containers (web, workers, scheduler, Redis)120-160
PostgreSQL B_Standard_B1ms with 32 GB20
Log Analytics, capped at 1 GB a day0-70
Registry, Key Vault, storageabout 10
Scans, 4 vCPU and 8 GiB eachabout 0.45 per scan-hour
NAT gateway, only if enabled35 plus traffic

6. Tear down

Shell
./destroy.sh

The script asks you to type the resource group name, then deletes everything the deployment created, the database with all scan data included, in about 15 minutes; -y skips the question. az group show -n <resource group> then reports that the group was not found. If the script stops with polling support for the Content-Type "" was not implemented, Azure has already deleted what the message names: run it again, two or three times if need be.

Common messages

PostgreSQL Flexible Server 16 is not available to this subscription: example E. ManagedEnvironmentCapacityHeavyUsageError: the region has no room for a new environment right now; delete the failed one (az containerapp env delete) and try again later, or in another region. no licence for image tag: put the .lic in ../key/ and match image_tag to its code. could not import docker.io/appsec4u/...: export the registry credentials in the same shell. Provider produced inconsistent result after apply, after re-creating a deployment with the same resource group name: use a new name, or wait 15 minutes.

Last reviewed 2026-10-01