Deployment guide

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-express 0.12.0 on Node.js 22 LTS, run as a dedicated non-root vroom user under a hardened systemd unit
  • An nginx reverse proxy on port 80 with per-VM HTTP Basic Auth, ready for TLS
  • A 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
  • 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.

1

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).

2

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 Standard
3

Connect via SSH

ssh azureuser@<vm-ip>
4

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/healthz

Expect both services active, vroom 1.15.0, the two listening sockets, and ok from the health endpoint.

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. Read it as root:

sudo cat /root/vroom-credentials.txt

Use 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.

6

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 -12

Expect 401, 401, and a firewall that is active with Default: deny (incoming) and only 22/tcp and 80/tcp allowed.

7

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 -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.

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.

8

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 -30

You 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 -25

The 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:

  • 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 tries to reach a routing engine to fetch them.

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 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.yml

Change the host and port under the profile you use, then restart the service:

sudo systemctl restart vroom-express
sudo systemctl is-active vroom-express

Once 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.

10

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 -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. The default, 5, is the most thorough.
  • 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, and nginx is configured to match. Raise both together if you routinely solve very large problems.
  • override is false on purpose. Setting it to 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 the service after editing:

sudo systemctl restart vroom-express curl -s http://127.0.0.1/healthz

Security model

  • Private service. 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.
  • Authenticated API. 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.
  • Per-VM credentials. The password is generated on first boot. It is 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.
  • Unprivileged runtime. 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. 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 -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 that the sentinel exists with ls -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, 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, 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

Deploy on Azure