Programster's Blog

Tutorials focusing on Linux, programming, and open-source

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=

Be sure to change the password values, and read the comment for filling in the value for the 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 "";
  }
}

The most important thing to be aware of is that the /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.

Last updated: 30th March 2026
First published: 3rd April 2024

This blog is created by Stuart Page

I'm a freelance web developer and technology consultant based in Surrey, UK, with over 10 years experience in web development, DevOps, Linux Administration, and IT solutions.

Need support with your infrastructure or web services?

Get in touch