VROOM on Ubuntu 24.04 on Azure User Guide
Overview
VROOM (Vehicle Routing Open source Optimization Machine) solves vehicle routing problems: given a set of jobs to complete and a fleet of vehicles to complete them, it decides which vehicle does what and in which order, minimising total cost. It handles the travelling salesman problem (TSP), capacitated routing (CVRP), routing with time windows (VRPTW), pickup and delivery problems, driver breaks, skills matching, job priorities and multi depot fleets. It is fast: problems with hundreds of stops solve in seconds, which is what makes it practical for same day dispatch and interactive planning rather than overnight batch runs.
The cloudimg image compiles VROOM 1.15.0 from the upstream source at /opt/vroom, and exposes it through vroom-express 0.12.0, the official HTTP wrapper, running on Node.js 22 LTS behind nginx on port 80.
How VROOM gets its travel times, and what that means for this image
This is the single most important thing to understand before you deploy, so it is stated plainly.
VROOM needs to know how long it takes to travel between your locations. It can get that in one of two ways:
Matrix mode — works out of the box on this image, with no external service. You supply the travel time (and optionally distance and cost) matrix in the request itself, and refer to locations by location_index. VROOM does the optimisation and contacts nothing at all. This is the mode this appliance ships ready to use, and it is a first class upstream feature, not a workaround: vroom-express even implements its own built in health check as a custom matrix solve. Matrix mode is the right fit when you already have travel times from an internal system, a commercial distance matrix API, a fixed tariff table, or a previous routing run you want to re optimise.
Coordinate mode — needs a routing engine that you run and point this image at. If you would rather send raw longitude and latitude coordinates and have travel times computed from real road geometry, VROOM queries an external routing engine to build the matrix for you. Support for OSRM, Openrouteservice and Valhalla is compiled into this image and ready to be configured, but no routing engine and no map data is bundled, so coordinate mode does not work until you point the image at an engine you operate. That is a deliberate choice: a useful OpenStreetMap extract runs to many gigabytes, would not fit the recommended VM size, and carries its own redistribution obligations. Connecting a routing engine below covers exactly how to wire one up.
In short: the solver is complete and self contained, and the travel time matrix is input data. If you already have travel times, this image is ready the moment it boots. If you want VROOM to derive them from road geometry, you supply the routing engine.
Secure by default, no default login: vroom-express ships with no authentication and permissive CORS, and an open optimisation endpoint is a free compute resource for anyone who finds it. This image never exposes it. vroom-express is reachable only through nginx, which fronts the entire API with HTTP Basic Auth, and a host firewall (ufw) admits only ports 22 and 80 so the Node service on port 3000 is unreachable from off the VM even if your network security group is permissive. A vroom-firstboot.service oneshot generates a unique per VM password on each VM's first boot, writes a bcrypt .htpasswd, proves the gate rejects unauthenticated and wrong password requests, proves a real solve returns the correct route, then disables itself. No two VMs share a password and none is baked into the image.
Note on the interface: VROOM is an API first engine with no bundled web interface. You drive it from your own dispatch system, a scheduled job, or curl. This guide uses curl.
What is included:
- VROOM 1.15.0 compiled from source, with plan mode and ETA validation enabled (
libglpk) vroom-express0.12.0 on Node.js 22 LTS, run as a dedicated non rootvroomuser under a hardened systemd unit- nginx reverse proxy on port 80 with per VM HTTP Basic Auth, ready for TLS
ufwhost firewall, default deny inbound, admitting only SSH and HTTP- An unauthenticated
/healthzendpoint for load balancer and probe checks - Two ready to run sample problems under
/opt/vroom/samples - A unique per VM password generated on first boot, in a root only
0600file - Ubuntu 24.04 LTS base, fully patched
- 24/7 cloudimg support, 24h response SLA
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet with a subnet. VROOM is CPU bound and parallelises across cores; problem size and the exploration level determine how long a solve takes. Recommended VM size: Standard_B2s (2 vCPU, 4 GB RAM) for evaluation and small fleets, or a Standard_F4s_v2 or larger for production dispatch with hundreds of stops.
Step 1: Deploy from the Azure Portal
Search the Marketplace for VROOM on Ubuntu 24.04, choose your VM size, and attach an NSG that allows TCP 22 (SSH) from your management network and TCP 80 (the API) from the applications that submit optimisation requests. Front port 80 with TLS in production (see the HTTPS section below).
Step 2: Deploy from the Azure CLI
RG="vroom-prod"; LOCATION="eastus"; VM_NAME="vroom-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/vroom-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name vroom-vnet --address-prefix 10.92.0.0/16 --subnet-name vroom-subnet --subnet-prefix 10.92.1.0/24
az network nsg create -g "$RG" --name vroom-nsg
az network nsg rule create -g "$RG" --nsg-name vroom-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name vroom-nsg --name allow-api --priority 110 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name vroom-vnet --subnet vroom-subnet --nsg vroom-nsg --public-ip-sku Standard
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
Step 4: Verify the services are running
vroom-express listens on 127.0.0.1:3000 and nginx fronts the API on port 80. The /healthz endpoint is public; every other endpoint requires your per VM password.
sudo systemctl is-active vroom-express nginx
/opt/vroom/bin/vroom --version
sudo ss -ltn | grep -E ':80 |:3000'
curl -s http://127.0.0.1/healthz
You should see both services active, vroom 1.15.0, the two listening sockets, and ok from the health endpoint.

