This repository provides complete automation for deploying a production-ready Drupal 11 site on DigitalOcean using Ansible. The automation handles everything from server provisioning to security hardening, delivering a fully configured Drupal installation with enterprise-grade security.
- 🚀 Complete Drupal 11 Installation: Latest version with Composer dependency management
- 🔒 Enterprise Security: SSH hardening, firewall configuration, and security headers
- ⚡ Optimized Performance: Nginx + PHP 8.3 + MySQL 8.0 stack
- ☁️ Cloudflare Integration: SSL termination and CDN support
- 🛡️ Production Hardening: UFW firewall, fail2ban protection, and secure configurations
- 📱 Modern UI: Responsive design with security modules pre-installed
- OS: Ubuntu 22.04 LTS
- Web Server: Nginx with security headers
- Database: MySQL 8.0 with optimized configuration
- PHP: PHP 8.3 with security hardening
- CMS: Drupal 11 with security modules
- Security: UFW firewall + SSH hardening + Security Kit module
Before running the automation, you need to configure the following placeholder values in your local copy:
cp vars.yml.example vars.yml
cp drupal-vars.yml.example drupal-vars.yml
cp inventory.example inventory
ADMIN_USER
: Replaceyour_admin_username
with your desired admin usernameSSH_PUBLIC_KEY
: Replaceyour-ssh-public-key-here
with your actual SSH public key
DRUPAL_DOMAIN_NAME
: Replaceyour-domain.com
with your actual domainDRUPAL_DB_PASSWORD
: Replaceyour_secure_db_password
with a strong database passwordDRUPAL_DB_ROOT_PASSWORD
: Replaceyour_secure_root_password
with a strong root passwordDRUPAL_ADMIN_PASSWORD
: Replaceyour_secure_admin_password
with a strong admin passwordDRUPAL_SITE_NAME
: ReplaceYour Drupal Site Name
with your site's name
your-server
: Replace with your server hostnameYOUR_SERVER_IP
: Replace with your DigitalOcean droplet's IP addressyour-private-key
: Replace with the path to your SSH private key
# Generate SSH key pair if you don't have one
ssh-keygen -t ed25519 -C "your-email@example.com"
# Add public key to DigitalOcean
doctl compute ssh-key import your-key-name --public-key-file ~/.ssh/id_ed25519.pub
- Never commit the actual configuration files (
vars.yml
,drupal-vars.yml
,inventory
) to version control - Use strong passwords (20+ characters) for all password fields
- Ensure your SSH private key is properly secured with appropriate file permissions (
chmod 600
)
doctl compute droplet list --format ID,Name,PublicIPv4,Status
ssh-keygen -R <old IP>
doctl compute ssh-key list
doctl compute droplet delete homedevbox-ghost
doctl compute droplet create homedevbox-drupal \
--image ubuntu-22-04-x64 \
--size s-1vcpu-2gb \
--region fra1 \
--ssh-keys 36:d2:d8:a4:72:45:62:7c:75:3c:f4:b8:d5:a8:43:dd \
--wait
doctl compute droplet list --format ID,Name,PublicIPv4,Status
sed -i '' 's/<old IP>/<new IP>/' ~/.ssh/config
Host your-server
HostName <new IP>
User your_admin_user
IdentityFile ~/.ssh/your-private-key
[droplets]
your-server ansible_host=<new IP> ansible_python_interpreter=/usr/bin/python3 ansible_ssh_private_key_file=~/.ssh/your-private-key
Note: These commands connect as
root
user for initial setup
ansible all -i inventory -m ping --private-key ~/.ssh/your-private-key
ansible-playbook -i inventory playbooks/baseline.yml --private-key ~/.ssh/your-private-key
ansible-playbook -i inventory playbooks/mysql.yml --private-key ~/.ssh/your-private-key
ansible-playbook -i inventory playbooks/php.yml --private-key ~/.ssh/your-private-key
ansible-playbook -i inventory playbooks/nginx.yml --private-key ~/.ssh/your-private-key
ansible-playbook -i inventory playbooks/drupal.yml --private-key ~/.ssh/your-private-key
ansible-playbook -i inventory playbooks/firewall.yml --private-key ~/.ssh/your-private-key
ansible-playbook -i inventory playbooks/ssh-security.yml --private-key ~/.ssh/your-private-key
⚠️ WARNING: After SSH hardening, root login is disabled. Use Phase 2 commands below.
Note: After SSH hardening, all commands must use
-u your_admin_user
(root login disabled)
ansible all -i inventory -m ping --private-key ~/.ssh/your-private-key -u your_admin_user
ansible all -i inventory -m shell -a "/var/www/drupal/vendor/bin/drush status" --private-key ~/.ssh/your-private-key -u your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush cache:rebuild" --private-key ~/.ssh/your-private-key -u your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush pm:list --type=module" --private-key ~/.ssh/your-private-key -u your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush pm:list --type=theme" --private-key ~/.ssh/your-private-key -u your_admin_user
# Download via Composer (requires file ownership)
ansible all -i inventory -m shell -a "cd /var/www/drupal && composer require drupal/pathauto" --private-key ~/.ssh/your-private-key -u your_admin_user --become-user your_admin_user
# Enable via Drush
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush pm:install pathauto -y" --private-key ~/.ssh/your-private-key -u your_admin_user
# Admin Toolbar (Enhanced admin navigation)
ansible all -i inventory -m shell -a "cd /var/www/drupal && composer require drupal/admin_toolbar" --private-key ~/.ssh/your-private-key -u your_admin_user --become-user your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush pm:install admin_toolbar admin_toolbar_tools -y" --private-key ~/.ssh/your-private-key -u your_admin_user
# Token (Provides token replacement functionality)
ansible all -i inventory -m shell -a "cd /var/www/drupal && composer require drupal/token" --private-key ~/.ssh/your-private-key -u your_admin_user --become-user your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush pm:install token -y" --private-key ~/.ssh/your-private-key -u your_admin_user
# Devel (Development tools)
ansible all -i inventory -m shell -a "cd /var/www/drupal && composer require drupal/devel" --private-key ~/.ssh/your-private-key -u your_admin_user --become-user your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush pm:install devel -y" --private-key ~/.ssh/your-private-key -u your_admin_user
# Download via Composer
ansible all -i inventory -m shell -a "cd /var/www/drupal && composer require drupal/bootstrap" --private-key ~/.ssh/your-private-key -u your_admin_user --become-user your_admin_user
# Enable theme
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush theme:enable bootstrap -y" --private-key ~/.ssh/your-private-key -u your_admin_user
# Set as default theme (optional)
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush config:set system.theme default bootstrap -y" --private-key ~/.ssh/your-private-key -u your_admin_user
- ✅ SSH Key Authentication: Password authentication disabled (VM level)
- ✅ Firewall Protection: UFW configured with minimal open ports (VM level)
- ✅ Domain-Only Access: IP access blocked, redirects to domain (VM level - Nginx)
- ✅ Strong Password Policy: Login Security module enforces complexity (Drupal level)
- ✅ Failed Login Protection: Login Security blocks brute force attempts (Drupal level)
- ✅ Auto Logout: Automated Logout module for idle sessions (Drupal level)
- ✅ Security Headers: SecKit module adds XSS/CSRF protection (Drupal level)
- ✅ Update Monitoring: Update Status module tracks security patches (Drupal level)
- ✅ HTTPS Termination: Cloudflare provides SSL/TLS encryption (External service)
- Strong Admin Password: Use a password manager with 20+ character passwords
- Limited Admin Access: Only log in when necessary, log out immediately
- Regular Updates: Monitor and apply security updates promptly
- Access Logs: Monitor
/var/log/nginx/drupal_access.log
for suspicious activity - Backup Strategy: Regular automated backups of database and files
ansible all -i inventory -m shell -a "cd /var/www/drupal && composer update drupal/core-recommended --with-dependencies" --private-key ~/.ssh/your-private-key -u your_admin_user --become-user your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush updatedb -y" --private-key ~/.ssh/your-private-key -u your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush cache:rebuild" --private-key ~/.ssh/your-private-key -u your_admin_user
ansible all -i inventory -m shell -a "cd /var/www/drupal && vendor/bin/drush sql:dump --gzip --result-file=/tmp/drupal-backup-$(date +%Y%m%d-%H%M%S).sql.gz" --private-key ~/.ssh/your-private-key -u your_admin_user
Phase 1 (Fresh Install): ansible-playbook ... --private-key ~/.ssh/your-private-key
Phase 2 (Post-Hardening): ansible ... --private-key ~/.ssh/your-private-key -u your_admin_user
For Composer operations: ansible ... --private-key ~/.ssh/your-private-key -u your_admin_user --become-user your_admin_user
Problem: Using only -u your_admin_user
causes Composer to run as root, triggering "Do not run Composer as root" warnings that cause commands to hang waiting for user input.
Solution: Always use --become-user your_admin_user
for Composer commands to ensure proper user context and prevent hanging.
Example:
# ❌ WRONG - Will hang
ansible ... -u your_admin_user -m shell -a "composer require drupal/module"
# ✅ CORRECT - Works properly
ansible ... -u your_admin_user --become-user your_admin_user -m shell -a "composer require drupal/module"