Lookyloo on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and use of Lookyloo on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Lookyloo is a web page capture and investigation tool built by CIRCL, the Computer Incident Response Center Luxembourg, and used by CERTs and security teams to triage phishing and malicious sites. You give it a URL; it visits the page with a genuine headless browser and records the complete journey, then renders it as an interactive tree: the redirect chain, every hostname and resource the page pulled in, the cookies it set, the scripts it ran, and a screenshot of what a victim would actually see.
The point of the tree is attribution: an analyst can see at a glance which third party actually delivers a payload, where a form would post credentials, or how a redirector hops between disposable domains, without ever touching the suspicious site from their own workstation.
The image installs Lookyloo v1.40.1, cloned from the upstream repository at that exact tag and verified during the build, together with Valkey 8.1.9 compiled from source exactly as the vendor's install guide specifies, and the Playwright browsers the capture engine drives. The full REST API is enabled and self documented on the instance.
Security by design — internal addresses cannot be captured. A capture tool that fetches any URL it is given is potentially a probe into your own network. This image keeps upstream's only_global_lookups protection enabled, so capture requests for private and internal address space, including the Azure instance metadata service, are refused. The build verifies this behaviour functionally before the image is published.
Security by design — no default password. In the shipped image the admin login does not exist at all: Lookyloo's user table is empty, so there is nothing to log in to and nothing to guess. At first boot your instance generates a 32 character random password for itself, writes the admin user into the configuration, and proves both that the new credential authenticates and that a wrong password is rejected, before first boot is allowed to finish.
What is included:
-
Lookyloo v1.40.1 (pinned upstream tag, BSD 3 Clause licence), run under systemd as
lookyloo.service -
The embedded LacusCore capture engine driving headless Playwright browsers (Chromium, Firefox and WebKit installed per the vendor's procedure)
-
Valkey 8.1.9 compiled from source (upstream refuses the older Redis 7 that Ubuntu packages), running two instances on local unix sockets for the capture cache and the index
-
nginx on port 80 reverse proxying the Lookyloo website, which itself stays bound to loopback, matching the upstream project's own production layout
-
The REST API with interactive documentation at
/doc/, plus compatibility with thepylookylooclient -
A per instance admin credential generated at first boot, stored root only at
/root/lookyloo-credentials.txt -
Unattended security upgrades left enabled so the underlying OS keeps receiving patches
Prerequisites
-
Active Azure subscription, SSH public key, VNet and subnet in the target region
-
Subscription to the Lookyloo listing on Azure Marketplace
-
Network Security Group rules allowing TCP 22 for administration and TCP 80 for the web interface, ideally restricted to your team's source addresses
Recommended virtual machine size: Standard_B2ms (2 vCPU, 8 GB RAM). This is a genuine minimum, not a suggestion: Lookyloo runs its website with ten worker processes plus six background daemons, two Valkey databases and a full headless browser per in flight capture, and the stack uses well over 4 GB under real use. On a 4 GB instance captures can fail with out of memory errors. For heavy multi analyst use choose Standard_D4s_v5 or larger.
Restrict who can reach port 80. Anyone who can load the page can submit captures and browse the public capture index; that is how the tool is designed for a team. Scope the NSG rule for port 80 to your organisation's address ranges rather than the whole internet.
Step 1: Deploy from the Azure Portal
Search Lookyloo in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 22 and TCP 80, restricted to your team's source addresses.
Step 2: Deploy from the Azure CLI
RG="soc-tools"; LOCATION="eastus"; VM_NAME="lookyloo1"
az group create --name "$RG" --location "$LOCATION"
az vm create \
--resource-group "$RG" \
--name "$VM_NAME" \
--image cloudimg:lookyloo:default:latest \
--size Standard_B2ms \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard \
--location "$LOCATION"
# Open the web interface to YOUR source ranges - not the whole internet:
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1001
Step 3: First boot
On first boot the instance generates its own admin password, writes it into the Lookyloo configuration, restarts the stack, and verifies that the new credential authenticates and that a wrong password is rejected, before it finishes. Allow two to three minutes for the full stack to come up the first time. Connect over SSH and read the details, which are unique to your instance:
sudo cat /root/lookyloo-credentials.txt

The file records the web address, the generated admin username and password, and how to obtain an API token. It is readable by root only.
Step 4: Confirm the stack is running
systemctl is-active lookyloo.service nginx.service
Both must report active. Confirm the installed versions and that nginx is the only public HTTP listener:
sudo -u lookyloo git -C /opt/lookyloo/lookyloo describe --tags
/opt/lookyloo/valkey/src/valkey-server -v
sudo ss -tlnp | grep -E ':80 |:5100 '

The website itself listens on 127.0.0.1:5100; nginx proxies it on port 80. Both services are enabled and start automatically on every boot:
systemctl is-enabled lookyloo.service nginx.service
Step 5: Capture your first page
Browse to http://<LOOKYLOO_HOST>/ and you land on the capture form. Paste a URL, choose the visibility level, and submit. The capture runs in the background with a real headless browser; a typical page completes in well under a minute.

When the capture completes you are taken to the tree view, which is the product: the captured page on the left, and every hostname it pulled content from branching to the right, with icons showing what each host delivered (JavaScript, cookies received or read, redirects, iframes, fonts, images) and a screenshot of the rendered page.

Click any node to inspect the URLs it served, or use the Analytical Tools menu to pivot the whole capture by IP addresses, hostnames, URLs, favicons or fuzzy hashes, and to consult third party reports on what you captured.

Step 6: Sign in as the admin user
Sign in at http://<LOOKYLOO_HOST>/login with the username and password from /root/lookyloo-credentials.txt. Authenticated analysts can see hidden captures, rescan and compare captures, and use the admin features; anonymous visitors only see the public index.

Step 7: Use the REST API
The API is self documented at http://<LOOKYLOO_HOST>/doc/ and everything the web interface does is available programmatically. Working on the instance itself, obtain your API token with the per instance credential:
curl -s -X POST http://127.0.0.1:5100/json/get_token \
-H 'Content-Type: application/json' \
-d '{"username":"<LOOKYLOO_USER>","password":"<LOOKYLOO_PASSWORD>"}'
A wrong password is rejected with HTTP 401, which you can verify without exposing anything:
curl -s -o /dev/null -w 'wrong password: HTTP %{http_code}\n' \
-X POST http://127.0.0.1:5100/json/get_token \
-H 'Content-Type: application/json' \
-d '{"username":"<LOOKYLOO_USER>","password":"not-the-password"}'
Submit a capture and wait for it to complete, then retrieve the redirect chain and the rendered screenshot. The submit call returns the capture's UUID, and status_code reaches 1 when the tree is ready:
CAPTURE_UUID=$(curl -s -X POST http://127.0.0.1:5100/submit \
-H 'Content-Type: application/json' \
-d '{"url": "https://www.example.com", "listing": true}' | tr -d '"')
echo "capture: $CAPTURE_UUID"
for i in $(seq 1 100); do
STATUS=$(curl -s http://127.0.0.1:5100/json/$CAPTURE_UUID/status | jq -r .status_code)
[ "$STATUS" = "1" ] && break
sleep 3
done
echo "final status_code: $STATUS"
curl -s http://127.0.0.1:5100/json/$CAPTURE_UUID/redirects
curl -s -o /tmp/capture-screenshot.png http://127.0.0.1:5100/bin/$CAPTURE_UUID/screenshot
file /tmp/capture-screenshot.png

From your own tooling, use the same endpoints against http://<LOOKYLOO_HOST>/ or install the upstream client with pip install pylookyloo.
Step 8: Verify internal addresses cannot be captured
The image keeps upstream's only_global_lookups protection enabled, which refuses captures of private and internal address space. Submitting the Azure metadata endpoint proves it: the capture errors instead of fetching.
grep -o '"only_global_lookups": true' /opt/lookyloo/lookyloo/config/generic.json
Leave this set to true on any instance that people other than you can reach; it is what stops the appliance being used to probe your VNet.
Server Components
| Component | Version | Purpose |
|---|---|---|
| Lookyloo | v1.40.1 (pinned tag) | Capture and investigation platform |
| LacusCore + Playwright | bundled with the release | Headless browser capture engine (Chromium, Firefox, WebKit) |
| Valkey | 8.1.9 (compiled from source) | Capture cache and index, unix sockets only |
| nginx | Ubuntu 24.04 archive | Public reverse proxy on port 80 |
| Python | 3.12 (Ubuntu 24.04) | Application runtime, Poetry managed virtualenv |
Filesystem Layout
| Mount Point | Size | Description |
|---|---|---|
| / | 29 GB | Root filesystem |
| /boot | 881 MB | Kernel files |
| /boot/efi | 105 MB | UEFI boot partition (Gen2 Hyper V) |
| /mnt | varies | Azure temporary resource disk |
Key directories:
| Path | Description |
|---|---|
| /opt/lookyloo/lookyloo | The application (LOOKYLOO_HOME), including its Poetry virtualenv |
| /opt/lookyloo/lookyloo/config/generic.json | Main configuration, including the users table |
| /opt/lookyloo/lookyloo/scraped | Capture data |
| /opt/lookyloo/lookyloo/archived_captures | Captures archived after the retention window |
| /opt/lookyloo/valkey | Valkey 8.1.9 built from source |
| /opt/lookyloo/.cache/ms-playwright | Playwright browser binaries |
| /root/lookyloo-credentials.txt | Per instance admin credential (root only) |
Managing the service
lookyloo.service wraps the vendor's own poetry run start and poetry run stop commands, which manage the Valkey databases, the capture daemons and the website together:
systemctl status --no-pager -l lookyloo.service | head -12
To restart the whole stack after a configuration change, run sudo systemctl restart lookyloo.service and allow a minute or two for every component to come back. Application logs live in /opt/lookyloo/lookyloo/logs/ and website logs in /opt/lookyloo/lookyloo/website/logs/:
ls /opt/lookyloo/lookyloo/logs/
Configuration
All configuration lives in /opt/lookyloo/lookyloo/config/. generic.json controls the website, capture behaviour, retention (archive, in days), the users table and module toggles; modules.json configures optional third party integrations such as VirusTotal, MISP and Phishtank, which are disabled by default and need your own API keys. After editing, restart the stack. Two settings deserve care:
-
only_global_lookups— leavetrueunless this instance is fully private and you knowingly need to capture internal addresses. -
async_capture_processes— how many captures run concurrently (default 3). Each in flight capture runs a full headless browser; raise it only on instances with more memory.
Troubleshooting
The website answers with 502 from nginx
The stack is still starting, or the website process stopped. Check systemctl status lookyloo.service and the most recent file in /opt/lookyloo/lookyloo/logs/. A restart of lookyloo.service brings the whole stack back in order.
A capture stays queued and never completes
Check memory first with free -m: a full headless browser needs headroom, and an undersized instance is the most common cause of failed or stuck captures. Then check the capture daemon's log under /opt/lookyloo/lookyloo/logs/.
A capture of an internal URL fails with an error
That is by design: only_global_lookups refuses private and internal address space. See Step 8.
I lost the admin password
Generate a new one, write it into the users table in /opt/lookyloo/lookyloo/config/generic.json (the value is stored as the plain password and hashed at load), update /root/lookyloo-credentials.txt to match, and restart lookyloo.service.
Security Recommendations
-
Scope the NSG rule for port 80 to your organisation's source ranges. An open Lookyloo will capture pages for anyone who finds it.
-
Leave
only_global_lookupsset totrueso the instance cannot probe internal address space. -
Restrict SSH (TCP 22) to known administrative addresses.
-
Leave unattended security upgrades enabled so the underlying OS stays patched.
-
Treat capture data as sensitive: it contains everything the captured pages served, including any malicious content, and
/opt/lookyloo/lookyloo/scrapedgrows with use.
Support and Licensing
Lookyloo is open source software licensed under the BSD 3 Clause licence, copyright CIRCL, the Computer Incident Response Center Luxembourg. The licence text is included on the image at /usr/share/doc/cloudimg/lookyloo/LICENSE.
cloudimg provides the packaged, hardened Azure image and 24/7 support for the image itself. cloudimg is not affiliated with or endorsed by CIRCL or the Lookyloo project; Lookyloo is the name of the upstream project and is used here only to identify the software this image packages.
Deploy on Azure
Find Lookyloo on Ubuntu 24.04 LTS by cloudimg on the Azure Marketplace.
Need Help?
Contact cloudimg support at support@cloudimg.co.uk.