Deploy Semaphore With Docker
Update - 30th of March 2026
A reader emailed me to let me know that there's currently an issue (listed on GitHub) with using Semaphore with Boltdb, which this simple deployment example uses. If this is indeed the case, then I would recommend readers use the advanced deployment example, which uses PostgreSQL as the backend database rather than Boltdb.
Apparently the bug is where the task templates page would refuse to load and kept spinning/loading. For the reader who notified me of the issue, apparently they switched over to sqlite, which seems to have temporarily fixed it. As I decided to use Rundeck as my alternative to Semaphore, and I'm sure that the bug will get fixed, I'm not going to dig too much into this but wanted to let users know.
Simple Deployment
This version uses a local Bolt database within the container.
services:
semaphore:
restart: unless-stopped
ports:
- 80:3000
image: semaphoreui/semaphore:latest
volumes:
- semaphore-config:/etc/semaphore # config.json location
- semaphore-database:/var/lib/semaphore # database.boltdb location (Not required if using mysql or postgres)
environment:
- SEMAPHORE_DB_DIALECT=bolt
- SEMAPHORE_ADMIN_PASSWORD
- SEMAPHORE_ADMIN_NAME
- SEMAPHORE_ADMIN_EMAIL
- SEMAPHORE_ADMIN
- TZ=Europe/London
volumes:
semaphore-config:
driver: local
semaphore-database:
driver: local
Env File
COMPOSE_PROJECT_NAME="semaphore"
# Set the login details of the initial admin user.
SEMAPHORE_ADMIN_PASSWORD="someStrongRandomPassword"
SEMAPHORE_ADMIN_NAME="My Full Name"
SEMAPHORE_ADMIN_EMAIL="myemail@gmail.com"
SEMAPHORE_ADMIN="myUsername"
Advanced Deployment
Unlike the previous (simple) deployment, this one uses a separate PostgreSQL container to store the backend state, instead of an internal Bolt database.
services:
postgres:
restart: unless-stopped
image: postgres:14
hostname: postgres
volumes:
- semaphore-postgres:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
semaphore:
restart: unless-stopped
ports:
- 80:3000
image: semaphoreui/semaphore:latest
environment:
- SEMAPHORE_DB_USER=${DB_USER}
- SEMAPHORE_DB_PASS=${DB_PASSWORD}
- SEMAPHORE_DB_HOST=postgres
- SEMAPHORE_DB_PORT=5432
- SEMAPHORE_DB_DIALECT=postgres
- SEMAPHORE_DB=${DB_NAME}
- SEMAPHORE_PLAYBOOK_PATH=/tmp/semaphore/
- SEMAPHORE_ADMIN_PASSWORD
- SEMAPHORE_ADMIN_NAME
- SEMAPHORE_ADMIN_EMAIL
- SEMAPHORE_ADMIN
- SEMAPHORE_ACCESS_KEY_ENCRYPTION
- SEMAPHORE_LDAP_ACTIVATED='no'
depends_on:
- postgres
volumes:
semaphore-postgres:
driver: local
Now create a .env file with our settings:
COMPOSE_PROJECT_NAME="semaphore"
# Set the database details. The most important thing is setting a random password.
DB_USER=semaphore
DB_PASSWORD="someRandomPasswordForTheDb"
DB_NAME="semaphore"
# Set the login details of the initial admin user.
SEMAPHORE_ADMIN_PASSWORD="someRandomPassword"
SEMAPHORE_ADMIN_NAME="My Name"
SEMAPHORE_ADMIN_EMAIL="myemail@gmail.com"
SEMAPHORE_ADMIN="myUsername"
# key for encrypting access keys in database. It must be generated by using the following command:
# head -c32 /dev/urandom | base64
SEMAPHORE_ACCESS_KEY_ENCRYPTION=
SEMAPHORE_ACCESS_KEY_ENCRYPTION. You don't need to wrap it with quotes.
TLS Certificates
One needs to use a reverse proxy in order to get a TLS connection in place. You can either do that by setting up and using Nginx Proxy manager, or if you want to do it raw with Nginx, then the docs provides the following example site configuration:
server {
listen 443 ssl;
server_name _;
# add Strict-Transport-Security to prevent man in the middle attacks
add_header Strict-Transport-Security "max-age=31536000" always;
# SSL
ssl_certificate /etc/nginx/cert/cert.pem;
ssl_certificate_key /etc/nginx/cert/privkey.pem;
# Recommendations from
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
# required to avoid HTTP 411: see Issue #1486
# (https://github.com/docker/docker/issues/1486)
chunked_transfer_encoding on;
location / {
proxy_pass http://127.0.0.1/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
}
location /api/ws {
proxy_pass http://127.0.0.1/api/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Origin "";
}
}
/api/ws path requires websocket support.
Conclusion
Thats it, in future tutorials we will demonstrate how to create and run a job, along with how to pass in variables at the last possible second, and make use of the vault.
First published: 3rd April 2024