Arroyo on Ubuntu 24.04 on Azure User Guide
Overview
Arroyo is a distributed stream processing engine that lets you run SQL over real time data streams. Instead of writing and operating a bespoke streaming application, you express your logic as a query and Arroyo turns it into a stateful, fault tolerant pipeline that processes events continuously as they arrive. It supports event time semantics with watermarks, tumbling, hopping and sliding windows, streaming joins, stateful aggregation, user defined functions, and exactly once processing backed by periodic checkpointing.
The cloudimg image installs Arroyo 0.15.0 and runs it as a self contained single node cluster behind an nginx reverse proxy. Arroyo ships with no authentication of its own, and out of the box every one of its services listens on all network interfaces. This image closes both gaps: every service is bound to loopback so nginx on port 80 is the only public listener, and access is gated twice. The browser signs in against an nginx login prompt, and nginx then presents an engine key to Arroyo on your behalf, so nothing reaches the engine unauthenticated. There is no default password. On first boot arroyo-firstboot.service generates a unique console password and a unique engine key, and writes both to a root only note at /root/arroyo-info.txt. Neither is baked into the image.
What is included:
- Arroyo 0.15.0 single binary (
/usr/local/bin/arroyo) running as a single node cluster - Embedded SQLite metadata store at
/var/lib/arroyo/config.sqlite(no external database to provision) arroyo.servicerunning asarroyo, with every service bound to loopback: api 5115, admin 5114, controller 5116, compiler 5117, node 5118nginx.servicereverse proxy on TCP 80 with an HTTP Basic Auth login gate, TLS readyarroyo-firstboot.servicegenerating a per VM console password and engine key on first boot- Built in sources and sinks for Kafka, Redpanda, Kinesis, Pulsar, MQTT, NATS, WebSockets, server sent events, Delta Lake, Iceberg and filesystem storage
- A browser console with a SQL editor, live query preview, pipeline graph and per operator metrics
- Ubuntu 24.04 LTS base, latest patches, unattended security upgrades enabled
- 24/7 cloudimg support, 24h response SLA
Prerequisites
An active Azure subscription, an SSH key, and a VNet with a subnet. Recommended VM size: Standard_B2s (2 vCPU, 4 GB). SQL pipelines are planned and executed by Arroyo's built in engine and need no compiler toolchain, so a small VM is sufficient for development and moderate throughput. Scale up if you run many concurrent pipelines, raise parallelism, or enable user defined functions (see Step 12).
Step 1: Deploy from the Azure Portal
Search the Azure Marketplace for Arroyo, choose the plan, and create the VM. In the networking step attach an NSG that allows inbound TCP 22 (SSH) and TCP 80 (the console) from your client networks only. Put a TLS reverse proxy in front of port 80 for production.
Step 2: Deploy from the Azure CLI
RG="arroyo-prod"; LOCATION="eastus"; VM_NAME="arroyo-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/arroyo-ubuntu-24-04/versions/<version>"
az vm create \
--resource-group "$RG" --name "$VM_NAME" --location "$LOCATION" \
--image "$GALLERY_IMAGE_ID" --size Standard_B2s \
--admin-username azureuser --generate-ssh-keys \
--public-ip-sku Standard
# then open TCP 80 to your own client network only:
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 900
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
arroyo.service, nginx.service and arroyo-firstboot.service all start automatically on first boot.
Step 4: Verify the Service
sudo systemctl is-active arroyo nginx arroyo-firstboot
sudo test -f /var/lib/cloudimg/arroyo-firstboot.done && echo FIRSTBOOT_DONE
sudo ss -tlnp | grep -E ':80 |:5115 |:5114 '
Expected output — all three services are active, nginx is bound to the public port 80, and the Arroyo services are bound to loopback only:
active
active
active
FIRSTBOOT_DONE
LISTEN 0 1024 127.0.0.1:5115 0.0.0.0:* users:(("arroyo",pid=3659,fd=14))
LISTEN 0 1024 127.0.0.1:5114 0.0.0.0:* users:(("arroyo",pid=3659,fd=12))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=3670,fd=5),...)
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=3670,fd=6),...)

