Hookdeck Outpost on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Hookdeck Outpost on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Outpost is open source outbound webhooks and event delivery infrastructure: a Bearer authenticated REST API that routes, signs, queues, retries and delivers the events your application sends to your users' destinations. Instead of building and operating a reliable event pipeline yourself, you create tenants, register their destinations (webhooks, message queues and more), publish events through the API, and Outpost handles signing, fan out, delivery, retries and full delivery logs.
The cloudimg image ships the free and open source, Apache 2.0 licensed Outpost service (a Go binary), run the officially supported way as the upstream containers pinned by image digest, alongside a bundled PostgreSQL log store, a bundled Valkey entity store and a bundled RabbitMQ message queue. All images are pinned by digest and captured into the VM, so your instance starts in seconds. Because Outpost is a control plane for outbound events, nothing ships with a known secret: a unique admin API key, a unique JWT signing secret, a unique encryption secret and unique datastore passwords are generated for each VM on first boot, before the port is reachable. Backed by 24/7 cloudimg support.
Hookdeck and Outpost are trademarks of their respective owner. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by Hookdeck. It ships the free and open source Apache 2.0 licensed self hosted software, unmodified.

What is included:
- Hookdeck Outpost v1.0.7 (the Apache 2.0 licensed Outpost Go service), pinned by image digest, run as its api, delivery and log workers
- A bundled PostgreSQL 16 log store, pinned by image digest, reachable only inside a private Docker network (never published to a host port)
- A bundled Valkey 8 entity store, pinned by image digest, password protected and reachable only inside the private Docker network
- A bundled RabbitMQ 3 message queue, pinned by image digest, password protected and reachable only inside the private Docker network
- Docker Engine with the Outpost API published to the loopback interface only, fronted by nginx on port
80 outpost.service,outpost-firstboot.service,outpost-postboot.serviceandnginx.serviceas systemd units, enabled and active on boot- A unique admin API key, a unique JWT signing secret, a unique encryption secret and unique PostgreSQL, Valkey and RabbitMQ passwords generated per VM on first boot, never baked into the image
- Clean, empty datastores on first boot: no default login, no shipped secret, no prior data
- Ubuntu 24.04 LTS base with latest security patches applied at build time
- Azure Linux Agent for seamless cloud integration and SSH key injection
- 24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
- Active Azure subscription, SSH public key, VNet + subnet in target region
- Subscription to the Hookdeck Outpost listing on Azure Marketplace
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is a sensible starting point. For higher event volumes use Standard_D2s_v5 or larger. NSG inbound: allow 22/tcp from your management network and 80/tcp for the API from the networks that call it.
Step 1: Deploy from the Azure Portal
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Hookdeck Outpost by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create.
Step 2: Deploy from the Azure CLI
RG="outpost-prod"; LOCATION="eastus"; VM_NAME="outpost-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/outpost-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 outpost-vnet --address-prefix 10.100.0.0/16 --subnet-name outpost-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name outpost-nsg
az network nsg rule create -g "$RG" --nsg-name outpost-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 outpost-nsg --name allow-http --priority 110 \
--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 outpost-vnet --subnet outpost-subnet --nsg outpost-nsg --public-ip-sku Standard
Step 3: Connect to your VM
ssh azureuser@<vm-ip>
Step 4: Confirm the services are running
The stack runs as the Outpost api, delivery and log workers plus a bundled PostgreSQL, Valkey and RabbitMQ, all under one outpost.service, fronted by nginx. Confirm the services are active, see the containers, and check which ports are bound. nginx listens on :80, the Outpost API is published to 127.0.0.1:3333 (loopback only), and the datastores have no host port at all:
sudo systemctl is-active docker outpost nginx
sudo docker compose -f /etc/outpost/compose.yaml ps
sudo ss -tlnp | grep -E ':80 |:3333'
You will see all three services report active, the containers Up (with PostgreSQL, Valkey and RabbitMQ healthy), and nginx bound on :80 with the Outpost API bound only on 127.0.0.1:3333. PostgreSQL (5432), Valkey (6379) and RabbitMQ (5672) never appear on a host port because they are reachable only inside the private Docker network.
Step 5: Read the per instance credentials
A unique admin API key, JWT signing secret, encryption secret and datastore passwords were generated for this VM on the first boot, before the port was reachable, and written to a root only file. Read them:
sudo cat /root/outpost-credentials.txt
The file (mode 0600 root:root) holds API_KEY (the admin key you authenticate the API with), API_JWT_SECRET, AES_ENCRYPTION_SECRET, POSTGRES_PASSWORD, REDIS_PASSWORD, RABBITMQ_PASSWORD, and OUTPOST_URL (the address to reach this VM on). None of these ship in the image; every value is unique to this VM, so no two instances share a credential and there is no default key to change.

