ScanCode.io on Ubuntu 24.04 on Azure User Guide
Overview
ScanCode.io is the AboutCode server for automated software composition analysis. You give it a codebase, an archive or a container image, choose a pipeline, and it scans every file and reports what is actually in there: the detected licences and copyright notices, the packages and their declared dependencies, and the exact file each finding came from. Results are browsable in a web interface, queryable through a REST API, and exportable as SPDX or CycloneDX so they can feed a software bill of materials.
The cloudimg image installs ScanCode.io 38.0.0 from the official published container image, pinned by digest, and runs it as a systemd service — so a working SCA server is online within minutes of launch, with authentication already switched on.
What is included:
- ScanCode.io 38.0.0 (Apache-2.0), deployed from the official
ghcr.io/aboutcode-org/scancode.ioimage pinned by digest so the appliance is reproducible and can never silently upgrade - Docker Engine (Docker CE) + the Docker Compose plugin, installed from the official Docker package repository
- The upstream Compose topology:
nginxon port 80,web(gunicorn serving the Django application),worker(the RQ pipeline worker),db(PostgreSQL 17.10) andredis(8.8.0, append-only persistence) — every image digest-pinned - Authentication required out of the box (
SCANCODEIO_REQUIRE_AUTHENTICATION=True). ScanCode.io ships upstream with authentication off; on a public VM that would let anyone submit scan pipelines and run arbitrary workloads on your machine - A per-VM
adminaccount with a generated web password and a generated REST API key, created at first boot - A per-VM Django
SECRET_KEYand PostgreSQL password, both rotated at first boot - All stateful data — the PostgreSQL database, the Redis append-only file and the scan workspace — on a dedicated 64 GiB data disk mounted at
/var/lib/scancodeio(Docker's data-root is relocated there), independently resizable and re-provisioned on every VM - A self-authored sample codebase and a one-command self-test that proves a full scan round-trip on your own instance
- Two systemd units:
scancodeio.service(a oneshot wrapper arounddocker compose up -d) andscancodeio-firstboot.service - 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key, and a VNet + subnet. Standard_B2s (2 vCPU / 4 GB RAM) is the recommended starting size and is enough to evaluate ScanCode.io and to scan modest codebases: the idle stack uses roughly 1.3 GB, leaving around 2.5 GB of headroom, and a 4 GiB swapfile on the data disk absorbs scan bursts.
Scanning is genuinely CPU and memory hungry, because each scan process loads a large licence index. If you intend to scan large codebases or container images, size up — Standard_B4ms (4 vCPU / 16 GB) or Standard_D4s_v5 (4 vCPU / 16 GB) are sensible next steps, and you can raise the worker's parallelism afterwards (see Step 11).
NSG inbound rules: allow 22/tcp from your management CIDR for SSH and 80/tcp from the CIDR that needs the ScanCode.io web UI and API (add 443/tcp if you enable HTTPS).
Step 1: Connect over SSH
Replace <vm-ip> with the public IP of your VM. The default login user is azureuser.
ssh azureuser@<vm-ip>
Step 2: Confirm the services are active
sudo systemctl is-active docker.service scancodeio.service
Both report active. scancodeio.service is a oneshot unit that brings the Docker Compose stack up, so active means the stack has been started.

Step 3: Confirm the stack containers are running
cd /opt/scancodeio && sudo docker compose --env-file docker.env ps --format '{{.Service}}\t{{.Status}}'
You should see all five services — db, nginx, redis, web and worker — with db and redis reporting (healthy).
Step 4: Confirm the data disk is mounted
The database, the Redis append-only file and every scan workspace live on a dedicated data disk. The scan workspace is the part that grows, because it holds each uploaded and extracted codebase.
mountpoint /var/lib/scancodeio && df -h /var/lib/scancodeio
Swap is provisioned on the same disk to absorb scan bursts:
swapon --show
Step 5: Check the ScanCode.io login page
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/accounts/login/
This returns 200. Note the login path is /accounts/login/.
Because authentication is required, the REST API refuses anonymous callers — this returns 401:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/api/projects/
Step 6: Read the per-VM credentials
A unique admin password, REST API key, Django secret key and database password are generated for your instance on its first boot. The user-facing ones are written to a root-only file.
sudo ls -l /root/scancodeio-credentials.txt
The file is mode 0600, owned by root. Read it with:
sudo cat /root/scancodeio-credentials.txt
It lists scancodeio.url (where to sign in), scancodeio.admin.user (admin), scancodeio.admin.pass and scancodeio.api.key. Store them somewhere safe.

Step 7: Sign in to the ScanCode.io web UI
Browse to http://<vm-ip>/ and sign in with the username admin and the scancodeio.admin.pass from Step 6. The UI and the API are served on port 80 by the bundled nginx reverse proxy.

After signing in you land on the Projects list. Each project is one scan: its pipeline, its status, and the counts of packages, dependencies and resources it found.

Step 8: Prove a full scan round-trip
The image ships a self-test that does the whole job end to end: it creates a project, uploads the bundled sample codebase, runs the scan_codebase pipeline to completion, and then checks that the results contain the specific licences and package the sample was authored to carry. It also confirms that anonymous API access is refused.
sudo /usr/local/sbin/scancodeio-selftest.sh
It finishes with SELFTEST_SUCCESS. This is a real scan, asserted on its content — not a liveness ping.

The sample codebase it scans lives at /opt/scancodeio/sample/ and is packaged at /opt/scancodeio/cloudimg-sample-codebase.zip. Every file in it was written by cloudimg specifically to exercise the scanner, with a known licence in each:
ls -l /opt/scancodeio/sample/
Step 9: Scan your own code
In the web UI choose New Project, give it a name, pick the scan_codebase pipeline, and either upload an archive or give it a download URL. The same thing over the REST API, using the API key from Step 6:
KEY=$(sudo sed -n 's/^SCANCODEIO_API_KEY=//p' /root/scancodeio-credentials.txt | head -1)
CODE=$(curl -s -o /tmp/scio-new-project.json -w '%{http_code}' \
-H "Authorization: Token $KEY" \
-F "name=my-first-scan-$$" \
-F "pipeline=scan_codebase" \
-F "execute_now=True" \
-F "upload_file=@/opt/scancodeio/cloudimg-sample-codebase.zip" \
http://127.0.0.1/api/projects/)
[ "$CODE" = "201" ] || { echo "FAIL: project creation returned HTTP $CODE"; cat /tmp/scio-new-project.json; exit 1; }
echo "created project HTTP $CODE"; head -c 200 /tmp/scio-new-project.json
The response includes the project uuid. Useful pipelines beyond scan_codebase: scan_single_package (one package archive), inspect_packages (dependency manifests), analyze_docker (a container image) and load_sbom (import an existing SBOM).
Step 10: Read the results
The Resources view lists every file with its detected licence expression, and the Packages view lists the packages discovered from manifests. This is the product's whole purpose — evidence of what your codebase actually contains.


The same data over the API — list your projects, then fetch one project's packages:
KEY=$(sudo sed -n 's/^SCANCODEIO_API_KEY=//p' /root/scancodeio-credentials.txt | head -1)
CODE=$(curl -s -o /tmp/scio-projects.json -w '%{http_code}' \
-H "Authorization: Token $KEY" 'http://127.0.0.1/api/projects/?page_size=5')
[ "$CODE" = "200" ] || { echo "FAIL: API returned HTTP $CODE"; cat /tmp/scio-projects.json; exit 1; }
python3 -c "import json;d=json.load(open('/tmp/scio-projects.json'));print('projects:',d['count']);[print(' -',p['name'],p.get('package_count'),'packages') for p in d['results']]"

Results export as SPDX or CycloneDX for use as a software bill of materials, from the Export button in the UI or from the API's results_download endpoint with an output_format of json, xlsx, spdx or cyclonedx.
Step 11: Manage and tune the stack
The whole stack is managed by the scancodeio.service systemd unit, which wraps docker compose under /opt/scancodeio.
sudo systemctl status scancodeio.service --no-pager | head -20
To inspect logs for a specific service:
cd /opt/scancodeio && sudo docker compose --env-file docker.env logs --tail 30 worker
Configuration lives in /opt/scancodeio/docker.env. Two settings matter most if you move to a larger VM:
SCANCODEIO_PROCESSES— how many parallel scan processes a pipeline may use. It ships as1to fit 4 GB of RAM. On a larger VM, raising it to the vCPU count speeds up big scans considerably.- The gunicorn
--workerscount in/opt/scancodeio/docker-compose.yml, which ships as2.
After editing either, restart the stack:
sudo systemctl restart scancodeio.service
The deliberate differences between this appliance and the stock upstream Compose file are documented on the VM:
cat /opt/scancodeio/README.cloudimg
Enabling HTTPS on your own domain
The appliance serves plain HTTP on port 80. To put it behind TLS, terminate HTTPS in front of ScanCode.io (an Azure Application Gateway, a load balancer, or your own reverse proxy with a certificate), point it at port 80 on this VM, and restrict the NSG so only your proxy can reach port 80.
Once TLS terminates in front of the application, harden the cookies by setting both SESSION_COOKIE_SECURE=True and CSRF_COOKIE_SECURE=True in /opt/scancodeio/docker.env and restarting the service. They ship as False because a browser refuses to send a Secure cookie over plain HTTP, which would make signing in impossible on a stock HTTP deployment.
First-boot security model
Nothing secret is baked into the image. On the first boot of every VM, scancodeio-firstboot.service generates and applies:
- a fresh Django
SECRET_KEY— this signs session cookies and CSRF tokens, so a key shared between VMs would be a real cryptographic weakness - a fresh PostgreSQL password
- a fresh
adminaccount, with both a web password and a REST API key
It writes them to /root/scancodeio-credentials.txt (mode 0600, owned by root), records /var/lib/cloudimg/scancodeio-firstboot.done so it never re-runs, and verifies before finishing that the API accepts the new key and refuses anonymous callers.
Authentication is required by default and should stay that way on any internet-facing VM: an open ScanCode.io lets anyone submit scan pipelines, which is arbitrary workload execution on your machine.
Support
Every cloudimg image comes with 24/7 support. Contact us at support@cloudimg.co.uk.