Applications Azure

VROOM on Ubuntu 24.04 on Azure User Guide

| Product: VROOM on Ubuntu 24.04 LTS on Azure

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-express 0.12.0 on Node.js 22 LTS, run as a dedicated non root vroom user under a hardened systemd unit
  • nginx reverse proxy on port 80 with per VM HTTP Basic Auth, ready for TLS
  • ufw host firewall, default deny inbound, admitting only SSH and HTTP
  • An unauthenticated /healthz endpoint 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 0600 file
  • 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.

VROOM service status, version, listening sockets and the open health probe

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.

The optimisation API rejecting unauthenticated and wrong password requests, and the host firewall admitting only SSH and HTTP

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.

A real time windowed vehicle routing solve from a supplied duration matrix, showing the optimised route

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.

A capacity constrained delivery plan solved across two vans

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:

  • capacity on a vehicle and delivery / pickup on a job — multi dimensional load limits (weight, volume, pallet count, all at once)
  • time_windows on a job and time_window on a vehicle — opening hours and driver shifts
  • skills — 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 first
  • breaks — mandatory driver rest periods inside a shift
  • max_travel_time, max_distance, max_tasks — per vehicle limits
  • costs with fixed, 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
  • threads is 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) and maxvehicles (200) bound request size, and limit (20mb) bounds the request body. Large matrices are large JSON: a 1000 by 1000 duration matrix is several megabytes.
  • timeout is 300000 ms. nginx is configured to match; raise both together if you routinely solve very large problems.
  • override is false on purpose. Setting it true lets 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-express is bound to the VM only. nginx on port 80 is the sole public listener, and ufw admits 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 static ok string 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.htpasswd and in clear only in /root/vroom-credentials.txt (mode 0600, root only). The image itself ships an empty .htpasswd, which fails closed: before first boot completes, every request is rejected with 401.
  • The service runs as the unprivileged vroom user 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 with ls -l /var/lib/cloudimg/vroom-firstboot.done and 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, a location_index outside 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's priority biases the solver toward including it.
  • HTTP 413. The request exceeded limit in config.yml or client_max_body_size in nginx. Raise both.
  • Solves are slow. Lower explore, raise threads on 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.