A thin derived Docker image on top of the official
semaphoreui/semaphore
(Ansible Semaphore UI, built on Alpine). It adds Ansible fact/inventory
caching backed by Redis, a few controller-side Ansible runtime packages,
and predefined environment defaults that pair with the cache.
Localized README: Русский
The image layers three concerns on top of the base:
-
Ansible runtime packages, installed via
apkusing the Alpinepy3-*naming convention. All are controller-side plugins:py3-redis— backs thecommunity.general.rediscache plugin.py3-jmespath— used by thejson_queryfilter and several Ansible lookups (kept explicit even though the base currently ships it).py3-netaddr— enables theipaddr/ipsubnet/ipmathfilters.py3-dnspython— backs thecommunity.general.diglookup.py3-paramiko— backs theparamiko_ssh/network_cliconnection plugins.
-
Predefined Ansible cache environment variables that configure
community.general.redisas the fact/inventory cache backend:Variable Default Purpose ANSIBLE_INVENTORY_CACHETrueEnable inventory caching. ANSIBLE_CACHE_PLUGINcommunity.general.redisFact cache plugin. ANSIBLE_INVENTORY_CACHE_PLUGINcommunity.general.redisInventory cache plugin. ANSIBLE_CACHE_PLUGIN_CONNECTIONlocalhost:6379:0Redis connection ( host:port:db).Override
ANSIBLE_CACHE_PLUGIN_CONNECTIONat runtime to point at the actual Redis host. -
Behavioral environment defaults that pair with the Redis cache:
Variable Default Purpose ANSIBLE_GATHERINGsmartReuse cached facts, don't re-gather. ANSIBLE_RETRY_FILES_ENABLEDFalseDon't write .retryfiles.Only universally-safe, controller-side defaults are baked. Fleet- or host-dependent knobs (
ANSIBLE_FORKS,ANSIBLE_PIPELINING,ANSIBLE_CACHE_PLUGIN_TIMEOUT) are left to the deployment env —ANSIBLE_PIPELININGin particular breaks on targets withrequirettyin sudoers.
docker build -t semaphore-custom .Confirm the added packages import from Ansible's own venv interpreter
(not just the system python3):
make smokePoint the cache at a real Redis instance:
docker run -d --name semaphore \
-p 3000:3000 \
-e ANSIBLE_CACHE_PLUGIN_CONNECTION=redis-host:6379:0 \
semaphore-customRefer to the official Semaphore documentation for the base image's own configuration (database, admin user, etc.).
A full example stack — Semaphore plus the Redis cache backend, a PostgreSQL
database, and persistent volumes — is provided in
docker-compose.yml. Fill in the CHANGE-ME secrets,
then:
docker compose up -dThe base tag defaults to latest but can be overridden at build time
without editing the Dockerfile:
docker build --build-arg BASE_TAG=v2.18.12 -t semaphore-custom .The image declares no VOLUMEs — all state lives in the container's
writable layer and is lost when the container is removed. Mount these paths
explicitly to persist data:
/etc/semaphore(SEMAPHORE_CONFIG_PATH) — required. Holdsconfig.json, including the access-key encryption secret. Lose it and the credentials stored in the database can no longer be decrypted./var/lib/semaphore(SEMAPHORE_DB_PATH) — required for the embedded database (database.boltdb/database.sqlite). Not needed with an external MySQL/Postgres./tmp/semaphore(SEMAPHORE_TMP_PATH) — cloned repositories and task working directories/output. Optional cache: safe to lose (repos re-clone), worth a volume to avoid re-cloning large repositories.
Self-contained run with an embedded SQLite database and persisted volumes:
docker run -d --name semaphore -p 3000:3000 \
-e SEMAPHORE_DB_DIALECT=sqlite \
-e SEMAPHORE_ADMIN=admin -e SEMAPHORE_ADMIN_PASSWORD=changeme \
-e SEMAPHORE_ADMIN_NAME=Admin -e SEMAPHORE_ADMIN_EMAIL=admin@localhost \
-e SEMAPHORE_ACCESS_KEY_ENCRYPTION="$(head -c32 /dev/urandom | base64)" \
-v semaphore-config:/etc/semaphore \
-v semaphore-db:/var/lib/semaphore \
-v semaphore-tmp:/tmp/semaphore \
semaphore-customThese variables belong to the upstream base image, not this layer; the
bundled server-wrapper reads them on startup. The most common ones:
| Variable | Purpose |
|---|---|
SEMAPHORE_PORT |
Web UI port (default 3000). |
SEMAPHORE_INTERFACE |
Bind to a specific network interface. |
SEMAPHORE_WEB_ROOT |
Base path when served under a subpath. |
SEMAPHORE_TLS_ENABLED |
Serve HTTPS (with cert/key file vars). |
| Variable | Purpose |
|---|---|
SEMAPHORE_DB_DIALECT |
sqlite, bolt, mysql, or postgres. |
SEMAPHORE_DB_HOST |
DB host, or file path for sqlite/bolt. |
SEMAPHORE_DB_NAME |
Schema name (MySQL/Postgres). |
SEMAPHORE_DB_USER / SEMAPHORE_DB_PASS |
DB credentials. |
| Variable | Purpose |
|---|---|
SEMAPHORE_ADMIN / …_PASSWORD |
Initial admin account. |
SEMAPHORE_ADMIN_NAME / …_EMAIL |
Initial admin profile. |
SEMAPHORE_ACCESS_KEY_ENCRYPTION |
Encrypts stored access keys. |
SEMAPHORE_COOKIE_HASH / …_ENCRYPTION |
Session cookie secrets. |
Notifications (Telegram, Slack, Teams, …), remote runners, LDAP/OIDC/TOTP
auth, high availability and structured logging are all configurable via
further SEMAPHORE_* variables — see the official environment-variable
reference.
A few behaviours of the bundled wrapper worth knowing:
- Dialect default is
mysqlin the wrapper (the binary's own default issqlite). For a self-contained image setSEMAPHORE_DB_DIALECT=sqliteorbolt, otherwise startup waits ~30s for a MySQL ping and then exits. config.jsonis generated on first run only. Later changes to DB/admin env vars do not regenerate it — edit the file or start from an empty config volume to apply them.*_FILEsecrets.SEMAPHORE_DB_USER,SEMAPHORE_DB_PASS,SEMAPHORE_ADMIN,SEMAPHORE_ADMIN_PASSWORD,SEMAPHORE_LDAP_PASSWORDandSEMAPHORE_ACCESS_KEY_ENCRYPTIONalso accept a…_FILEvariant pointing at a file (Docker/Compose secrets).- Extra Python deps without rebuilding. A
requirements.txtinSEMAPHORE_CONFIG_PATHispip installed on startup; since Ansible's venv uses--system-site-packages, packages likedockerorhvacbecome importable by playbooks this way.
- The base tag is pinned to
:latestdeliberately. Changing the default base image tag is a meaningful decision, not a routine bump. - All package installs live in a single
RUNlayer with--no-cacheto avoid bloating the image with the apk cache. - The install step drops to
USER rootto install packages, then restoresUSER semaphoreso the container does not run as root. - The build runs a layered verification (mirrored in
make smoke): the added packages import through Ansible's own venv interpreter, the relied-on plugins load viaansible-doc, the baked env defaults applied, and the Semaphore base contract holds — so base drift fails the build instead of breaking silently at runtime. - A
HEALTHCHECKprobes the Semaphore web port (:3000). It is a liveness check: any HTTP response counts as healthy, a refused connection or timeout as unhealthy — so it survives Semaphore API changes across the rolling:latestbase.
Upstream Ansible Semaphore project (this image only extends it):
- Website
- Documentation
- Docker install guide
- Source code
- Discussions
- Issue tracker
- Docker Hub
- Online demo
See CHANGELOG.md.