uTask on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of uTask on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. uTask is an open source automation engine from OVH for turning operational runbooks into auditable, repeatable, self service tasks. You describe a process once as a YAML task template, with typed and validated inputs, steps that declare their dependencies, conditions, loops and schema checks on each step's output, and uTask executes it as a resumable, fully audited task: it retries what can be retried, pauses for human validation where you ask it to, and keeps a complete history of everything that ran, who requested it and what it returned.
The image installs uTask 1.34.0 from the official upstream release (a single self contained Go binary, SHA-256 verified during the build) with its PostgreSQL database on the same machine, and the official prebuilt Angular dashboard. A web dashboard and a REST API expose your templates to the people and pipelines that need them.
Authentication follows the shape uTask itself prescribes. uTask deliberately ships with no built in login: upstream documents that it "is meant to be placed behind a reverse proxy that provides a username through the x-remote-user http header". In this image nginx is that reverse proxy on port 80 and the sole network facing surface. It authenticates every request with HTTP Basic against a per instance credential and passes the verified username on to uTask, overwriting anything a client tries to claim for itself. uTask itself listens on 8081 and is bound away from the network by a host firewall (nftables), so the proxy cannot be sidestepped.
Secure by default, no shared credentials. Nothing secret is baked into the image, and the image ships with no database at all. On the first boot of every instance, uTask generates the PostgreSQL role password, the dashboard/API password and the storage encryption key that protects every task input and result at rest, then creates the database, loads the schema and writes the credentials to a root only file. No build time data and no shared key can follow you into production.
What is included:
-
uTask 1.34.0 (BSD-3-Clause) from the official upstream release binary, run under systemd (
utask.service) as an unprivilegedutaskuser, bound to127.0.0.1:8081 -
PostgreSQL (
postgresql.service), uTask's only dependency, bound to loopback and created fresh with a per instance role password on first boot -
nginx (
nginx.service) on port 80 as the only network facing service: it authenticates with HTTP Basic against a per instance credential and injects the authenticated username asx-remote-user -
An nftables rule (
nftables.service) that drops inbound8081from anywhere except the loopback interface, so the reverse proxy cannot be bypassed -
Two self contained example templates (
hello-worldandhttp-healthcheck) preloaded so the dashboard is useful the moment you log in -
A per instance dashboard/API password, database password and storage encryption key, generated fresh on first boot, with the values written to
/root/utask-credentials.txt
Prerequisites
-
Active Azure subscription, SSH public key, VNet + subnet in target region
-
Subscription to the uTask listing on Azure Marketplace
-
Network Security Group rules allowing TCP 22 (admin) and TCP 80 (the dashboard and REST API) from the operators and pipelines that use uTask
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for a team automation engine. Increase the size when you run many concurrent tasks or store a large task history.
Step 1: Deploy from the Azure Portal
Search uTask in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 80 from the workstations and pipelines that will use uTask, and TCP 22 for administration. Keep 80 restricted to the networks you serve; put your own TLS certificate and domain in front for production.
Step 2: Deploy from the Azure CLI
RG="utask-prod"; LOCATION="eastus"; VM_NAME="utask1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/utask-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$GALLERY_IMAGE_ID" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1002
Step 3: First boot and per instance credentials
On first boot the image generates the per instance secrets, creates the PostgreSQL database and role, loads the schema, starts uTask and writes everything to /root/utask-credentials.txt. This completes within seconds. SSH in as azureuser and read the credentials file:
sudo cat /root/utask-credentials.txt
The file is 0600 root:root and records the dashboard URL, the admin username and password for the dashboard and API, and the database name, role and password. These values are unique to this instance. The storage encryption key that protects task data at rest lives in /etc/utask/utask.env and is likewise unique per instance.
Step 4: Confirm the services are running
SSH to the instance and confirm the four services are active and that nginx on port 80 is the only network facing listener, while uTask and PostgreSQL are bound to loopback:
systemctl is-active postgresql nftables utask nginx
ss -tlnp | grep -E ':80 |:8081 |:5432 '
Expected output:
active
active
active
active
LISTEN 0.0.0.0:80
LISTEN *:8081
LISTEN 127.0.0.1:5432
nginx listens on port 80 on all interfaces; PostgreSQL is bound to loopback. uTask binds 8081 on all interfaces because it has no bind address option of its own, which is exactly why the nftables rule below matters: it is what keeps 8081 unreachable from the network.

Step 5: uTask is secure by default
uTask has no authentication of its own and trusts the x-remote-user header, so this image places it behind an authenticating reverse proxy and firewalls it off the network. Confirm that the dashboard and API require the per instance credential, that a client cannot reach uTask directly to spoof its identity, and that nginx sets the authenticated username itself:
PW=$(sudo grep -E '^UTASK_ADMIN_PASSWORD=' /root/utask-credentials.txt | cut -d= -f2-)
HOSTIP=$(hostname -I | awk '{print $1}')
# public liveness routes
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/unsecured/mon/ping
# the API is rejected without the per instance credential, accepted with it
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/template
curl -s -o /dev/null -w '%{http_code}\n' -u admin:"$PW" http://127.0.0.1/template
# uTask on :8081 is unreachable off loopback, so x-remote-user cannot be spoofed
curl -s -o /dev/null -w '%{http_code}\n' -m 5 -H 'x-remote-user: admin' http://"$HOSTIP":8081/meta
# nginx reports the AUTHENTICATED username, overwriting any client supplied value
curl -s -u admin:"$PW" -H 'x-remote-user: not-this-user' http://127.0.0.1/meta
Expected output:
200
200
401
200
000
{"application_name":"uTask","user_is_admin":true,"username":"admin","user_groups":[],"version":"v1.34.0",...}
The /template API returns 401 with no credential and 200 with the per instance credential. The direct request to 8081 returns nothing (000) because the firewall dropped it. The /meta response reports admin, the authenticated user, not the not-this-user value the client tried to inject.

