Automate VPS Configuration with Ansible: Playbooks, Roles, and Idempotent Server Setup

Automate VPS Configuration with Ansible: Playbooks, Roles, and Idempotent Server Setup

Manually configuring a VPS is time-consuming and error-prone. Ansible automates server configuration as code — you write YAML playbooks that describe the desired server state, and Ansible enforces it. Run the same playbook against a fresh VPS and it produces an identical configured server. Run it again and nothing changes (idempotent). This guide builds Ansible playbooks for a complete LEMP stack deployment.

Why Ansible Over Shell Scripts

  • Idempotent: Running a playbook twice is safe — Ansible skips tasks already in the desired state
  • Agentless: Ansible connects over SSH — no agent installed on the managed server
  • Readable: YAML playbooks document what the server should look like, not just the steps to get there
  • Reproducible: Spin up an identical server from scratch with one command
  • Version controlled: Infrastructure configuration lives in Git alongside application code

Install Ansible (Control Machine)

# Install on your local machine (Mac, Linux, or WSL)
pip install ansible

# Verify
ansible --version

Project Structure

mkdir -p ~/ansible-vps/{inventory,roles,group_vars}
cd ~/ansible-vps

tree .
# .
# ├── inventory/
# │   └── hosts.yml          # Server list
# ├── group_vars/
# │   └── all.yml            # Variables for all servers
# ├── roles/
# │   ├── common/            # Base server hardening
# │   ├── nginx/             # Web server
# │   ├── php/               # PHP-FPM
# │   ├── mariadb/           # Database
# │   └── wordpress/         # WordPress deployment
# └── site.yml               # Main playbook

Step 1: Inventory File

nano inventory/hosts.yml
all:
  children:
    webservers:
      hosts:
        prod-web:
          ansible_host: 203.0.113.47
          ansible_user: deploy
          ansible_ssh_private_key_file: ~/.ssh/prod_ed25519
        staging-web:
          ansible_host: 203.0.113.48
          ansible_user: deploy
          ansible_ssh_private_key_file: ~/.ssh/dev_ed25519
    databases:
      hosts:
        prod-db:
          ansible_host: 10.0.0.5
          ansible_user: deploy
# Test connectivity
ansible all -i inventory/hosts.yml -m ping

Step 2: Variables File

nano group_vars/all.yml
---
# System
ubuntu_version: "24.04"
timezone: "UTC"

# Nginx
nginx_worker_processes: auto
nginx_worker_connections: 1024

# PHP
php_version: "8.2"
php_fpm_max_children: 10
php_fpm_start_servers: 3

# MariaDB
mysql_root_password: "{{ vault_mysql_root_password }}"   # From Ansible Vault
mysql_db_name: wordpress
mysql_db_user: wordpress
mysql_db_password: "{{ vault_mysql_db_password }}"

# WordPress
wp_domain: yourdomain.com
wp_admin_user: admin
wp_admin_email: admin@yourdomain.com

Step 3: Common Role — Base Server Setup

mkdir -p roles/common/{tasks,handlers}
nano roles/common/tasks/main.yml
---
- name: Update apt cache
  apt:
    update_cache: yes
    cache_valid_time: 3600

- name: Install essential packages
  apt:
    name:
      - curl
      - wget
      - git
      - unzip
      - ufw
      - fail2ban
      - htop
      - vim
    state: present

- name: Set timezone
  timezone:
    name: "{{ timezone }}"

- name: Configure UFW defaults
  ufw:
    default: deny
    direction: incoming

- name: Allow SSH
  ufw:
    rule: limit
    port: "22"
    proto: tcp

- name: Allow HTTP and HTTPS
  ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop:
    - "80"
    - "443"

- name: Enable UFW
  ufw:
    state: enabled

- name: Disable root SSH password login
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: "^PermitRootLogin"
    line: "PermitRootLogin no"
    state: present
  notify: reload sshd

- name: Add deploy user to sudoers
  user:
    name: deploy
    groups: sudo
    append: yes
    shell: /bin/bash
nano roles/common/handlers/main.yml
---
- name: reload sshd
  service:
    name: ssh
    state: reloaded