Step 5: Retrieve your per VM password
The first boot service generated a password unique to this VM and stored it in a root only file.
sudo cat /root/vroom-credentials.txt
The VROOM_PASSWORD value is what you use with the admin username. Store it in your secret manager; it is not recoverable from anywhere else and is different on every VM you launch from this image.
Step 6: Confirm the API is protected
Before sending real work, confirm the gate is closed. An unauthenticated request and a wrong password request must both be rejected with 401, and only your per VM password should return 200.
curl -s -o /dev/null -w 'no credentials -> HTTP %{http_code}\n' -X POST \
-H 'Content-Type: application/json' \
--data-binary @/opt/vroom/samples/cloudimg-sample-problem.json http://127.0.0.1/
curl -s -o /dev/null -w 'wrong password -> HTTP %{http_code}\n' -X POST \
-u 'admin:definitely-wrong-pw' -H 'Content-Type: application/json' \
--data-binary @/opt/vroom/samples/cloudimg-sample-problem.json http://127.0.0.1/
sudo ufw status verbose | head -12
Expect 401, 401, and a firewall that is active with Default: deny (incoming) and only 22/tcp and 80/tcp allowed.

Step 7: Solve your first routing problem
The image ships a ready to run problem at /opt/vroom/samples/cloudimg-sample-problem.json: one vehicle, three jobs with delivery time windows, and a supplied 4x4 duration matrix. Because the matrix is in the request, this solve contacts no external service.
curl -s -u "admin:<VROOM_PASSWORD>" -H 'Content-Type: application/json' \
--data-binary @/opt/vroom/samples/cloudimg-sample-problem.json http://127.0.0.1/ \
| python3 -m json.tool | head -40
VROOM returns "code": 0, one route, zero unassigned jobs, and a step by step plan. The time windows in this sample admit exactly one feasible ordering, so a correct solve always visits job 101, then 102, then 103, with a total travel time of 3600 seconds.
Note the scheduling intelligence in the result: rather than leaving the depot at time zero and idling at a closed stop, VROOM delays departure to 600 so that arrivals line up with the windows, cutting total waiting time to 300 seconds.

Step 8: Solve a capacity constrained multi vehicle problem
The second sample models four deliveries in two geographic clusters with two vans, each able to carry two units. VROOM assigns one cluster to each van.
curl -s -u "admin:<VROOM_PASSWORD>" -H 'Content-Type: application/json' \
--data-binary @/opt/vroom/samples/cloudimg-sample-capacity.json http://127.0.0.1/ \
| python3 -m json.tool | head -30
You get two routes, zero unassigned jobs, and each van carrying exactly its capacity.