Step 6: Run a task through the engine
This is what uTask exists to do. Create a task from the preloaded hello-world template through the REST API with the per instance credential, then watch the engine resolve it to state DONE and return a result:
PW=$(sudo grep -E '^UTASK_ADMIN_PASSWORD=' /root/utask-credentials.txt | cut -d= -f2-)
# create a task from the hello-world template
TID=$(curl -s -u admin:"$PW" -H 'Content-Type: application/json' \
-d '{"template_name":"hello-world","input":{"name":"cloudimg"}}' \
http://127.0.0.1/task | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
echo "created task: $TID"
# poll until the engine resolves it
curl -s -u admin:"$PW" http://127.0.0.1/task/"$TID" | python3 -m json.tool | grep -A3 '"result"'
Expected output:
created task: 47ac2de2-f738-4d97-888e-71e1df1c2007
"result": {
"greeting": "Hello cloudimg, this task ran on uTask by cloudimg"
},
The greeting field was produced by the template's echo step: the engine actually executed the workflow and recorded its output. A real task, not a page that merely loaded.

Step 7: Open the dashboard
Open http://<vm-public-ip>/ui/dashboard/ in a browser. Your browser will prompt for HTTP Basic credentials: enter admin and the UTASK_ADMIN_PASSWORD from the credentials file. The Tasks page lists every task and its state; the tasks you ran through the API in Step 6 appear here in green DONE state.

Click any task to see its detail: the task's status, the request that created it, the execution, and the Result panel with the exact output the engine produced.

The Templates page lists the automations available to run. The image ships two self contained example templates so the dashboard is useful immediately.

Click New task to launch an automation from the browser: pick a template, fill in its typed inputs and submit. This is the self service path your team uses without touching the API.

Step 8: Author your own task templates
Templates live in /opt/utask/templates as YAML files. A template declares typed inputs, then steps with dependencies, conditions and action blocks. uTask ships builtin actions for HTTP calls, SSH commands, scripts, email, notifications and subtasks, so a template can drive the systems a real procedure touches. Copy the bundled hello-world.yaml as a starting point:
sudo cp /opt/utask/templates/hello-world.yaml /opt/utask/templates/my-first-template.yaml
sudo nano /opt/utask/templates/my-first-template.yaml
sudo systemctl restart utask
uTask loads templates from that directory at startup, so restart the service after adding or editing a template. Confirm it was picked up over the API:
PW=$(sudo grep -E '^UTASK_ADMIN_PASSWORD=' /root/utask-credentials.txt | cut -d= -f2-)
curl -s -u admin:"$PW" http://127.0.0.1/template | python3 -c 'import json,sys; print([t["name"] for t in json.load(sys.stdin)])'
The full template authoring reference — value templating, step conditions, loops, JSON schema validation and the builtin action catalogue — is documented upstream at github.com/ovh/utask.
Step 9: Per instance credentials and the encryption key
Every secret on the instance is unique to that instance and generated at first boot:
sudo cat /root/utask-credentials.txt
sudo -u postgres psql -tAc 'SELECT name FROM task_template ORDER BY name' utask
The credentials file is 0600 root:root. The storage encryption key in /etc/utask/utask.env is a 32 byte aes-gcm key that encrypts every task input and result at rest — back it up before you rotate it, because previously stored task data becomes unreadable without the key it was written with. The key rotation procedure is documented upstream.

Step 10: Security recommendations
-
Front uTask with your own TLS certificate and domain. nginx serves plain HTTP on port 80; put your own certificate and a real hostname in front for production so credentials and task data are encrypted in transit.
-
Restrict port 80 to the networks you serve. A private automation engine rarely needs to be reachable from the whole internet; scope the Network Security Group to your operators and pipelines.
-
Integrate your own identity provider. For teams, replace the single HTTP Basic credential by having your reverse proxy or identity provider set
x-remote-user(and optionally group headers) from real authenticated users; uTask uses that username for authorization and itsadmin_usernameslist. -
Rotate the dashboard credential and back up the encryption key. The per instance dashboard password lives in the nginx htpasswd; rotate it with
htpasswd -B. Always back up the storage encryption key before rotating it. -
Keep the base image patched. Unattended security upgrades are enabled; reboot after kernel updates.
Step 11: Support and Licensing
This is a repackaged open source software product with additional charges for cloudimg support services. uTask is licensed under the BSD 3 Clause License. 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
The cloudimg uTask image is available on the Azure Marketplace. Search uTask by cloudimg, deploy into your subscription, and follow this guide.
Need Help?
cloudimg provides 24/7 technical support for this uTask product. We help with deployment and first boot configuration, authoring and debugging task templates, fronting the dashboard with your own TLS certificate and custom domain, integrating your identity provider, wiring notification backends, encryption key rotation, PostgreSQL sizing and backup, and uTask version upgrades. Critical issues receive a one hour average response time. Contact support@cloudimg.co.uk.