VROOM
Overview
VROOM (Vehicle Routing Open source Optimization Machine) is a fast solver for vehicle routing problems. Given a set of jobs and a fleet of vehicles, it decides which vehicle does what, and in which order, to minimise 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.
Problems with hundreds of stops solve in seconds, which makes VROOM practical for same-day dispatch and interactive planning rather than overnight batch runs. This SecureAnyCloud 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
VROOM needs to know how long it takes to travel between your locations, and it can get that information in one of two ways. Read this section before you deploy, because it determines what works on the image out of the box.
Matrix mode works out of the box, with no external service. You supply the travel-time matrix (and optionally distance and cost) in the request itself, and refer to locations by location_index. VROOM does the optimisation and contacts nothing else. This is the mode the image is 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 suits you when you already have travel times from one of your own systems, 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 the image at. To 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. Support for OSRM, Openrouteservice and Valhalla is compiled into this image, 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. This is deliberate: a useful OpenStreetMap extract runs to many gigabytes, would not fit the recommended VM size, and carries its own redistribution obligations. Step 9 explains how to connect an engine.
In short, the solver is complete and self-contained, and the travel-time matrix is input data. If you already have travel times, the 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, with 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. 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.
On each VM's first boot, a vroom-firstboot.service oneshot generates a unique per-VM password and writes a bcrypt .htpasswd. It then proves that the gate rejects unauthenticated and wrong-password requests, proves that a real solve returns the correct route, and disables itself. No two VMs share a password, and none is baked into the image.
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 you get:
- 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- An nginx reverse proxy on port 80 with per-VM HTTP Basic Auth, ready for TLS
- A
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 - An Ubuntu 24.04 LTS base, patched at build time
- 24/7 SecureAnyCloud support, 24h response SLA
Prerequisites
You need 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. Use Standard_B2s (2 vCPU, 4 GB RAM) for evaluation and small fleets. Use Standard_F4s_v2 or larger for production dispatch with hundreds of stops.
Deploy from the Azure Portal
Search the Azure Marketplace for VROOM on Ubuntu 24.04 and choose your VM size.
Attach an NSG that allows TCP 22 (SSH) from your management network and TCP 80 (the API) from the applications that submit optimisation requests. In production, front port 80 with TLS (see Enabling HTTPS below).
Deploy from the Azure CLI
RG="vroom-prod"; LOCATION="eastus"; VM_NAME="vroom-01" IMAGE_URN="sac1647283583153:vroom-ubuntu-24-04:default:latest" 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
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:vroom-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
--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 StandardConnect via SSH
ssh azureuser@<vm-ip>Verify the services are running
Run the commands below on the VM. 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/healthzExpect both services active, vroom 1.15.0, the two listening sockets, and ok from the health endpoint.
Retrieve your per-VM password
The first-boot service generated a password unique to this VM and stored it in a root-only file. Read it as root:
sudo cat /root/vroom-credentials.txtUse the VROOM_PASSWORD value with the admin username. Store it in your secret manager. It is not recoverable from anywhere else, and it is different on every VM you launch from this image.
Confirm the API is protected
Before you send real work, confirm that the gate is closed. An unauthenticated request and a wrong-password request must both be rejected with 401. 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/sac-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/sac-sample-problem.json http://127.0.0.1/
sudo ufw status verbose | head -12Expect 401, 401, and a firewall that is active with Default: deny (incoming) and only 22/tcp and 80/tcp allowed.
Solve your first routing problem
Submit the ready-to-run problem at /opt/vroom/samples/sac-sample-problem.json. It has 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/sac-sample-problem.json http://127.0.0.1/ \
| python3 -m json.tool | head -40VROOM 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.
The result also shows the scheduler at work. 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. This cuts total waiting time to 300 seconds.
Solve a capacity-constrained multi-vehicle problem
The second sample models four deliveries in two geographic clusters, served by two vans that can each 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/sac-sample-capacity.json http://127.0.0.1/ \
| python3 -m json.tool | head -30You get two routes, zero unassigned jobs, and each van carrying exactly its capacity.
Write 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 -25The matrix is square and measured in seconds. Row and column i correspond to location_index i. Entries need not be symmetric: one-way systems and turn restrictions are exactly why they often are not.
These modelling keys are 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 tries to reach a routing engine to fetch them.
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 you provision the engine yourself. Run OSRM, Openrouteservice or Valhalla on another host (or in a container on this VM) with a map extract that covers your operating area.
Edit the routingServers section of the configuration to set 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.ymlChange the host and port under the profile you use, then restart the service:
sudo systemctl restart vroom-express
sudo systemctl is-active vroom-expressOnce the engine is reachable, requests can use "location": [lon, lat] on jobs and "start" / "end" on vehicles, and can omit matrices entirely. Coordinates are always [longitude, latitude], in that order. This is 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.
Tune for larger problems
Edit the configuration in /opt/vroom-express/config.yml. List the active settings with:
sudo grep -vE '^\s*#|^\s*$' /opt/vroom-express/config.yml | head -20threadsis 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. The default, 5, is the most thorough.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, and nginx is configured to match. Raise both together if you routinely solve very large problems.overrideisfalseon purpose. Setting it totruelets 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 the service after editing:
sudo systemctl restart vroom-express curl -s http://127.0.0.1/healthzSecurity model
- Private service.
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. - Authenticated API. 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. - Per-VM credentials. The password is generated on first boot. It is 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. - Unprivileged runtime. 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. You have two options.
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/sac-vroom by adding a listen 443 ssl; server with your ssl_certificate and ssl_certificate_key. With the direct option, open TCP 443 in both ufw and your NSG:
sudo ufw allow 443/tcp
sudo nginx -tBackups
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.gzTroubleshooting
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 that the sentinel exists withls -l /var/lib/sac/data/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. This is an input error, and 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, with one line per request, are under /var/log/vroom-express:
sudo ls -l /var/log/vroom-express/Support
SecureAnyCloud provides 24/7 support with a 24h response SLA for this image. Contact sales@secureanycloud.com 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, and the upstream licence texts ship in /opt/vroom/licenses/. Upstream project documentation is at github.com/VROOM-Project/vroom.
Deploy on Azure
Launch VROOM on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com