Step 6: Verify the API security model
The health endpoint is public, but every admin endpoint requires the bearer key. Confirm that an unauthenticated request is rejected with 401, while the per instance key authenticates a real call:
API_KEY=$(sudo grep '^API_KEY=' /root/outpost-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'healthz (no auth): %{http_code}\n' http://localhost/api/v1/healthz
curl -s -o /dev/null -w 'tenants (no key): %{http_code}\n' http://localhost/api/v1/tenants
curl -s -X PUT -H "Authorization: Bearer $API_KEY" http://localhost/api/v1/tenants/acme >/dev/null
curl -s -o /dev/null -w 'tenant/acme (with key): %{http_code}\n' -H "Authorization: Bearer $API_KEY" http://localhost/api/v1/tenants/acme
The health check returns 200 with no authentication, the unauthenticated tenants call is rejected with 401, and reading the tenant back with the per instance key returns 200. This is the whole security model: the health probe is open for load balancers, and everything else is gated by the admin API key.

Step 7: Publish your first event
The core workflow is: create a tenant (one per customer or team), register their destinations (the webhooks and queues Outpost delivers to), then publish events. Outpost routes each event to every matching destination, signs it, queues it and delivers it, retrying with backoff on failure. The image ships with a wildcard topic (*) so destinations subscribed to * receive every event out of the box. Run the whole flow from the VM:
API_KEY=$(sudo grep '^API_KEY=' /root/outpost-credentials.txt | cut -d= -f2-)
curl -s -X PUT http://localhost/api/v1/tenants/orders-team \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{"metadata":{"name":"Orders team"}}'
echo
curl -s -X POST http://localhost/api/v1/tenants/orders-team/destinations \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{"type":"webhook","topics":["*"],"config":{"url":"https://example.com/webhook"}}'
echo
curl -s -X POST http://localhost/api/v1/publish \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-d '{"tenant_id":"orders-team","topic":"*","eligible_for_retry":true,"data":{"order_id":"ord_123","amount":4200}}'
echo
Each call returns the created object as JSON. The destination create returns a webhook id and a signing secret, and the final POST /api/v1/publish returns an event id and the destination_ids it was delivered to; Outpost has accepted the event, signed it, and queued it for delivery to the https://example.com/webhook destination with automatic retries. Point the destination URL at a real receiver of yours (for a quick test, a request bin) and you will see the signed delivery arrive.

Step 8: Call the API from your own machine
From anywhere that can reach the VM on port 80, use the admin API key and the VM public IP. Retrieve the key once (over SSH) and then call the API with your VM address:
curl -H "Authorization: Bearer <API_KEY>" http://<vm-ip>/api/v1/tenants/acme
For production, use the official Outpost SDKs (available for Go, Python and TypeScript). Point the SDK's base URL at http://<vm-ip>/ (or your own domain, see Step 10) and pass the admin API key, then manage tenants, destinations and events from your application code exactly as the curl calls above do.
Step 9: Configure your own event topics
The image ships with a wildcard topic (*) so you can publish and subscribe immediately. To use your own named event topics (for example user.created, order.shipped), set the comma separated TOPICS list in the per instance environment file, then restart the stack:
sudo sed -i 's/^TOPICS=.*/TOPICS=user.created,user.updated,order.shipped/' /etc/outpost/outpost.env
sudo systemctl restart outpost
Destinations then subscribe to specific topics ("topics":["user.created"]) or to all of them ("topics":["*"]), and you publish events on those topic names. View the currently valid topics any time with curl -H "Authorization: Bearer $API_KEY" http://localhost/api/v1/topics.
Step 10: Use your own domain and HTTPS (production)
The image serves the API over plain HTTP on port 80, which is convenient behind a load balancer or private network. For production you should terminate TLS in front of Outpost so keys and payloads travel encrypted:
- Front the VM with Azure Application Gateway, a load balancer with a managed certificate, or a CDN, and forward to nginx on port
80. - Or install your own certificate: add a TLS
serverblock to/etc/nginx/sites-available/outpostreferencing your certificate and key, open443/tcpon the NSG, thensudo systemctl reload nginx. - Point a DNS name you control at the VM public IP, then set
OUTPOST_URLin/etc/outpost/outpost.envtohttps://your-domain/and use that name as the base URL in your Outpost SDKs.
Step 11: Server components
| Component | Version / Detail |
|---|---|
| Event delivery service | Hookdeck Outpost v1.0.7 (Go binary, pinned by image digest) |
| Workers | Outpost api, delivery and log services from one image |
| API front | nginx on :80 reverse proxying to 127.0.0.1:3333 |
| Log store | bundled PostgreSQL 16 (private Docker network, no host port) |
| Entity store | bundled Valkey 8 (private Docker network, no host port, password protected) |
| Message queue | bundled RabbitMQ 3 (private Docker network, no host port, password protected) |
| Orchestration | Docker Compose under outpost.service |
| Admin API key | per instance, generated first boot into /etc/outpost/outpost.env |
| Credentials file | per instance, written first boot into /root/outpost-credentials.txt |
| Operating system | Ubuntu 24.04 LTS (patched at build) |
| License | Apache 2.0 (Outpost) |
Step 12: Managing the Outpost service
sudo systemctl status outpost --no-pager | head -12
sudo docker compose -f /etc/outpost/compose.yaml logs --tail 40 api
Restart the whole stack with sudo systemctl restart outpost, follow the API logs live with sudo docker compose -f /etc/outpost/compose.yaml logs -f api, and view configuration in /etc/outpost/outpost.env and /etc/outpost/compose.yaml. After changing the environment file, restart the service to apply it.
Step 13: Security recommendations
- Restrict the NSG. Allow TCP 80 (and 22 for admin) only from the networks that call the API.
- Terminate TLS in front of Outpost (Step 10) so the bearer key and payloads are encrypted in transit.
- Treat the API key like a password. It is stored in
/root/outpost-credentials.txt(root only); rotateAPI_KEYin/etc/outpost/outpost.envand restart if it is ever exposed. - Keep the bundled datastores private. PostgreSQL, Valkey and RabbitMQ are on a private Docker network with no host port; keep them that way.
- Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Step 14: Support and Licensing
Outpost is distributed under the Apache License 2.0. This cloudimg image bundles the unmodified official open source Outpost release; cloudimg provides the packaging, the bundled PostgreSQL, Valkey and RabbitMQ, the per instance secret generation, the paired deploy guide, and 24/7 support with a guaranteed 24 hour response SLA. Outpost is an independent open source project by Hookdeck and this image is not affiliated with or endorsed by Hookdeck.
Deploy on Azure
Find Hookdeck Outpost on Ubuntu 24.04 on the Azure Marketplace, published by cloudimg. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email support@cloudimg.co.uk for deployment help, configuration questions, or licensing enquiries.