otf on Ubuntu 24.04 on Azure User Guide
Overview
otf is an open source, self hosted alternative to Terraform Enterprise and HCP Terraform. It runs OpenTofu and Terraform plans and applies with a web interface, remote state management, run history and role based access control, so a team can collaborate on infrastructure as code on their own infrastructure instead of a SaaS. The cloudimg image installs otf 0.6.3 as a complete single node deployment on Ubuntu 24.04 LTS: the otfd daemon (which includes a built in server runner, so it executes remote runs on its own with no separate agent), a local PostgreSQL database, and nginx terminating TLS in front. The daemon binds loopback only and runs as a dedicated unprivileged system user. The application encryption secret, the site admin token and the PostgreSQL password are all generated fresh on the first boot of every VM, so there is no default login. Backed by 24/7 cloudimg support.
What is included:
- otf 0.6.3 (
otfd) and the OpenTofu 1.12.4 CLI - PostgreSQL backing otf's state, runs and RBAC, bound to loopback
- nginx terminating TLS on port 443 with an automatic redirect from port 80
- A per-VM site admin token, application secret and database password generated on first boot into a root only credentials file
- A pre warmed OpenTofu engine cache so the first run does not wait on a download
otfd.service,postgresql.serviceandnginx.serviceas systemd units, enabled and active- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point. NSG inbound: allow 22/tcp from your management network and 443/tcp for the web UI and the Terraform/OpenTofu API. otf serves everything over HTTPS; port 80 only issues a redirect to 443.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for otf 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 HTTPS (443). Then Review + create -> Create.
Step 2 - Deploy from the Azure CLI
Replace the resource group, location and admin username to suit your environment. This launches the latest cloudimg otf image, opens ports 22 and 443, and prints the public IP.
az vm create \
--resource-group my-rg \
--name otf1 \
--image cloudimg:otf-ubuntu-24-04:otf-ubuntu-24-04:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port --resource-group my-rg --name otf1 --port 22 --priority 1001
az vm open-port --resource-group my-rg --name otf1 --port 443 --priority 1002
Step 3 - Connect to your VM
Use the public IP shown by the deployment. otf's first boot generates the per-VM secrets and starts the services automatically.
ssh azureuser@<public-ip>
Step 4 - Confirm the services are running
otf's daemon, PostgreSQL and nginx are enabled systemd units. Confirm each is active and check the unauthenticated health endpoint, which nginx forwards to the daemon.
otfd --version
sudo systemctl is-active otfd postgresql nginx
curl -fsS http://127.0.0.1:8080/healthz
You should see version otfd version 0.6.3, three active lines, and {"status":"OK"}.

Step 5 - Retrieve your site admin token
Every VM generates a unique site admin token, application secret and database password on first boot, written to a root only file. Read the token and the URL:
sudo cat /root/otf-credentials.txt

The site admin token is the credential you paste into the web UI, and it is also the bearer token for the API and the OpenTofu/Terraform CLI. Keep it safe.
Step 6 - Sign in to the web UI
Browse to https://<public-ip>/. The TLS certificate is self signed on first boot, so your browser will warn on the first visit; accept it (or install a trusted certificate as shown later). On the login page click the site admin link in the bottom right, paste the OTF_SITE_TOKEN value from the credentials file, and choose Login.

Once signed in you land on the organizations view, where you create organizations and drill into their workspaces, runs, state and variables.

Step 7 - Verify the API
The site admin token authenticates the Terraform Enterprise compatible API. A request with the token succeeds (HTTP 200); a request without one is rejected (HTTP 401). Run this on the VM to confirm the round-trip:
TOKEN=$(sudo grep '^OTF_SITE_TOKEN=' /root/otf-credentials.txt | cut -d= -f2-)
echo -n 'no token -> '; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/api/v2/organizations
echo -n 'with token -> '; curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/api/v2/organizations

Step 8 - Run your first plan
otf executes runs with the CLI driven remote workflow: you write an ordinary OpenTofu (or Terraform) configuration whose cloud block points at your otf server, and tofu plan and tofu apply run remotely on the daemon's built in server runner. No VCS is required for this workflow.
From your workstation (which has the tofu or terraform CLI installed), save the site admin token so the CLI can authenticate, replacing <public-ip> and <your-token>:
mkdir -p ~/.terraform.d
cat > ~/.terraform.d/credentials.tfrc.json <<'JSON'
{ "credentials": { "<public-ip>": { "token": "<your-token>" } } }
JSON
Create a working directory with a minimal configuration. The cloud block's hostname must match your VM address and the organization and workspace are created automatically if they do not exist:
terraform {
cloud {
hostname = "<public-ip>"
organization = "cloudimg"
workspaces { name = "dev" }
}
}
resource "terraform_data" "hello" {
input = "hello from otf"
}
Then initialise and run. Because the certificate is self signed, either install a trusted certificate (below) or point the CLI at the VM's certificate first.
# run from the directory holding your main.tf (cloud hostname = <public-ip>)
tofu init
tofu plan
tofu apply
The plan and apply execute on the server; the streamed output, the run status and the resulting state are all recorded in the workspace. Open the workspace in the web UI to watch the run and inspect the managed resources and state.


Step 9 - Connect a VCS provider (optional)
To trigger runs from a Git repository, connect a VCS provider. otf supports GitHub, GitLab and other OAuth providers, configured at runtime - nothing is baked into the image. In the provider (for example GitHub -> Settings -> Developer settings -> OAuth Apps) create an OAuth app whose callback URL is https://<your-domain>/oauth/github/callback, then start otfd with the matching client id and secret (--github-client-id, --github-client-secret, and --hostname <your-domain>). Add the client id and secret to /etc/otf/otfd.env and restart the daemon:
sudo systemctl restart otfd
See the otf documentation at docs.otf.ninja for the full VCS and team/RBAC configuration.
Step 10 - Install a trusted TLS certificate
The image ships a per-VM self signed certificate so TLS works out of the box. For a public deployment, point a DNS name at the VM and install a trusted certificate with certbot, replacing <your-domain>:
sudo apt-get update && sudo apt-get install -y certbot
sudo certbot certonly --standalone -d <your-domain>
sudo sed -i "s#ssl_certificate .*#ssl_certificate /etc/letsencrypt/live/<your-domain>/fullchain.pem;#" /etc/nginx/sites-available/otf
sudo sed -i "s#ssl_certificate_key .*#ssl_certificate_key /etc/letsencrypt/live/<your-domain>/privkey.pem;#" /etc/nginx/sites-available/otf
sudo nginx -t && sudo systemctl reload nginx
Set OTF_HOSTNAME in /etc/otf/otfd.env to your domain and restart otfd so run links and the API advertise the correct host.
Maintenance
otf state, runs and RBAC live in PostgreSQL. Restart or check the services with:
sudo systemctl restart otfd
sudo systemctl status otfd --no-pager
Security updates are applied automatically by unattended-upgrades. Take regular backups of the PostgreSQL otf database (for example with pg_dump) so your workspaces, runs and state can be restored.
Support
This image is maintained by cloudimg with 24/7 support. If you have any questions or run into issues, contact us at support@cloudimg.co.uk.