Step 4: Nginx Role

mkdir -p roles/nginx/{tasks,templates,handlers}
nano roles/nginx/tasks/main.yml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present

- name: Deploy Nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    backup: yes
  notify: reload nginx

- name: Deploy site configuration
  template:
    src: site.conf.j2
    dest: "/etc/nginx/sites-available/{{ wp_domain }}"
  notify: reload nginx

- name: Enable site
  file:
    src: "/etc/nginx/sites-available/{{ wp_domain }}"
    dest: "/etc/nginx/sites-enabled/{{ wp_domain }}"
    state: link
  notify: reload nginx

- name: Remove default site
  file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  notify: reload nginx

- name: Ensure Nginx is started and enabled
  service:
    name: nginx
    state: started
    enabled: yes
nano roles/nginx/templates/site.conf.j2
server {
    listen 80;
    server_name {{ wp_domain }} www.{{ wp_domain }};
    root /var/www/{{ wp_domain }};
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php{{ php_version }}-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires max;
        access_log off;
    }
}
nano roles/nginx/handlers/main.yml
---
- name: reload nginx
  service:
    name: nginx
    state: reloaded

Step 5: PHP Role

mkdir -p roles/php/tasks
nano roles/php/tasks/main.yml
---
- name: Install PHP and extensions
  apt:
    name:
      - "php{{ php_version }}-fpm"
      - "php{{ php_version }}-mysql"
      - "php{{ php_version }}-curl"
      - "php{{ php_version }}-gd"
      - "php{{ php_version }}-mbstring"
      - "php{{ php_version }}-xml"
      - "php{{ php_version }}-zip"
      - "php{{ php_version }}-redis"
    state: present

- name: Configure PHP-FPM pool
  template:
    src: www.conf.j2
    dest: "/etc/php/{{ php_version }}/fpm/pool.d/www.conf"
  notify: restart php-fpm

- name: Enable OPcache
  lineinfile:
    path: "/etc/php/{{ php_version }}/fpm/conf.d/10-opcache.ini"
    regexp: "^;?opcache.enable="
    line: "opcache.enable=1"
  notify: restart php-fpm

- name: Start and enable PHP-FPM
  service:
    name: "php{{ php_version }}-fpm"
    state: started
    enabled: yes

Step 6: Main Playbook

nano site.yml
---
- name: Configure all web servers
  hosts: webservers
  become: yes    # Run as sudo

  roles:
    - common
    - nginx
    - php
    - mariadb
    - wordpress

  post_tasks:
    - name: Verify Nginx is running
      uri:
        url: "http://{{ ansible_host }}"
        status_code: 200
      delegate_to: localhost

Run the Playbook

# Dry run first (--check = show what would change without applying)
ansible-playbook -i inventory/hosts.yml site.yml --check

# Apply to staging first
ansible-playbook -i inventory/hosts.yml site.yml --limit staging-web

# Apply to production
ansible-playbook -i inventory/hosts.yml site.yml --limit prod-web

# Run only specific roles (tags)
ansible-playbook -i inventory/hosts.yml site.yml --tags nginx,php

Ansible Vault for Secrets

# Create encrypted variables file
ansible-vault create group_vars/vault.yml

# Add secrets (vault.yml content):
# vault_mysql_root_password: SuperSecureRootPassword!
# vault_mysql_db_password: SuperSecureDbPassword!

# Run playbook with vault password
ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass

Getting Started

Ansible is the most practical infrastructure automation tool for VPS fleets of any size — from 1 server to 100. Start by automating your most repetitive manual task (package installation, user creation, config file deployment). Each playbook you write is configuration that documents your infrastructure and can reproduce it exactly on any new Ubuntu VPS at VPS.DO.

Conclusion

Ansible playbooks transform server configuration from a manual checklist into version-controlled, repeatable automation. A playbook that configures a LEMP stack, deploys WordPress, and hardens SSH can provision a production-ready VPS from scratch in under 10 minutes — and the same playbook applied to a running server changes only what differs from the desired state. Infrastructure as code is the right foundation for managing any VPS fleet.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!