From batch SSH to UI automation: managing multi-vps fleets with Ansible Semaphore
Back to blog

From batch SSH to UI automation: managing multi-vps fleets with Ansible Semaphore

9/1/2026 · 3 min · DevOps

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:

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:#

  1. The docker-compose package on PyPI is the legacy Python implementation (v1.29.2), which relies on PyYAML 5.4.1.
  2. On Python 3.12 with modern setuptools, deprecated distutils modules were removed, causing compilation failures in Cython.
  3. 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.

FeatureAnsible AWXAnsible Semaphore
ArchitectureKubernetes (AWX Operator)Single Go binary or Docker Compose
Memory Footprint4 GB to 8 GB+ RAMUnder 200 MB RAM
Survey SupportYesYes (visual form fields injecting extra_vars)
Setup ComplexityHighUp 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#

  1. Avoid pip for Docker Compose: On Python 3.12, install the official Go plugin (docker-compose-plugin) via APT or standalone GitHub binary.
  2. Correct derivative distro codenames: If APT reports a release 404, substitute the internal codename with Canonical's base codename in the repository configuration.
  3. 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:

CC BY-NC

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments