Deploy WordPress with Ansible: LAMP Stack Automation
Teach me Ansible |
2025-01-06 |
30 min read
Learn how to automate WordPress deployment with Ansible. This complete guide covers LAMP stack installation, database setup, WordPress configuration, and SSL certificates using Let's Encrypt.
What You'll Build
By the end of this tutorial, you'll have a fully automated WordPress deployment that includes:
- LAMP Stack (Linux, Apache, MySQL, PHP)
- WordPress installation and configuration
- MySQL database and user setup
- SSL certificate with Let's Encrypt
- Security hardening
- Automated backups
Prerequisites
- Ubuntu 20.04/22.04 server
- Domain name pointed to your server
- Ansible installed on control node
- SSH access to target server
Step 1: Project Structure
Create the following directory structure:
wordpress-deployment/
├── ansible.cfg
├── inventory/
│ └── production
├── group_vars/
│ └── all.yml
├── roles/
│ ├── common/
│ ├── apache/
│ ├── mysql/
│ ├── php/
│ └── wordpress/
└── site.yml
Step 2: Configuration Files
ansible.cfg
[defaults]
inventory = inventory/production
remote_user = ubuntu
host_key_checking = False
retry_files_enabled = False
[privilege_escalation]
become = True
become_method = sudo
become_user = root
inventory/production
[wordpress]
your-domain.com ansible_host=YOUR_SERVER_IP
group_vars/all.yml
---
# Domain configuration
domain_name: "your-domain.com"
admin_email: "admin@your-domain.com"
# MySQL configuration
mysql_root_password: "{{ vault_mysql_root_password }}"
wordpress_db_name: "wordpress"
wordpress_db_user: "wpuser"
wordpress_db_password: "{{ vault_wordpress_db_password }}"
# WordPress configuration
wordpress_version: "6.4.2"
wordpress_admin_user: "admin"
wordpress_admin_password: "{{ vault_wordpress_admin_password }}"
wordpress_admin_email: "{{ admin_email }}"
# PHP configuration
php_version: "8.1"
# SSL configuration
enable_ssl: true
letsencrypt_email: "{{ admin_email }}"
Step 3: Create Vault File for Secrets
# Create encrypted vault file
ansible-vault create group_vars/vault.yml
Add your secrets:
---
vault_mysql_root_password: "SuperSecureRootPass123!"
vault_wordpress_db_password: "SecureDBPass456!"
vault_wordpress_admin_password: "SecureAdminPass789!"
Step 4: Main Playbook (site.yml)
---
- name: Deploy WordPress on LAMP Stack
hosts: wordpress
become: yes
roles:
- common
- apache
- mysql
- php
- wordpress
post_tasks:
- name: Display access information
debug:
msg:
- "WordPress installation complete!"
- "Access your site at: https://{{ domain_name }}"
- "Admin URL: https://{{ domain_name }}/wp-admin"
Step 5: Common Role (System Setup)
roles/common/tasks/main.yml
---
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install common packages
apt:
name:
- software-properties-common
- curl
- wget
- unzip
- git
- ufw
state: present
- name: Configure UFW firewall
ufw:
rule: allow
port: "{{ item }}"
loop:
- "22"
- "80"
- "443"
- name: Enable UFW
ufw:
state: enabled
policy: deny
Step 6: Apache Role
roles/apache/tasks/main.yml
---
- name: Install Apache
apt:
name: apache2
state: present
- name: Enable Apache modules
apache2_module:
name: "{{ item }}"
state: present
loop:
- rewrite
- ssl
- headers
notify: restart apache
- name: Create virtual host configuration
template:
src: wordpress.conf.j2
dest: "/etc/apache2/sites-available/{{ domain_name }}.conf"
notify: restart apache
- name: Enable virtual host
command: "a2ensite {{ domain_name }}.conf"
args:
creates: "/etc/apache2/sites-enabled/{{ domain_name }}.conf"
notify: restart apache
- name: Disable default site
command: a2dissite 000-default.conf
args:
removes: /etc/apache2/sites-enabled/000-default.conf
notify: restart apache
- name: Ensure Apache is running
service:
name: apache2
state: started
enabled: yes
roles/apache/templates/wordpress.conf.j2
<VirtualHost *:80>
ServerName {{ domain_name }}
ServerAlias www.{{ domain_name }}
DocumentRoot /var/www/{{ domain_name }}
<Directory /var/www/{{ domain_name }}>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/{{ domain_name }}_error.log
CustomLog ${APACHE_LOG_DIR}/{{ domain_name }}_access.log combined
</VirtualHost>
roles/apache/handlers/main.yml
---
- name: restart apache
service:
name: apache2
state: restarted
Step 7: MySQL Role
roles/mysql/tasks/main.yml
---
- name: Install MySQL
apt:
name:
- mysql-server
- python3-pymysql
state: present
- name: Start MySQL service
service:
name: mysql
state: started
enabled: yes
- name: Set MySQL root password
mysql_user:
name: root
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
state: present
- name: Create WordPress database
mysql_db:
name: "{{ wordpress_db_name }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Create WordPress database user
mysql_user:
name: "{{ wordpress_db_user }}"
password: "{{ wordpress_db_password }}"
priv: "{{ wordpress_db_name }}.*:ALL"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Remove anonymous MySQL users
mysql_user:
name: ''
host_all: yes
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Remove MySQL test database
mysql_db:
name: test
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
Step 8: PHP Role
roles/php/tasks/main.yml
---
- name: Add PHP repository
apt_repository:
repo: "ppa:ondrej/php"
state: present
update_cache: yes
- name: Install PHP and extensions
apt:
name:
- "php{{ php_version }}"
- "php{{ php_version }}-mysql"
- "php{{ php_version }}-curl"
- "php{{ php_version }}-gd"
- "php{{ php_version }}-mbstring"
- "php{{ php_version }}-xml"
- "php{{ php_version }}-xmlrpc"
- "php{{ php_version }}-zip"
- libapache2-mod-php
state: present
- name: Configure PHP settings
lineinfile:
path: "/etc/php/{{ php_version }}/apache2/php.ini"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^upload_max_filesize', line: 'upload_max_filesize = 64M' }
- { regexp: '^post_max_size', line: 'post_max_size = 64M' }
- { regexp: '^memory_limit', line: 'memory_limit = 256M' }
- { regexp: '^max_execution_time', line: 'max_execution_time = 300' }
notify: restart apache
Step 9: WordPress Role
roles/wordpress/tasks/main.yml
---
- name: Create WordPress directory
file:
path: "/var/www/{{ domain_name }}"
state: directory
owner: www-data
group: www-data
mode: '0755'
- name: Download WordPress
get_url:
url: "https://wordpress.org/wordpress-{{ wordpress_version }}.tar.gz"
dest: /tmp/wordpress.tar.gz
- name: Extract WordPress
unarchive:
src: /tmp/wordpress.tar.gz
dest: /tmp/
remote_src: yes
- name: Copy WordPress files
shell: "cp -r /tmp/wordpress/* /var/www/{{ domain_name }}/"
args:
creates: "/var/www/{{ domain_name }}/wp-config-sample.php"
- name: Set WordPress ownership
file:
path: "/var/www/{{ domain_name }}"
owner: www-data
group: www-data
recurse: yes
- name: Generate WordPress salts
uri:
url: https://api.wordpress.org/secret-key/1.1/salt/
return_content: yes
register: wordpress_salts
- name: Configure WordPress
template:
src: wp-config.php.j2
dest: "/var/www/{{ domain_name }}/wp-config.php"
owner: www-data
group: www-data
mode: '0644'
- name: Install WP-CLI
get_url:
url: https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
dest: /usr/local/bin/wp
mode: '0755'
- name: Install WordPress via WP-CLI
command: >
wp core install
--url="https://{{ domain_name }}"
--title="My WordPress Site"
--admin_user="{{ wordpress_admin_user }}"
--admin_password="{{ wordpress_admin_password }}"
--admin_email="{{ wordpress_admin_email }}"
--path="/var/www/{{ domain_name }}"
--allow-root
args:
creates: "/var/www/{{ domain_name }}/wp-config.php"
roles/wordpress/templates/wp-config.php.j2
<?php
define('DB_NAME', '{{ wordpress_db_name }}');
define('DB_USER', '{{ wordpress_db_user }}');
define('DB_PASSWORD', '{{ wordpress_db_password }}');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
{{ wordpress_salts.content }}
$table_prefix = 'wp_';
define('WP_DEBUG', false);
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
require_once ABSPATH . 'wp-settings.php';
Step 10: SSL with Let's Encrypt (Optional Role)
roles/letsencrypt/tasks/main.yml
---
- name: Install Certbot
apt:
name:
- certbot
- python3-certbot-apache
state: present
- name: Obtain SSL certificate
command: >
certbot --apache
--non-interactive
--agree-tos
--email {{ letsencrypt_email }}
--domains {{ domain_name }},www.{{ domain_name }}
args:
creates: "/etc/letsencrypt/live/{{ domain_name }}/fullchain.pem"
- name: Set up certificate auto-renewal
cron:
name: "Renew Let's Encrypt certificates"
minute: "0"
hour: "3"
job: "certbot renew --quiet"
Step 11: Run the Playbook
# Test syntax
ansible-playbook site.yml --syntax-check
# Dry run
ansible-playbook site.yml --check --ask-vault-pass
# Execute
ansible-playbook site.yml --ask-vault-pass
Step 12: Verify Installation
- Visit https://your-domain.com to see your WordPress site
- Access admin panel at https://your-domain.com/wp-admin
- Login with credentials from vault file
Bonus: Automated Backups
Add a backup role:
# roles/backup/tasks/main.yml
---
- name: Create backup directory
file:
path: /var/backups/wordpress
state: directory
mode: '0700'
- name: Create backup script
template:
src: backup.sh.j2
dest: /usr/local/bin/wordpress-backup.sh
mode: '0755'
- name: Schedule daily backups
cron:
name: "WordPress daily backup"
minute: "0"
hour: "2"
job: "/usr/local/bin/wordpress-backup.sh"
Success!
You now have a fully automated WordPress deployment! This playbook can be run multiple times safely thanks to idempotency, and can easily be adapted for multiple sites.
Next Steps
- Add monitoring with roles for Prometheus/Grafana
- Implement automated WordPress updates
- Set up staging environment
- Add Redis cache for performance
- Configure CDN integration
Conclusion
Ansible makes WordPress deployment reliable, repeatable, and fast. With this setup, you can deploy new WordPress sites in minutes and ensure consistency across all your deployments.
Try building this in our interactive playground or deploy to your own server!