Moving automation from isolated terminals to a centralized UI#
Managing dozens of VPS servers by opening manual SSH sessions or running ad-hoc scripts eventually creates operational bottlenecks and increases the risk of configuration drift.
I recently set up a local graphical interface to centralize playbook execution, trigger maintenance tasks through interactive forms (Surveys), and maintain visibility over server inventories without losing code-based versioning.
During the setup, I encountered compilation errors with legacy docker-compose on Python 3.12, APT repository codename mismatches, and the heavy resource footprint of AWX. Here is the step-by-step breakdown of the root causes and the lightweight architecture I selected.
1. The declarative model for fleet management#
Managing servers as unique, manually configured instances introduces human error. The recommended declarative stack organizes operations into clear tiers:
- Provisioning with Terraform: Declares compute instances, networks, and firewall rules as code, tracking state in
terraform.tfstate. - Configuration management with Ansible: An agentless framework pushing temporary modules over SSH, with concurrency controlled by the
forksparameter inansible.cfg(e.g.forks = 50). - Secure networking: Removing exposed public SSH ports (
0.0.0.0/0) by routing traffic across private WireGuard mesh overlays (such as Tailscale) or Zero Trust certificate proxies (such as Teleport). - Centralized observability: Shipping system logs (Promtail and Loki) and host metrics (Prometheus with
node_exporter).
To make playbooks operational without constantly editing YAML files from the CLI, a web UI with Survey support (form-driven input fields that inject variables) provides the ideal control plane.
2. Obstacle 1: legacy docker-compose failure on Python 3.12#
Preparing the local environment to run containers, installing Docker Compose via pip3 failed immediately:
sudo pip3 install docker-compose --break-system-packages
Traceback summary:
Collecting docker-compose
Downloading docker_compose-1.29.2-py2.py3-none-any.whl.metadata (4.7 kB)
Collecting PyYAML<6,>=3.10 (from docker-compose)
Downloading PyYAML-5.4.1.tar.gz (175 kB)
...
AttributeError: 'build_ext' object has no attribute 'cython_sources'
error: subprocess-exited-with-error
Root cause:#
- The
docker-composepackage on PyPI is the legacy Python implementation (v1.29.2), which relies onPyYAML 5.4.1. - On Python 3.12 with modern
setuptools, deprecateddistutilsmodules were removed, causing compilation failures in Cython. - Modern Docker Compose (V2) is a standalone native binary written in Go, packaged as a Docker CLI plugin (
docker compose).
3. Obstacle 2: APT repository codename error on derived distributions#
When installing the official plugin package on a derivative Linux distribution (such as Linux Mint):
E: The repository 'https://download.docker.com/linux/ubuntu zara Release' does not have a Release file.
Root cause and fix:#
Docker mirrors only host package trees for official Canonical release codenames (noble, jammy), not derivative codenames like zara. Point the repository to the underlying base release:
# 1. Remove the invalid repository entry
sudo rm /etc/apt/sources.list.d/docker.list
# 2. Add with compatible upstream codename
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
noble stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 3. Update and install the Go plugin
sudo apt-get update
sudo apt-get install docker-compose-plugin
Alternatively, download the static Go binary directly from GitHub releases:
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Verify the installation:
docker compose version
4. The AWX challenge: complexity and memory footprint#
With Docker ready, I evaluated AWX (the upstream project for Ansible Automation Controller). However, AWX has transitioned entirely to the AWX Operator on Kubernetes (K8s/K3s), demanding between 4 GB and 8 GB of RAM simply sitting idle.
For a local administrative workstation, this overhead consumes excessive system resources.
5. The lightweight solution: Ansible Semaphore#
I shifted the setup to Ansible Semaphore, a modern, lightweight web UI written in Go.
| Feature | Ansible AWX | Ansible Semaphore |
|---|---|---|
| Architecture | Kubernetes (AWX Operator) | Single Go binary or Docker Compose |
| Memory Footprint | 4 GB to 8 GB+ RAM | Under 200 MB RAM |
| Survey Support | Yes | Yes (visual form fields injecting extra_vars) |
| Setup Complexity | High | Up and running in 1 minute |
How surveys work in practice#
When defining a Job Template in Semaphore, you can attach an interactive Survey form. When executed, user inputs pass directly to the playbook as high-precedence extra variables (extra_vars):
---
- name: Service Maintenance via Survey
hosts: "{{ vps_environment }}"
become: true
tasks:
- name: Ensure selected service state
ansible.builtin.service:
name: "{{ target_service }}"
state: restarted
Running Semaphore with Docker compose#
The docker-compose.yml specifies Semaphore and persistent MySQL storage:
services:
mysql:
image: mysql:8
container_name: semaphore-mysql
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=db_root_secure_pass
- MYSQL_DATABASE=semaphore
- MYSQL_USER=semaphore_user
- MYSQL_PASSWORD=semaphore_db_pass
volumes:
- semaphore_db_data:/var/lib/mysql
semaphore:
image: semaphoreui/semaphore:latest
container_name: semaphore-ui
restart: unless-stopped
ports:
- "3000:3000"
environment:
- SEMAPHORE_DB_USER=semaphore_user
- SEMAPHORE_DB_PASS=semaphore_db_pass
- SEMAPHORE_DB_HOST=mysql
- SEMAPHORE_DB_PORT=3306
- SEMAPHORE_DB_DIALECT=mysql
- SEMAPHORE_DB=semaphore
- SEMAPHORE_PLAYBOOK_PATH=/tmp/semaphore
- SEMAPHORE_ADMIN_PASSWORD=admin_secure_pass
- SEMAPHORE_ADMIN_NAME=Admin
- SEMAPHORE_ADMIN_EMAIL=user@email.com
- SEMAPHORE_ADMIN_USERNAME=admin
depends_on:
- mysql
volumes:
semaphore_db_data:
Launch the stack:
docker compose up -d
Open the dashboard at http://localhost:3000 to connect Git playbook repositories, import SSH credentials, and launch interactive job templates.
Practical lessons and stack selection#
- Avoid pip for Docker Compose: On Python 3.12, install the official Go plugin (
docker-compose-plugin) via APT or standalone GitHub binary. - Correct derivative distro codenames: If APT reports a release 404, substitute the internal codename with Canonical's base codename in the repository configuration.
- Match tools to practical scope: AWX serves large enterprises requiring Kubernetes-level governance, while Ansible Semaphore delivers fast, low-overhead UI orchestration for multi-VPS fleets.
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments