Deployment guide

Semaphore UI

Overview

Semaphore UI is the open source web interface and REST API for running automation. It turns Ansible playbooks, OpenTofu and Terraform plans, Terragrunt, PowerShell and shell scripts into named templates that your team can run from a browser.

Semaphore UI supports scheduled and event-triggered runs, live streaming output, and a full task history that shows who ran what and when. It also provides role-based access control across projects, plus an API and webhook surface for wiring runs into an existing pipeline.

The SecureAnyCloud image delivers Semaphore UI 2.19.12 on Ubuntu 24.04, served over HTTPS by Semaphore itself. ansible-core 2.21.4 and OpenTofu 1.12.6 are installed alongside it, the datastore is empty, and every secret is generated on the first boot of your VM. The image is backed by 24/7 SecureAnyCloud support.

What you get:

  • Semaphore UI 2.19.12 at /usr/bin/semaphore, run by systemd as the unprivileged semaphore user. It is built from the project's community release artifact, so the image contains only MIT-licensed code.
  • The web interface and REST API served over HTTPS on port 443 with a certificate generated for your VM. Port 80 only redirects browsers to HTTPS and refuses anything that could carry a password.
  • An embedded SQLite datastore at /var/lib/semaphore/semaphore.db, so there is no separate database server to run, patch or back up.
  • ansible-core 2.21.4 in an isolated virtual environment at /opt/semaphore-ansible, on Ubuntu's own Python 3.12, and OpenTofu 1.12.6 at /usr/local/bin/tofu. Semaphore registers both automatically.
  • No default login. The administrator account, the session cookie keys, the TLS certificate and the key that encrypts your stored secrets are all created on your VM's first boot, before the web interface is allowed to start.
  • Self-service registration closed, including the runner registration endpoint, and project creation restricted to administrators.
  • A semaphore systemd service, enabled and active.

Semaphore UI, Ansible, OpenTofu and Terraform are trademarks of their respective owners. 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. This image packages the unmodified open source software, which is distributed under the MIT License.

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region.

Standard_B2s (2 vCPU, 4 GiB RAM) runs Semaphore and its engines comfortably for a small team. Choose a larger size if you run many tasks in parallel or against large inventories.

Configure these network security group inbound rules: 22/tcp from your management network, and 443/tcp for the web interface from your own addresses only. Step 11 explains why this matters more for this product than for most.

1

Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Semaphore UI by SecureAnyCloud, 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 select Review + create and Create.

When the VM exists, narrow those two rules to your own source addresses (Step 11).

2

Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:semaphore-ui-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name my-semaphore \
  --image sac1647283583153:semaphore-ui-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_rsa.pub \
  --public-ip-sku Standard \
  --os-disk-delete-option Delete \
  --nic-delete-option Delete

Then open the web port to your own address range:

az vm open-port --resource-group my-resource-group --name my-semaphore --port 443 --priority 1001
3

Connect to your VM

ssh azureuser@<vm-ip>

The login user is azureuser, or whichever administrator name you chose at deployment. The image carries no build-time account and no password login.

4

Confirm Semaphore is running

First boot takes under a minute. It generates this VM's TLS certificate, both session cookie keys, the key that encrypts your stored secrets, and the administrator password. It then creates an empty datastore, and only then allows the web interface to start. Until first boot finishes, the interface is deliberately not reachable.

sudo systemctl is-active semaphore
active

Check the product and its engines:

semaphore version && ansible --version | head -1 && tofu version | head -1
2.19.12-012ed06-1788086239 ansible [core 2.21.4] OpenTofu v1.12.6

Semaphore discovers its engines on PATH when it starts, so these are exactly the binaries your tasks will run.

5

Check what the network can reach

sudo /usr/local/sbin/semaphore-port-check.sh
off-box TCP exactly [22 80 443] SEMAPHORE_PORTS_OK

This is the complete off-box surface of this image: SSH, the HTTPS front door, and the HTTP redirect. Semaphore terminates TLS itself, so there is no reverse proxy in front of it and no second certificate to rotate. The datastore is a file on disk, so no database server listens at all.

sudo ss -Hltn | awk '{print $4}' | sort -u
*:443 *:80 0.0.0.0:22 127.0.0.53%lo:53 127.0.0.54:53 [::]:22
6

Retrieve the first-boot credentials

sudo stat -c '%A %U:%G %n' /root/semaphore-credentials.txt /etc/semaphore/config.json
-rw------- root:root /root/semaphore-credentials.txt -rw-r----- root:semaphore /etc/semaphore/config.json
sudo cat /root/semaphore-credentials.txt

The file records this VM's web address, the administrator login and the password generated for it. None of these values exists in the image. Every value is created on this VM's first boot, before the web interface serves a single request.