Writing your own problem
A request is a JSON object with vehicles, jobs (or shipments for paired pickup and delivery), and — in matrix mode — a matrices object. Locations are referred to by their index into that matrix.
cat > /tmp/my-problem.json <<'JSON'
{
"vehicles": [
{ "id": 1, "start_index": 0, "end_index": 0, "capacity": [10] }
],
"jobs": [
{ "id": 1, "location_index": 1, "delivery": [3], "service": 180 },
{ "id": 2, "location_index": 2, "delivery": [4], "service": 180 }
],
"matrices": {
"car": {
"durations": [
[0, 480, 720],
[480, 0, 300],
[720, 300, 0]
]
}
}
}
JSON
curl -s -u "admin:<VROOM_PASSWORD>" -H 'Content-Type: application/json' \
--data-binary @/tmp/my-problem.json http://127.0.0.1/ | python3 -m json.tool | head -25
The matrix is square, in seconds, and row/column i corresponds to location_index i. Entries need not be symmetric — one way systems and turn restrictions are exactly why they often are not.
Useful modelling keys, all documented in the upstream API reference:
capacityon a vehicle anddelivery/pickupon a job — multi dimensional load limits (weight, volume, pallet count, all at once)time_windowson a job andtime_windowon a vehicle — opening hours and driver shiftsskills— only vehicles holding every skill a job requires may serve it (refrigeration, a tail lift, a certified driver)priority— when not everything fits, control what gets dropped firstbreaks— mandatory driver rest periods inside a shiftmax_travel_time,max_distance,max_tasks— per vehicle limitscostswithfixed,per_hour,per_km— model what a route actually costs you
If you supply a non zero per_km cost, or ask for route geometry, VROOM needs distances as well as durations. In matrix mode supply a distances matrix alongside durations, otherwise VROOM will try to reach a routing engine to fetch them.
Step 9: Optional, connect a routing engine for coordinate input
To send location coordinates instead of location_index, point this image at a routing engine you operate. Nothing is bundled, so this step is entirely yours to provision — run OSRM, Openrouteservice or Valhalla on another host (or in a container on this VM) with a map extract covering your operating area.
Edit the routingServers section of the configuration, setting the host and port of your engine:
sudo cp /opt/vroom-express/config.yml /opt/vroom-express/config.yml.bak
sudo sed -n '/^routingServers:/,$p' /opt/vroom-express/config.yml
Change the host and port under the profile you use, then restart:
sudo systemctl restart vroom-express
sudo systemctl is-active vroom-express
With an engine reachable, requests may use "location": [lon, lat] on jobs and "start" / "end" on vehicles, and omit matrices entirely. Coordinates are always [longitude, latitude], in that order — the reverse of the usual mapping convention, and the most common first time mistake.
If you send coordinates without a reachable engine, VROOM returns status code 3 (routing error). That is the expected result, not a fault in the image.
Step 10: Tune for larger problems
Configuration lives in /opt/vroom-express/config.yml.
sudo grep -vE '^\s*#|^\s*$' /opt/vroom-express/config.yml | head -20
threadsis set to 2 to match Standard_B2s. Raise it to the vCPU count of a larger VM.explore(0 to 5) trades solve time against solution quality. 5 is the most thorough and the default.maxlocations(1000) andmaxvehicles(200) bound request size, andlimit(20mb) bounds the request body. Large matrices are large JSON: a 1000 by 1000 duration matrix is several megabytes.timeoutis 300000 ms. nginx is configured to match; raise both together if you routinely solve very large problems.overrideisfalseon purpose. Setting ittruelets any caller pass"options": {"t": 64, "x": 5}in the request body and dictate thread count and exploration depth on your VM. Leave it disabled on anything internet facing.
Restart after editing:
sudo systemctl restart vroom-express
curl -s http://127.0.0.1/healthz
Security model
vroom-expressis bound to the VM only. nginx on port 80 is the sole public listener, andufwadmits only ports 22 and 80, so port 3000 is unreachable from off the VM regardless of how your NSG is configured.- Every API endpoint requires HTTP Basic Auth with the per VM password. The single exception is
/healthz, which is a staticokstring and never reaches the solver, so it cannot be used to consume CPU. - The password is generated on first boot, stored bcrypt hashed in
/etc/nginx/.vroom.htpasswdand in clear only in/root/vroom-credentials.txt(mode0600, root only). The image itself ships an empty.htpasswd, which fails closed: before first boot completes, every request is rejected with401. - The service runs as the unprivileged
vroomuser under a hardened systemd unit (NoNewPrivileges,ProtectSystem=full,ProtectHome,PrivateTmp).
To rotate the password, generate a fresh one, write the bcrypt entry, update the on VM record and reload nginx:
NEW_PW="$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | cut -c1-24)"
sudo htpasswd -nbB admin "$NEW_PW" | sudo tee /etc/nginx/.vroom.htpasswd >/dev/null
sudo chown root:www-data /etc/nginx/.vroom.htpasswd
sudo chmod 0640 /etc/nginx/.vroom.htpasswd
sudo sed -i "s|^VROOM_PASSWORD=.*|VROOM_PASSWORD=${NEW_PW}|" /root/vroom-credentials.txt
sudo systemctl reload nginx
echo "rotated; new password is in /root/vroom-credentials.txt"
Copy the new value into your secret manager before you disconnect.
Enabling HTTPS
Port 80 carries HTTP Basic Auth credentials, so terminate TLS in front of it for anything beyond a private subnet. Either put the VM behind an Azure Application Gateway or Front Door with a certificate attached, or add a certificate directly to the nginx server block at /etc/nginx/sites-available/cloudimg-vroom, adding a listen 443 ssl; server with your ssl_certificate and ssl_certificate_key, then open TCP 443 in both ufw and your NSG:
sudo ufw allow 443/tcp
sudo nginx -t
Backups
VROOM is stateless. Each request is solved and discarded, and nothing is persisted between calls, so there is no database to back up. The only machine specific state worth keeping is /root/vroom-credentials.txt and any edits you have made to /opt/vroom-express/config.yml.
sudo tar czf /tmp/vroom-config-backup.tar.gz /opt/vroom-express/config.yml /root/vroom-credentials.txt
sudo ls -lh /tmp/vroom-config-backup.tar.gz
Troubleshooting
Check service health and recent logs:
sudo systemctl status vroom-express --no-pager | head -12
sudo journalctl -u vroom-express -n 30 --no-pager
- Every request returns
401. The first boot service has not completed, or the password is wrong. Confirm the sentinel exists withls -l /var/lib/cloudimg/vroom-firstboot.doneand re read/root/vroom-credentials.txt. - A solve returns
"code": 3. This is a routing error: the request used coordinates rather than a supplied matrix, and no routing engine is reachable. Either switch to matrix mode or complete Step 9. - A solve returns
"code": 2. Input error. The message names the offending field; the most common causes are a matrix that is not square, alocation_indexoutside the matrix, or duplicate job ids. - Some jobs come back in
unassigned. The problem is over constrained rather than broken. Check capacity against total demand, whether time windows are physically reachable given your matrix, and whether skills exclude every vehicle. Raising a job'sprioritybiases the solver toward including it. - HTTP 413. The request exceeded
limitinconfig.ymlorclient_max_body_sizein nginx. Raise both. - Solves are slow. Lower
explore, raisethreadson a larger VM, or reduce the problem size. Solve time grows quickly with location count.
Access logs, including one line per request, are under /var/log/vroom-express:
sudo ls -l /var/log/vroom-express/
Support
cloudimg provides 24/7 support with a 24h response SLA for this image. Contact support@cloudimg.co.uk with the output of sudo systemctl status vroom-express --no-pager and the relevant journalctl extract.
VROOM and vroom-express are open source under the BSD 2-Clause licence; the upstream licence texts ship in /opt/vroom/licenses/. Upstream project documentation lives at github.com/VROOM-Project/vroom.