Current Docker Image: passbolt/passbolt:5.9.0-1-non-root | Passbolt Version: 5.9.0
This guide provides a Ansible playbook to deploy Passbolt with Docker Compose on Debian 11+, Ubuntu 20.04/22.04/24.04, and RHEL 9+/AlmaLinux 9+ hosts. The playbook includes production hardening, backup configuration, and health checks.
| Distribution | Version | Status |
|---|---|---|
| Debian | 11 (Bullseye), 12 (Bookworm) | ✅ Supported |
| Ubuntu | 20.04 LTS, 22.04 LTS, 24.04 LTS | ✅ Supported |
| RHEL | 9.x | ✅ Supported |
| AlmaLinux | 9.x | ✅ Supported |
| Rocky Linux | 9.x | ✅ Supported |
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 2GB | 4-8GB |
| Storage | 20GB | 50GB+ SSD |
Create your inventory file (inventory.ini):
[passbolt_servers]
passbolt01.your-domain.com ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519
[passbolt_servers:vars]
ansible_python_interpreter=/usr/bin/python3
[all:vars]
# Passbolt configuration
passbolt_domain=passbolt.your-domain.com
passbolt_admin_email=admin@your-domain.com
passbolt_admin_firstname=Admin
passbolt_admin_lastname=User
# SMTP configuration
smtp_host=smtp.your-domain.com
smtp_port=587
smtp_username=passbolt@your-domain.com
smtp_password=your-smtp-password
smtp_from_name="Passbolt"
smtp_from=passbolt@your-domain.com
smtp_tls=true
# Database configuration
db_root_password=strong-root-password-here
db_password=strong-db-password-here
security_salt=random-salt-string-min-32-chars
# SSL/TLS configuration
ssl_enabled=true
ssl_email=letsencrypt@your-domain.com
# Backup configuration
backup_enabled=true
backup_retention_days=30
backup_directory=/backup/passbolt
Save as passbolt-deploy.yml:
---
- name: Deploy Passbolt with Docker Compose
hosts: passbolt_servers
become: true
gather_facts: true
vars:
# Application settings
app_name: passbolt
app_root: /opt/passbolt
app_port: 8080
passbolt_version: "ce-4.2.0" # Pin specific version
# Docker settings
docker_packages_debian:
- docker.io
- docker-compose-plugin
- git
docker_packages_redhat:
- docker
- docker-compose-plugin
- git
# Firewall settings
firewall_enabled: true
firewall_allowed_tcp_ports:
- 22
- 443
tasks:
# =========================================
# System Preparation
# =========================================
- name: Update system packages
ansible.builtin.apt:
update_cache: true
upgrade: dist
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Update system packages (RedHat)
ansible.builtin.dnf:
update_cache: true
upgrade: true
when: ansible_os_family == "RedHat"
- name: Install required packages
ansible.builtin.package:
name:
- git
- curl
- gnupg
- apt-transport-https
- ca-certificates
- software-properties-common
state: present
when: ansible_os_family == "Debian"
- name: Install required packages (RedHat)
ansible.builtin.dnf:
name:
- git
- curl
- gnupg2
- epel-release
state: present
when: ansible_os_family == "RedHat"
# =========================================
# Docker Installation
# =========================================
- name: Install Docker (Debian/Ubuntu)
ansible.builtin.apt:
name: "{{ docker_packages_debian }}"
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install Docker (RedHat family)
ansible.builtin.dnf:
name: "{{ docker_packages_redhat }}"
state: present
when: ansible_os_family == "RedHat"
- name: Enable and start Docker service
ansible.builtin.systemd:
name: docker
state: started
enabled: true
daemon_reload: true
- name: Create Docker configuration directory
ansible.builtin.file:
path: /etc/docker
state: directory
mode: '0755'
- name: Configure Docker daemon
ansible.builtin.copy:
dest: /etc/docker/daemon.json
mode: '0644'
content: |
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"features": {
"buildkit": true
}
}
notify: Restart Docker
# =========================================
# Application Directory Setup
# =========================================
- name: Create application directory
ansible.builtin.file:
path: "{{ app_root }}"
state: directory
mode: '0755'
owner: root
group: root
- name: Create backup directory
ansible.builtin.file:
path: "{{ backup_directory | default('/backup/passbolt') }}"
state: directory
mode: '0750'
owner: root
group: root
when: backup_enabled | default(true)
- name: Create certificates directory
ansible.builtin.file:
path: "{{ app_root }}/certs"
state: directory
mode: '0755'
# =========================================
# Docker Compose File
# =========================================
- name: Download Docker Compose file
ansible.builtin.get_url:
url: https://download.passbolt.com/ce/docker/docker-compose-ce.yaml
dest: "{{ app_root }}/docker-compose.yml"
mode: '0644'
force: false
- name: Download checksum file
ansible.builtin.get_url:
url: https://github.com/passbolt/passbolt_docker/releases/latest/download/docker-compose-ce-SHA512SUM.txt
dest: "{{ app_root }}/docker-compose-SHA512SUM.txt"
mode: '0644'
- name: Verify Docker Compose file integrity
ansible.builtin.shell: |
cd {{ app_root }} && sha512sum -c docker-compose-SHA512SUM.txt
register: checksum_result
changed_when: false
failed_when: checksum_result.rc != 0
- name: Create production Docker Compose configuration
ansible.builtin.copy:
dest: "{{ app_root }}/docker-compose-prod.yml"
mode: '0644'
content: |
services:
passbolt:
image: passbolt/passbolt:{{ passbolt_version }}
container_name: passbolt-app
restart: unless-stopped
depends_on:
mariadb:
condition: service_healthy
ports:
- "127.0.0.1:{{ app_port }}:8080"
environment:
APP_FULL_BASE_URL: https://{{ passbolt_domain }}
PASSBOLT_REGISTRATION_PUBLIC_URL: https://{{ passbolt_domain }}
DATASOURCES_DEFAULT_HOST: mariadb
DATASOURCES_DEFAULT_PORT: 3306
DATASOURCES_DEFAULT_USERNAME: passbolt
DATASOURCES_DEFAULT_PASSWORD: {{ db_password }}
DATASOURCES_DEFAULT_DATABASE: passbolt
DATASOURCES_DEFAULT_ENCODING: utf8
EMAIL_DEFAULT_FROM_NAME: "{{ smtp_from_name }}"
EMAIL_DEFAULT_FROM: {{ smtp_from }}
EMAIL_TRANSPORT_DEFAULT_HOST: {{ smtp_host }}
EMAIL_TRANSPORT_DEFAULT_PORT: {{ smtp_port }}
EMAIL_TRANSPORT_DEFAULT_USERNAME: {{ smtp_username }}
EMAIL_TRANSPORT_DEFAULT_PASSWORD: {{ smtp_password }}
EMAIL_TRANSPORT_DEFAULT_TLS: {{ smtp_tls | lower }}
PASSBOLT_SECURITY_SALT: {{ security_salt }}
PASSBOLT_SSL_FORCE: true
PASSBOLT_GPG_SERVER_KEY_EMAIL: {{ smtp_from }}
PASSBOLT_GPG_SERVER_KEY_NAME: "Passbolt Server"
volumes:
- passbolt_gpg:/etc/passbolt/gpg
- passbolt_db:/var/lib/mysql
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health.json"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mariadb:
image: mariadb:10.11
container_name: passbolt-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: {{ db_root_password }}
MYSQL_DATABASE: passbolt
MYSQL_USER: passbolt
MYSQL_PASSWORD: {{ db_password }}
MYSQL_CHARSET: utf8mb4
MYSQL_COLLATION: utf8mb4_unicode_ci
volumes:
- passbolt_db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
passbolt_gpg:
driver: local
passbolt_db:
driver: local
# =========================================
# Environment File
# =========================================
- name: Create environment file
ansible.builtin.copy:
dest: "{{ app_root }}/.env"
mode: '0600'
content: |
# Passbolt Configuration
PASSBOLT_DOMAIN={{ passbolt_domain }}
PASSBOLT_ADMIN_EMAIL={{ passbolt_admin_email }}
PASSBOLT_ADMIN_FIRSTNAME={{ passbolt_admin_firstname }}
PASSBOLT_ADMIN_LASTNAME={{ passbolt_admin_lastname }}
# Database
DB_ROOT_PASSWORD={{ db_root_password }}
DB_PASSWORD={{ db_password }}
# Security
SECURITY_SALT={{ security_salt }}
# SMTP
SMTP_HOST={{ smtp_host }}
SMTP_PORT={{ smtp_port }}
SMTP_USERNAME={{ smtp_username }}
SMTP_PASSWORD={{ smtp_password }}
# =========================================
# Firewall Configuration
# =========================================
- name: Install UFW (Debian/Ubuntu)
ansible.builtin.apt:
name: ufw
state: present
when:
- firewall_enabled
- ansible_os_family == "Debian"
- name: Configure UFW
ansible.builtin.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: "{{ firewall_allowed_tcp_ports }}"
when:
- firewall_enabled
- ansible_os_family == "Debian"
- name: Enable UFW
ansible.builtin.ufw:
state: enabled
policy: deny
when:
- firewall_enabled
- ansible_os_family == "Debian"
- name: Install firewalld (RedHat)
ansible.builtin.dnf:
name: firewalld
state: present
when:
- firewall_enabled
- ansible_os_family == "RedHat"
- name: Configure firewalld
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
state: enabled
loop:
- ssh
- https
when:
- firewall_enabled
- ansible_os_family == "RedHat"
- name: Enable and start firewalld
ansible.builtin.systemd:
name: firewalld
state: started
enabled: true
when:
- firewall_enabled
- ansible_os_family == "RedHat"
# =========================================
# Systemd Service
# =========================================
- name: Create systemd service for Passbolt
ansible.builtin.copy:
dest: /etc/systemd/system/passbolt.service
mode: '0644'
content: |
[Unit]
Description=Passbolt Password Manager
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory={{ app_root }}
ExecStart=/usr/bin/docker compose -f docker-compose-prod.yml up -d
ExecStop=/usr/bin/docker compose -f docker-compose-prod.yml down
ExecReload=/usr/bin/docker compose -f docker-compose-prod.yml restart
[Install]
WantedBy=multi-user.target
- name: Reload systemd daemon
ansible.builtin.systemd:
daemon_reload: true
- name: Enable and start Passbolt service
ansible.builtin.systemd:
name: passbolt
state: started
enabled: true
# =========================================
# Wait for Service
# =========================================
- name: Wait for Passbolt to be ready
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ app_port }}"
delay: 10
timeout: 120
state: started
- name: Run health check
ansible.builtin.shell: |
docker compose -f {{ app_root }}/docker-compose-prod.yml \
exec passbolt su -m -c \
"/usr/share/php/passbolt/bin/cake passbolt healthcheck" \
-s /bin/sh www-data
register: healthcheck_result
changed_when: false
failed_when: false
retries: 3
delay: 10
# =========================================
# Backup Script
# =========================================
- name: Create backup script
ansible.builtin.copy:
dest: "{{ app_root }}/backup-passbolt.sh"
mode: '0755'
content: |
#!/bin/bash
set -e
BACKUP_DIR="{{ backup_directory | default('/backup/passbolt') }}/$(date +%F)"
mkdir -p $BACKUP_DIR
cd {{ app_root }}
# Database backup
docker compose exec -T mariadb mysqldump \
-u root -p{{ db_root_password }} passbolt > $BACKUP_DIR/database.sql
# GPG keys backup
docker compose exec passbolt gpg --export-secret-keys \
--armor {{ smtp_from }} > $BACKUP_DIR/gpg-keys.asc
# Configuration backup
docker compose exec passbolt cat /etc/passbolt/passbolt.php > $BACKUP_DIR/passbolt.php
# Compress backup
tar -czf $BACKUP_DIR.tar.gz -C $BACKUP_DIR .
rm -rf $BACKUP_DIR
# Cleanup old backups
find {{ backup_directory | default('/backup/passbolt') }} \
-name "*.tar.gz" -mtime +{{ backup_retention_days | default(30) }} -delete
echo "Backup completed: $BACKUP_DIR.tar.gz"
# =========================================
# Cron Job for Backups
# =========================================
- name: Create backup cron job
ansible.builtin.cron:
name: "Passbolt daily backup"
minute: "0"
hour: "2"
job: "{{ app_root }}/backup-passbolt.sh >> /var/log/passbolt-backup.log 2>&1"
user: root
when: backup_enabled | default(true)
# =========================================
# Final Output
# =========================================
- name: Display deployment summary
ansible.builtin.debug:
msg: |
=========================================
Passbolt Deployment Complete!
=========================================
URL: https://{{ passbolt_domain }}
Next Steps:
1. Create admin user:
cd {{ app_root }}
docker compose -f docker-compose-prod.yml \
exec passbolt su -m -c \
"/usr/share/php/passbolt/bin/cake passbolt register_user \
-u {{ passbolt_admin_email }} \
-f {{ passbolt_admin_firstname }} \
-l {{ passbolt_admin_lastname }} \
-r admin" -s /bin/sh www-data
2. Configure HTTPS (reverse proxy or Traefik)
3. Review security hardening guide
4. Test backup restoration
=========================================
handlers:
- name: Restart Docker
ansible.builtin.systemd:
name: docker
state: restarted
daemon_reload: true
# Create inventory file
cat > inventory.ini <<EOF
[passbolt_servers]
passbolt01.your-domain.com ansible_user=ubuntu
[passbolt_servers:vars]
ansible_python_interpreter=/usr/bin/python3
EOF
# Test connectivity
ansible all -i inventory.ini -m ping
# Run the deployment
ansible-playbook -i inventory.ini passbolt-deploy.yml
# Run with specific tags
ansible-playbook -i inventory.ini passbolt-deploy.yml --tags "docker,app"
# Dry run (check mode)
ansible-playbook -i inventory.ini passbolt-deploy.yml --check --diff
After deployment completes, create the first admin user:
# SSH to the server
ssh ubuntu@passbolt01.your-domain.com
# Navigate to app directory
cd /opt/passbolt
# Create admin user
docker compose -f docker-compose-prod.yml \
exec passbolt su -m -c "/usr/share/php/passbolt/bin/cake \
passbolt register_user \
-u admin@your-domain.com \
-f Admin \
-l User \
-r admin" -s /bin/sh www-data
For HA setups with external database:
# Add to vars section
external_db_enabled: true
external_db_host: mariadb-cluster.internal
external_db_port: 3306
# Modify Docker Compose task to use external DB
# Add SSL certificate deployment
- name: Deploy SSL certificates
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ app_root }}/certs/{{ item.dest }}"
mode: '0644'
loop:
- { src: 'files/fullchain.pem', dest: 'fullchain.pem' }
- { src: 'files/privkey.pem', dest: 'privkey.pem' }
# Add Prometheus Node Exporter
- name: Install Node Exporter
ansible.builtin.docker_container:
name: node_exporter
image: prom/node-exporter:latest
state: started
restart_policy: unless-stopped
ports:
- "127.0.0.1:9100:9100"
Docker Compose not found:
# Verify installation
ansible all -m shell -a "docker compose version"
Service won’t start:
# Check systemd status
ansible all -m shell -a "systemctl status passbolt"
# View logs
ansible all -m shell -a "journalctl -u passbolt -n 50"
Health check fails:
# Run manual health check
ansible all -m shell -a "docker compose -f /opt/passbolt/docker-compose-prod.yml exec passbolt su -m -c '/usr/share/php/passbolt/bin/cake passbolt healthcheck' -s /bin/sh www-data"
Vault Integration: Use Ansible Vault for sensitive variables:
ansible-vault encrypt inventory.ini
ansible-playbook -i inventory.ini passbolt-deploy.yml --ask-vault-pass
SSH Hardening: Configure SSH key-based authentication and disable password auth.
Firewall: Ensure only required ports are open (22, 443).
Updates: Schedule regular system updates:
- name: Schedule security updates
ansible.builtin.cron:
name: "Security updates"
minute: "0"
hour: "3"
weekday: "0"
job: "apt-get update && apt-get upgrade -y"
Any questions?
Feel free to contact us. Find all contact information on our contact page.