This guide provides a comprehensive, production-ready Ansible playbook for deploying Psono with Docker Compose on Linux servers. Automate your secret management infrastructure with repeatable, version-controlled deployments.
Tested On: Debian 10-12, Ubuntu 20.04-22.04, RHEL 9+, Rocky Linux 9, AlmaLinux 9
Ansible Version: 2.12+ | Docker Version: 20.10+
This Ansible deployment provides:
| Requirement | Version | Installation |
|---|---|---|
| Ansible | 2.12+ | pip install ansible or package manager |
| Python | 3.8+ | Usually pre-installed |
| SSH Access | Key-based | ssh-keygen, ssh-copy-id |
| Requirement | Minimum | Recommended |
|---|---|---|
| OS | Debian 10, Ubuntu 20.04, RHEL 9 | Debian 12, Ubuntu 22.04 |
| CPU | 2 cores | 4+ cores |
| RAM | 2 GB | 4-8 GB |
| Storage | 10 GB SSD | 50+ GB SSD |
| Network | Public IP, domain name | Static IP, DNS configured |
# inventory.ini
[psono_servers]
psono.example.com ansible_host=192.0.2.1 ansible_user=deploy ansible_ssh_private_key_file=~/.ssh/id_rsa
[psono_servers:vars]
ansible_python_interpreter=/usr/bin/python3
ansible-psono/
βββ inventory.ini
βββ psono-deploy.yml
βββ group_vars/
β βββ all.yml
βββ host_vars/
β βββ psono.example.com.yml
βββ roles/
β βββ docker/
β β βββ tasks/
β β βββ main.yml
β βββ psono/
β β βββ tasks/
β β β βββ main.yml
β β βββ templates/
β β β βββ docker-compose.yml.j2
β β β βββ nginx.conf.j2
β β β βββ .env.j2
β β βββ handlers/
β β βββ main.yml
β βββ security/
β βββ tasks/
β βββ main.yml
βββ files/
βββ backup-script.sh
psono-deploy.yml---
# Psono Production Deployment Playbook
# Usage: ansible-playbook -i inventory.ini psono-deploy.yml
- name: Deploy Psono Password Manager
hosts: psono_servers
become: true
gather_facts: true
vars:
# Application settings
psono_version: "15.1"
psono_domain: "{{ psono_server_domain }}"
psono_admin_email: "{{ psono_admin_email }}"
# Directory paths
psono_base_dir: /opt/psono
psono_backup_dir: /opt/psono/backups
psono_log_dir: /opt/psono/logs
# Docker settings
docker_compose_version: "2.21.0"
# Security settings
firewall_enabled: true
fail2ban_enabled: true
# Backup settings
backup_retention_days: 7
backup_schedule_hour: 2
backup_schedule_minute: 0
pre_tasks:
- name: Validate required variables
ansible.builtin.assert:
that:
- psono_server_domain is defined
- psono_admin_email is defined
- psono_secret_key is defined
- psono_db_password is defined
fail_msg: "Required variables are not defined. Check group_vars or host_vars."
- name: Update apt cache (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Update dnf cache (RHEL/Rocky/Alma)
ansible.builtin.dnf:
update_cache: true
when: ansible_os_family == "RedHat"
roles:
- role: docker
tags: ['docker', 'prerequisites']
- role: security
tags: ['security', 'hardening']
- role: psono
tags: ['psono', 'application']
post_tasks:
- name: Verify Psono is running
ansible.builtin.uri:
url: "https://{{ psono_domain }}/health/"
method: GET
validate_certs: false
status_code: 200
register: health_check
retries: 5
delay: 10
until: health_check.status == 200
- name: Display deployment summary
ansible.builtin.debug:
msg: |
============================================
Psono Deployment Complete!
============================================
Domain: https://{{ psono_domain }}
Admin Email: {{ psono_admin_email }}
Backup Directory: {{ psono_backup_dir }}
============================================
Next Steps:
1. Access https://{{ psono_domain }} to create admin account
2. Enable 2FA for all admin users
3. Configure email settings for notifications
4. Review backup configuration
============================================
group_vars/all.yml---
# Global variables for Psono deployment
# Docker configuration
docker_packages:
- docker.io
- docker-compose-plugin
- docker-buildx-plugin
docker_daemon_options:
storage-driver: overlay2
log-driver: json-file
log-opts:
max-size: "10m"
max-file: "3"
# Security configuration
security_ssh_port: 22
security_ssh_password_auth: false
security_ssh_permit_root_login: false
# Firewall configuration
firewall_allowed_tcp_ports:
- 22 # SSH
- 80 # HTTP (redirect to HTTPS)
- 443 # HTTPS
# Monitoring configuration
monitoring_enabled: true
health_check_interval: 30s
host_vars/psono.example.com.yml---
# Host-specific variables
psono_server_domain: "psono.example.com"
psono_admin_email: "admin@example.com"
# Generate these secrets before deployment (DO NOT commit to version control!)
psono_secret_key: "CHANGE_ME_64_CHAR_RANDOM_STRING_USE_VAULT"
psono_db_password: "CHANGE_ME_STRONG_DB_PASSWORD_USE_VAULT"
psono_admin_password: "CHANGE_ME_INITIAL_ADMIN_PASSWORD_USE_VAULT"
# Email configuration (optional)
psono_email_host: "smtp.example.com"
psono_email_port: 587
psono_email_user: "noreply@example.com"
psono_email_password: "CHANGE_ME_SMTP_PASSWORD_USE_VAULT"
psono_from_email: "noreply@example.com"
# Resource limits
psono_cpu_limit: "2.0"
psono_memory_limit: "2G"
psono_cpu_reservation: "0.5"
psono_memory_reservation: "512M"
π Security Note: Use Ansible Vault to encrypt sensitive variables:
ansible-vault encrypt host_vars/psono.example.com.yml ansible-playbook -i inventory.ini psono-deploy.yml --ask-vault-pass
roles/docker/tasks/main.yml---
# Docker installation and configuration
- name: Install Docker on Debian/Ubuntu
ansible.builtin.apt:
name: "{{ docker_packages }}"
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install Docker on RHEL family
ansible.builtin.dnf:
name: "{{ docker_packages }}"
state: present
when: ansible_os_family == "RedHat"
- name: Enable and start Docker service
ansible.builtin.service:
name: docker
state: started
enabled: true
- name: Create Docker configuration directory
ansible.builtin.file:
path: /etc/docker
state: directory
mode: '0755'
- name: Configure Docker daemon
ansible.builtin.copy:
content: |
{
"storage-driver": "{{ docker_daemon_options['storage-driver'] }}",
"log-driver": "{{ docker_daemon_options['log-driver'] }}",
"log-opts": {
"max-size": "{{ docker_daemon_options['log-opts']['max-size'] }}",
"max-file": "{{ docker_daemon_options['log-opts']['max-file'] }}"
}
}
dest: /etc/docker/daemon.json
mode: '0644'
notify: Restart Docker
- name: Add deploy user to docker group
ansible.builtin.user:
name: "{{ ansible_user }}"
groups: docker
append: true
- name: Install Docker Compose plugin (if not available)
ansible.builtin.get_url:
url: "https://github.com/docker/compose/releases/download/v{{ docker_compose_version }}/docker-compose-linux-x86_64"
dest: /usr/local/bin/docker-compose
mode: '0755'
when: docker_compose_version is defined
roles/security/tasks/main.yml---
# Security hardening tasks
- name: Install security packages
ansible.builtin.package:
name:
- ufw
- fail2ban
- unattended-upgrades
state: present
when: ansible_os_family == "Debian"
- name: Configure UFW firewall
ansible.builtin.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: "{{ firewall_allowed_tcp_ports }}"
- name: Enable UFW (if not already enabled)
ansible.builtin.ufw:
state: enabled
policy: deny
when: firewall_enabled | bool
- name: Configure SSH hardening
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
loop:
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?PubkeyAuthentication', line: 'PubkeyAuthentication yes' }
notify: Restart SSH
- name: Enable unattended upgrades
ansible.builtin.debconf:
name: unattended-upgrades
question: unattended-upgrades/enable_auto_updates
value: 'true'
vtype: boolean
when: ansible_os_family == "Debian"
roles/psono/tasks/main.yml---
# Psono application deployment
- name: Create Psono directory structure
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- "{{ psono_base_dir }}"
- "{{ psono_backup_dir }}"
- "{{ psono_log_dir }}"
- "{{ psono_base_dir }}/nginx/ssl"
- name: Generate .env file from template
ansible.builtin.template:
src: .env.j2
dest: "{{ psono_base_dir }}/.env"
mode: '0600'
- name: Deploy Docker Compose configuration
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ psono_base_dir }}/docker-compose.yml"
mode: '0644'
- name: Deploy Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: "{{ psono_base_dir }}/nginx/nginx.conf"
mode: '0644'
- name: Start Psono stack
ansible.builtin.command:
cmd: docker compose up -d
chdir: "{{ psono_base_dir }}"
register: compose_up
changed_when: "'Creating' in compose_up.stdout or 'Starting' in compose_up.stdout"
- name: Install backup script
ansible.builtin.copy:
src: backup-script.sh
dest: /usr/local/bin/psono-backup
mode: '0755'
- name: Configure backup cron job
ansible.builtin.cron:
name: "Psono daily backup"
minute: "{{ backup_schedule_minute }}"
hour: "{{ backup_schedule_hour }}"
job: "/usr/local/bin/psono-backup >> /var/log/psono-backup.log 2>&1"
user: root
roles/psono/templates/docker-compose.yml.j2version: '3.8'
services:
psono-server:
image: psono/psono-server:{{ psono_version }}
container_name: psono-server
restart: unless-stopped
stop_grace_period: 30s
networks:
- psono-internal
expose:
- "8080"
environment:
# Database
- PSONO_DATABASE_ENGINE=django.db.backends.postgresql
- PSONO_DATABASE_NAME=psono
- PSONO_DATABASE_USER=psono
- PSONO_DATABASE_PASSWORD={{ psono_db_password }}
- PSONO_DATABASE_HOST=postgres
- PSONO_DATABASE_PORT=5432
# Security
- PSONO_SECRET_KEY={{ psono_secret_key }}
- PSONO_ALLOWED_HOSTS={{ psono_domain }}
# Email (optional)
{% if psono_email_host is defined %}
- PSONO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
- PSONO_EMAIL_HOST={{ psono_email_host }}
- PSONO_EMAIL_PORT={{ psono_email_port }}
- PSONO_EMAIL_USE_TLS=true
- PSONO_EMAIL_HOST_USER={{ psono_email_user }}
- PSONO_EMAIL_HOST_PASSWORD={{ psono_email_password }}
- PSONO_DEFAULT_FROM_EMAIL={{ psono_from_email }}
{% endif %}
volumes:
- psono-settings:/etc/psono
- psono-static:/var/www/static
- {{ psono_log_dir }}:/var/log/psono
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health/"]
interval: {{ health_check_interval }}
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
cpus: '{{ psono_cpu_limit }}'
memory: {{ psono_memory_limit }}
reservations:
cpus: '{{ psono_cpu_reservation }}'
memory: {{ psono_memory_reservation }}
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:15-alpine
container_name: psono-postgres
restart: unless-stopped
networks:
- psono-internal
environment:
- POSTGRES_DB=psono
- POSTGRES_USER=psono
- POSTGRES_PASSWORD={{ psono_db_password }}
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- postgres-data:/var/lib/postgresql/data
- {{ psono_backup_dir }}/postgres:/backups
healthcheck:
test: ["CMD-SHELL", "pg_isready -U psono"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
redis:
image: redis:7-alpine
container_name: psono-redis
restart: unless-stopped
command: redis-server --appendonly yes
networks:
- psono-internal
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
container_name: psono-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- psono-internal
volumes:
- {{ psono_base_dir }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- {{ psono_base_dir }}/nginx/ssl:/etc/nginx/ssl:ro
- psono-static:/var/www/static:ro
depends_on:
- psono-server
networks:
psono-internal:
driver: bridge
volumes:
psono-settings:
psono-static:
postgres-data:
redis-data:
roles/psono/templates/.env.j2# Psono Environment Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Last updated: {{ ansible_date_time.iso8601 }}
# Application
PSONO_VERSION={{ psono_version }}
PSONO_DOMAIN={{ psono_domain }}
# Database
PSONO_DB_NAME=psono
PSONO_DB_USER=psono
PSONO_DB_PASSWORD={{ psono_db_password }}
# Security
PSONO_SECRET_KEY={{ psono_secret_key }}
PSONO_ALLOWED_HOSTS={{ psono_domain }}
# Email Configuration
{% if psono_email_host is defined %}
PSONO_EMAIL_HOST={{ psono_email_host }}
PSONO_EMAIL_PORT={{ psono_email_port }}
PSONO_EMAIL_USER={{ psono_email_user }}
PSONO_EMAIL_PASSWORD={{ psono_email_password }}
PSONO_FROM_EMAIL={{ psono_from_email }}
{% endif %}
# Resource Limits
PSONO_CPU_LIMIT={{ psono_cpu_limit }}
PSONO_MEMORY_LIMIT={{ psono_memory_limit }}
files/backup-script.sh#!/bin/bash
# Psono Backup Script
# Deployed by Ansible
set -e
BACKUP_DIR="/opt/psono/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
echo "[$(date)] Starting Psono backup: ${DATE}"
# Create backup directories
mkdir -p ${BACKUP_DIR}/{postgres,settings,static}
# Backup PostgreSQL database
echo "[$(date)] Backing up PostgreSQL..."
docker exec psono-postgres pg_dump -U psono psono | gzip > ${BACKUP_DIR}/postgres/db_${DATE}.sql.gz
# Backup settings volume
echo "[$(date)] Backing up settings..."
docker run --rm \
-v psono_settings:/source:ro \
-v ${BACKUP_DIR}/settings:${BACKUP_DIR}/settings \
alpine tar czf ${BACKUP_DIR}/settings/settings_${DATE}.tar.gz -C /source .
# Backup static files
echo "[$(date)] Backing up static files..."
docker run --rm \
-v psono_static:/source:ro \
-v ${BACKUP_DIR}/static:${BACKUP_DIR}/static \
alpine tar czf ${BACKUP_DIR}/static/static_${DATE}.tar.gz -C /source .
# Cleanup old backups
echo "[$(date)] Cleaning up backups older than ${RETENTION_DAYS} days..."
find ${BACKUP_DIR} -type f -mtime +${RETENTION_DAYS} -delete
echo "[$(date)] Backup completed successfully: ${DATE}"
roles/psono/handlers/main.yml---
# Handlers for Psono role
- name: Restart Docker
ansible.builtin.service:
name: docker
state: restarted
- name: Restart SSH
ansible.builtin.service:
name: sshd
state: restarted
when: ansible_os_family == "RedHat"
- name: Restart SSH
ansible.builtin.service:
name: ssh
state: restarted
when: ansible_os_family == "Debian"
- name: Restart Psono
ansible.builtin.command:
cmd: docker compose restart
chdir: "{{ psono_base_dir }}"
# Encrypt sensitive variables
ansible-vault encrypt host_vars/psono.example.com.yml
# Run deployment
ansible-playbook -i inventory.ini psono-deploy.yml --ask-vault-pass
# Or with vault password file
ansible-playbook -i inventory.ini psono-deploy.yml --vault-password-file=~/.ansible_vault_pass
# Update version in group_vars/all.yml
# psono_version: "15.1" -> "15.2"
# Run update
ansible-playbook -i inventory.ini psono-deploy.yml --tags psono --ask-vault-pass
# Only Docker setup
ansible-playbook -i inventory.ini psono-deploy.yml --tags docker
# Only security hardening
ansible-playbook -i inventory.ini psono-deploy.yml --tags security
# Only application deployment
ansible-playbook -i inventory.ini psono-deploy.yml --tags psono
# Check mode (no changes made)
ansible-playbook -i inventory.ini psono-deploy.yml --check --diff --ask-vault-pass
# Check service status
ansible psono_servers -m shell -a "docker compose ps" -i inventory.ini
# Verify health endpoint
ansible psono_servers -m uri -a "url=https://psono.example.com/health/ validate_certs=no" -i inventory.ini
# Check backup directory
ansible psono_servers -m shell -a "ls -la /opt/psono/backups/" -i inventory.ini
https://psono.example.com in browser| Issue | Diagnosis | Solution |
|---|---|---|
| Docker not installed | Check Ansible output | Run with --tags docker |
| Certificate errors | Check DNS propagation | Verify domain points to server |
| Database connection failed | Check env vars | Verify psono_db_password |
| Firewall blocking | Check UFW status | ansible -m ufw -a "rule=allow port=443" |
# Verbose output
ansible-playbook -i inventory.ini psono-deploy.yml -vvv --ask-vault-pass
# Step through tasks
ansible-playbook -i inventory.ini psono-deploy.yml --step --ask-vault-pass
ansible.builtin. module namespacedocker_compose module replaced with command for Docker Compose v2| OS | Notes |
|---|---|
| Debian 12 | Preferred for latest packages |
| Ubuntu 22.04 LTS | Long-term support until 2027 |
| RHEL 9/Rocky 9 | Use dnf package manager |
| SUSE | Adjust package names for zypper |
Any questions?
Feel free to contact us. Find all contact information on our contact page.