Tutorial

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

  1. Visit https://your-domain.com to see your WordPress site
  2. Access admin panel at https://your-domain.com/wp-admin
  3. 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!