The configuration file alongside it holds three more per-VM secrets. The most important is access_key_encryption, which encrypts every SSH key and secret you later store in Semaphore:

sudo python3 -c "import json; d=json.load(open('/etc/semaphore/config.json')); print('\n'.join('%-26s %s' % (k, ('set, %d chars' % len(d[k])) if k in d else 'absent') for k in ('access_key_encryption','cookie_hash','cookie_encryption','runner_registration_token')))"
access_key_encryption set, 44 chars cookie_hash set, 44 chars cookie_encryption set, 44 chars runner_registration_token absent

The last line is deliberate. POST /api/internal/runners is unauthenticated by design, and a configured global registration token would let anyone who can reach this server register a runner that executes tasks. Leaving the token unset closes that door. Step 7 proves it is closed.

7

Confirm the security posture

Before you sign in, confirm from the command line that nobody else can:

sudo /usr/local/sbin/semaphore-verify-login.sh /root/semaphore-credentials.txt
front door: this VM's credential accepted (204), cookie Secure+HttpOnly+SameSite, session usable refused: admin/admin and 6 other weak defaults, blank password, anonymous API, anonymous user creation self-registration: runner registration refused for every token; no global token configured transport: plaintext not served on 443; :80 redirects GET (307) and refuses POST (403) SEMAPHORE_LOGIN_OK

Each check is a live request against your own running server, not a claim about the image. You can reproduce any one of them by hand, for example with the credential that every Semaphore tutorial uses:

curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
  -H 'Content-Type: application/json' \
  --data '{"auth":"admin","password":"admin"}' \
  https://127.0.0.1/api/auth/login
401
8

Sign in

Open https://<vm-ip>/ in a browser. The certificate is generated for your VM and is self-signed, so the browser warns on the first visit. Step 12 shows how to install a certificate for a real domain name.

Sign in as admin with the password from Step 6. A fresh instance has no projects yet. Create one from the project selector at the top left, then add a repository, an inventory and a task template.

9

Run your first task

A task template ties together four things: a repository holding your playbooks, an inventory describing the hosts, a variable group for environment values, and the playbook to run. Create them from the left-hand navigation in that order. Then choose Task Templates, then New Template.

Press the run button on the template. The task opens a live log. Semaphore clones the repository, installs any Galaxy requirements it finds, and streams the engine's real output as it runs.

The same function is available from the API. This command signs in with your own credentials and lists the apps that Semaphore has registered:

PASS=$(sudo grep '^SEMAPHORE_ADMIN_PASSWORD=' /root/semaphore-credentials.txt | cut -d= -f2-)
JAR=$(mktemp) printf '{"auth":"admin","password":"%s"}' "$PASS" | curl -sk -o /dev/null -c "$JAR" \
  -X POST -H 'Content-Type: application/json' \
  https://127.0.0.1/api/auth/login --data-binary @- curl -sk -b "$JAR" https://127.0.0.1/api/apps \
  | python3 -c "import json,sys; d=json.load(sys.stdin); rows=d if isinstance(d,list) else list(d.values()); print('\n'.join(sorted(r.get('id','') for r in rows if isinstance(r,dict))))" rm -f "$JAR"
ansible bash tofu
10

The automation engines

Semaphore runs the same binaries that you can run yourself, so anything you can debug from the shell behaves identically inside a task.

cd /tmp && ansible localhost -m ansible.builtin.ping
localhost | SUCCESS => { "changed": false, "ping": "pong" }

ansible-core is installed from a version-pinned wheel into /opt/semaphore-ansible. It is isolated from the system Python, so it cannot collide with anything the operating system manages. To move to a newer release, run:

sudo /opt/semaphore-ansible/bin/pip install --upgrade 'ansible-core==2.21.*'
sudo systemctl restart semaphore

Collections and roles install into the same environment:

sudo /opt/semaphore-ansible/bin/ansible-galaxy collection install community.general
11

Restrict network access

This step matters more for Semaphore than for most products. Semaphore exists to execute automation, and it holds the SSH keys and secrets that reach your estate. Anyone who can both reach it and authenticate to it can run code on the hosts it manages. Treat the instance as infrastructure, not as a public web application.

Limit both inbound rules to the addresses your team actually uses:

az network nsg rule update \
  --resource-group my-resource-group \
  --nsg-name my-semaphoreNSG \
  --name allow-https \
  --source-address-prefixes 203.0.113.0/24

If your team does not have stable source addresses, put the instance on a private subnet. Reach it over a VPN, Azure Bastion or a peered network instead of a public IP. The image does not open any port by itself. Everything reachable is something that the network security group allows.

12

Use your own domain and certificate