Step 5: Get Your Login Credentials
The console has no default password. On first boot a unique console password and a unique engine key are generated and written to a root only note, and the nginx login gate is configured to require the password:
sudo cat /root/arroyo-info.txt
curl -s -o /dev/null -w 'no credentials -> HTTP %{http_code}\n' http://127.0.0.1/api/v1/pipelines
P=$(sudo grep '^ARROYO_ADMIN_PASSWORD=' /root/arroyo-info.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'with the per-VM login -> HTTP %{http_code}\n' -u "admin:$P" http://127.0.0.1/api/v1/pipelines
The note contains the console username, password, engine key and URL, all unique to this machine and never baked into the image. An unauthenticated request is rejected with 401, and the per VM login returns 200:
ARROYO_ADMIN_USERNAME=admin
ARROYO_ADMIN_PASSWORD=<ARROYO_ADMIN_PASSWORD>
ARROYO_API_KEY=<ARROYO_API_KEY>
ARROYO_URL=http://<vm-ip>/
no credentials -> HTTP 401
with the per-VM login -> HTTP 200
The ARROYO_URL line uses the address the VM sees for itself; browse to the console using your VM's own public IP address.

Step 6: Confirm the Second Access Layer
Arroyo has no login of its own, so this image adds two layers. nginx is the browser gate; the engine itself additionally requires a bearer key, which nginx presents for you. That means even a process on the VM cannot reach the engine directly without the key:
K=$(sudo grep '^ARROYO_API_KEY=' /root/arroyo-info.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'direct to the engine, no key -> HTTP %{http_code}\n' http://127.0.0.1:5115/api/v1/pipelines
curl -s -o /dev/null -w 'direct to the engine, with key -> HTTP %{http_code}\n' -H "Authorization: Bearer $K" http://127.0.0.1:5115/api/v1/pipelines
Expected output — the engine rejects an unkeyed request and accepts a keyed one:
direct to the engine, no key -> HTTP 401
direct to the engine, with key -> HTTP 200
You only need the engine key if you call the REST API directly rather than through the console or the proxy.
Step 7: Sign In to the Console
Open the console in a browser at your VM's public IP address. The nginx login prompt appears; enter the username admin and the password from Step 5. Once signed in you land on the Pipelines view, which lists every pipeline on this cluster with its state, runtime and task count. A freshly deployed machine starts with an empty list; the screenshot below shows it after two pipelines have been created.

Step 8: Create Your First Pipeline in the Console
Choose Create Pipeline to open the SQL editor. Arroyo pipelines are SQL: you declare a source table with a connector, then write a query over it. Paste the following, which reads Arroyo's built in demo stream and transforms each event:
CREATE TABLE impulse_src WITH (
connector = 'impulse',
event_rate = '20'
);
SELECT counter, counter * 10 + 7 AS transformed FROM impulse_src;
The editor validates and plans the query as you type and shows a live preview of the results, so you can confirm the output is what you expect before starting the pipeline. Note that the impulse connector supplies its own schema, so the CREATE TABLE declares no columns.

Step 9: Start the Pipeline and Watch It Run
Give the pipeline a name and start it. The job detail view shows the pipeline as a graph of operators, with live throughput, watermark and checkpoint metrics for each one, so you can see events flowing through the source, the projection and the sink.

Step 10: Do the Same Through the REST API
Everything in the console is also available over the REST API, through the same login. Create a pipeline, wait for it to run, and read its live output:
P=$(sudo grep '^ARROYO_ADMIN_PASSWORD=' /root/arroyo-info.txt | cut -d= -f2-)
Q='CREATE TABLE impulse_src WITH (connector = '"'"'impulse'"'"', event_rate = '"'"'20'"'"');
SELECT counter, counter * 10 + 7 AS transformed FROM impulse_src;'
PID=$(curl -s -u "admin:$P" -H 'Content-Type: application/json' \
--data "$(jq -nc --arg q "$Q" '{name:"guide-demo", query:$q, parallelism:1}')" \
http://127.0.0.1/api/v1/pipelines | jq -r .id)
echo "pipeline: $PID"
curl -s -u "admin:$P" "http://127.0.0.1/api/v1/pipelines/$PID/jobs" | jq -r '.data[0] | "job \(.id) state \(.state)"'
Expected output — the pipeline is created and its job is scheduled:
pipeline: pl_XXXXXXXXXX
job job_XXXXXXXXXX state Created

Remove the demo pipeline when you are finished with it. A running pipeline cannot be deleted, so stop it first and wait for the job to report Stopped:
P=$(sudo grep '^ARROYO_ADMIN_PASSWORD=' /root/arroyo-info.txt | cut -d= -f2-)
PID=$(curl -s -u "admin:$P" http://127.0.0.1/api/v1/pipelines | jq -r '.data[]|select(.name=="guide-demo")|.id' | head -1)
curl -s -o /dev/null -u "admin:$P" -X PATCH -H 'Content-Type: application/json' \
--data '{"stop":"immediate"}' "http://127.0.0.1/api/v1/pipelines/$PID"
for i in $(seq 1 20); do
S=$(curl -s -u "admin:$P" "http://127.0.0.1/api/v1/pipelines/$PID/jobs" | jq -r '.data[0].state')
[ "$S" = "Stopped" ] && break; sleep 3
done
curl -s -o /dev/null -w 'delete -> HTTP %{http_code}\n' -u "admin:$P" -X DELETE "http://127.0.0.1/api/v1/pipelines/$PID"
Expected output:
delete -> HTTP 200
Step 11: Connect a Real Source
The impulse connector is a demo source. In production you point Arroyo at your own streams. Connectors are configured in the same CREATE TABLE statement — for example a Kafka topic:
CREATE TABLE events (
user_id TEXT,
amount DOUBLE,
event_time TIMESTAMP
) WITH (
connector = 'kafka',
bootstrap_servers = 'broker.example.com:9092',
topic = 'events',
type = 'source',
format = 'json',
'source.offset' = 'latest'
);
SELECT user_id, sum(amount) AS total
FROM events
GROUP BY user_id, tumble(interval '1 minute');
Browse the available connectors in the console under Connections, then Create Connection. Each connector card shows whether it can act as a source, a sink or both, and creating a connection profile lets you configure and test it once before referencing it from SQL.

Step 12: Enable User Defined Functions (Optional)
SQL pipelines need no compiler toolchain, so the image ships without one and starts fast and fully offline. User defined functions are compiled from Rust, so they do require one. To enable UDFs, allow the compiler service to install its toolchain and restart Arroyo:
sudo sed -i 's/^install-clang = false/install-clang = true/; s/^install-rustc = false/install-rustc = true/' /etc/arroyo/config.toml
sudo systemctl restart arroyo
The first restart after enabling this downloads the toolchain, so allow several minutes and outbound internet access. Compiling UDFs is CPU and memory intensive; use at least a 4 vCPU, 16 GB VM size such as Standard_D4s_v3 if you rely on them. The same two settings live in the template at /usr/share/arroyo/config.toml.tmpl if you want the change to survive a re-render.
Step 13: Server Components
Everything the image installs lives in a handful of predictable paths:
/usr/local/bin/arroyo --version
ls -1 /usr/local/bin/arroyo /etc/arroyo/config.toml /usr/share/arroyo/config.toml.tmpl \
/etc/systemd/system/arroyo.service /usr/local/sbin/arroyo-firstboot.sh \
/etc/nginx/sites-available/cloudimg-arroyo
Expected output — the pinned version and the installed components:
arroyo 0.15.0
/etc/arroyo/config.toml
/etc/nginx/sites-available/cloudimg-arroyo
/etc/systemd/system/arroyo.service
/usr/local/bin/arroyo
/usr/local/sbin/arroyo-firstboot.sh
/usr/share/arroyo/config.toml.tmpl

Step 14: Managing the Service
sudo systemctl status arroyo --no-pager
sudo systemctl restart arroyo
sudo journalctl -u arroyo -n 50 --no-pager
Arroyo is a simple systemd service with Restart=always, so it comes back automatically after a crash or a reboot. Pipeline definitions live in the SQLite metadata store at /var/lib/arroyo/config.sqlite and pipeline state in /var/lib/arroyo/checkpoints; back up /var/lib/arroyo to preserve both.
Step 15: Change the Console Password
The console login is enforced by nginx HTTP Basic Auth, using the file /etc/nginx/arroyo.htpasswd. To set a new password, rewrite that file with htpasswd (bcrypt) and reload nginx:
NEWPASS='<new-password>'
sudo htpasswd -bB /etc/nginx/arroyo.htpasswd admin "$NEWPASS"
sudo systemctl reload nginx
Update /root/arroyo-info.txt to match if you rely on it, and store the new password in your own secret manager. The engine key is separate and does not change when you change the console password.
Step 16: Add TLS (Optional)
For production, terminate TLS in front of port 80. The reverse proxy is already in place; add a certificate with certbot against your own domain name:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.example.com
certbot edits the nginx server block to listen on 443 with your certificate and can redirect HTTP to HTTPS. Once TLS is in place the console login is protected in transit.
Step 17: Security Recommendations
- Restrict inbound TCP 80 (and 443) to your own client networks with an NSG.
- Add TLS (Step 16) so the login and your pipeline definitions are encrypted in transit.
- Change the console password (Step 15) and keep
/root/arroyo-info.txtreadable only by root. - Every Arroyo service binds to loopback only; never expose ports 5114 to 5118 directly.
- Treat the engine key as a secret — it grants full API access if the proxy is bypassed.
- Keep the OS patched — unattended security upgrades are enabled by default.
Step 18: Support and Licensing
Arroyo is dual licensed under the Apache License 2.0 or the MIT license, at your option. This is a repackaged open source software product with additional charges for cloudimg support services. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.
Deploy on Azure
Find Arroyo on Ubuntu 24.04 LTS on the Azure Marketplace and launch it in minutes.
Need Help?
cloudimg provides 24/7 support with a 24h response SLA. Visit www.cloudimg.co.uk or contact support through the Azure Marketplace listing.