Semaphore UI

Semaphore UI is a web-based interface for running and scheduling Ansible playbooks, Bash and Python scripts.

This compose file deploys Semaphore UI with a PostgreSQL database. All data is stored on NFS volumes, and the web interface is proxied through a Traefik reverse proxy with Cloudflare TLS certificates.

Docker Compose

# compose.yaml

services:
  semaphore:
    image: semaphoreui/semaphore:latest
    container_name: semaphore
    restart: unless-stopped
    depends_on:
      semaphore-db:
        condition: service_healthy
    # ports:
    #  - 3000:3000
    networks:
      - semaphore
      - semaphore_proxy
    volumes:
      - type: volume
        source: docker-nfs
        target: /var/lib/semaphore
        volume:
          subpath: semaphore/data
      - type: volume
        source: docker-nfs
        target: /etc/semaphore
        volume:
          subpath: semaphore/config
    environment:
      SEMAPHORE_DB_USER: ${POSTGRES_USER}
      SEMAPHORE_DB_PASS: ${POSTGRES_PASSWORD}
      SEMAPHORE_DB_HOST: semaphore-db
      SEMAPHORE_DB_PORT: 5432
      SEMAPHORE_DB_DIALECT: postgres
      SEMAPHORE_DB: ${POSTGRES_DB}
      SEMAPHORE_PLAYBOOK_PATH: /tmp/semaphore/
      SEMAPHORE_ADMIN_PASSWORD: ${SEMAPHORE_ADMIN_PASSWORD}
      SEMAPHORE_ADMIN_NAME: ${SEMAPHORE_ADMIN_NAME}
      SEMAPHORE_ADMIN_EMAIL: ${SEMAPHORE_ADMIN_EMAIL}
      SEMAPHORE_ADMIN: ${SEMAPHORE_ADMIN}
      SEMAPHORE_ACCESS_KEY_ENCRYPTION: ${SEMAPHORE_ACCESS_KEY_ENCRYPTION}
      SEMAPHORE_LDAP_ACTIVATED: "no"
      TZ: Europe/London
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=semaphore_proxy"

      - "traefik.http.services.semaphore.loadbalancer.server.port=3000"

      - "traefik.http.routers.semaphore.rule=Host(`semaphore.${TRAEFIK_BASE_URL}`)"
      - "traefik.http.routers.semaphore.entrypoints=websecure"
      - "traefik.http.routers.semaphore.tls=true"
      - "traefik.http.routers.semaphore.tls.certresolver=cloudflare"

  semaphore-db:
    image: postgres:18
    container_name: semaphore-db
    restart: unless-stopped
    networks:
      - semaphore
    volumes:
      - type: volume
        source: docker-nfs
        target: /var/lib/postgresql
        volume:
          subpath: semaphore/postgres
    healthcheck:
      test:
        ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      TZ: Europe/London

volumes:
  docker-nfs:
    driver: local
    driver_opts:
      type: nfs
      o: addr=xxx.xxx.xxx.xxx,nolock,soft,rw,nfsvers=4.2
      device: :/mnt/nfs-volume

networks:
  semaphore:
    name: semaphore
  semaphore_proxy:
    name: semaphore_proxy

Environment Variables

# .env

POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
SEMAPHORE_ADMIN_PASSWORD=
SEMAPHORE_ADMIN_NAME=
SEMAPHORE_ADMIN_EMAIL=
SEMAPHORE_ADMIN=
SEMAPHORE_ACCESS_KEY_ENCRYPTION= # generated with `openssl rand -base64 32`

TRAEFIK_BASE_URL=example.com

Traefik Configuration

# compose.yaml (excerpt)

services:
  traefik:
    image: traefik:latest
    container_name: traefik
    ...
    networks:
      - traefik
      # here
      - semaphore_proxy
    ...

networks:
  # here
  semaphore_proxy:
    name: semaphore_proxy