Semaphore reads its certificate from /etc/ssl/semaphore/. Point a DNS record at your VM and obtain a certificate however you normally do. Then install the certificate and tell Semaphore its public URL:

sudo install -o root -g semaphore -m 0644 /path/to/fullchain.pem /etc/ssl/semaphore/semaphore.crt
sudo install -o root -g semaphore -m 0640 /path/to/privkey.pem /etc/ssl/semaphore/semaphore.key
sudo python3 - <<'PY' import json p = "/etc/semaphore/config.json" c = json.load(open(p)) c["web_host"] = "https://semaphore.example.com" json.dump(c, open(p, "w"), indent=2) PY
sudo systemctl restart semaphore

The web_host value is essential. Semaphore only marks its session cookie Secure when that value is an https URL, and the web interface uses it as the base for every asset it serves. Keep the scheme as https.

13

Change the administrator password

Change the password from the web interface. Select the avatar at the bottom left, then Edit account. You can also change it from the command line with semaphore. Choose a strong value. This block refuses an empty or trivially weak password rather than accepting it:

NEW='ReplaceThisWithYourOwnStrongValue' if [ ${#NEW} -lt 16 ] || [ "$NEW" = "admin" ] || [ "$NEW" = "password" ]; then echo "REJECTED: choose a password of at least 16 characters" else echo "would run: sudo semaphore user change-by-login --login admin --password ... --config /etc/semaphore/config.json" fi
would run:
sudo semaphore user change-by-login --login admin --password ... --config /etc/semaphore/config.json

After you change the password, update /root/semaphore-credentials.txt so the file and the live password agree.

14

Back up your instance

Everything that matters lives in two places: the datastore, and the configuration file that holds the key that decrypts it.

sudo install -d -m 0700 /root/semaphore-backup
sudo sqlite3 /var/lib/semaphore/semaphore.db ".backup '/root/semaphore-backup/semaphore.db'"
sudo cp -a /etc/semaphore/config.json /root/semaphore-backup/config.json
sudo ls -l /root/semaphore-backup

.backup takes a consistent copy of a live database, so you do not have to stop the service.

Keep both files together. The SSH keys and secrets in the datastore are encrypted with access_key_encryption from the configuration file. A datastore restored without that key cannot be decrypted, and the configuration file without the datastore is useless. Store both files somewhere at least as protected as the credentials they unlock.

15

Keep the image patched

Ubuntu security updates are applied automatically by unattended-upgrades, which is enabled in this image. Check its status:

systemctl is-enabled unattended-upgrades
enabled

Semaphore itself is a single binary. To move to a newer release, download the community artifact for the version you want. Verify it against the checksums file published with that release, then install it:

echo "sudo apt-get install -y ./semaphore_community_<version>_linux_amd64.deb && sudo systemctl restart semaphore"
sudo apt-get install -y ./semaphore_community_<version>_linux_amd64.deb && sudo systemctl restart semaphore

Take the artifact whose name contains community. Every Semaphore release also publishes a larger build that links the project's separately licensed Pro module. This image ships the community build deliberately, so that it contains only MIT-licensed code.

Troubleshooting

The web interface does not answer. First boot has not finished, or it failed. Check both units:

sudo systemctl is-active semaphore-firstboot semaphore
active active

If semaphore-firstboot is not active, read its log:

sudo journalctl -u semaphore-firstboot --no-pager | tail -20

The server will not start until first boot has written /var/lib/sac/data/semaphore.bootstrap-ready. This is deliberate: it stops the instance from ever serving without an administrator and an encryption key.

A task fails immediately with a repository problem. Semaphore clones with git as the semaphore user. For a private repository, add an SSH key in Key Store and select it on the repository. For a host it has not seen before, accept the host key for that user:

sudo -u semaphore ssh-keyscan -H github.com 2>/dev/null | sudo -u semaphore tee -a /var/lib/semaphore/.ssh/known_hosts >/dev/null echo done
done

A task fails to reach your hosts. Semaphore runs Ansible as the semaphore user. The key it uses is the one you attached to the inventory, not any key in your own home directory.

Check everything at once. The image ships a self-test that re-runs every security assertion against the running instance. It checks the listening set, the login gates, the transport posture, the engine versions, and that the certificate really names your VM:

sudo /usr/local/sbin/semaphore-selftest.sh
SEMAPHORE_SELFTEST_OK

Adding --customer-vm also asserts the image-level facts: no build account survives, first boot completed, and the image carries no swap. This is what SecureAnyCloud's own release gate runs against a freshly launched instance.

Support

SecureAnyCloud supports this image 24/7. Contact us through the SecureAnyCloud website for help with deployment, TLS certificates and custom domains, connecting inventories and source repositories, runner and pipeline design, upgrades and scaling.

Deploy on Azure

Launch Semaphore UI 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