24.06.2025

Using Ansible to Automate Server and Database Setup on VPS

Manual server setup on VPS is time-consuming, error-prone, and inconsistent. Ansible is an automation tool that lets you define server configurations as code, ensuring idempotency and repeatability. This guide will help you set up a VPS with Nginx, MySQL, and PHP using Ansible, minimizing manual work and improving reliability.

What is Ansible?

Ansible is a simple yet powerful open-source automation tool (acquired by Red Hat). Its key features:

Advantages of Ansible

Disadvantages and Limitations

Why Use Ansible for VPS?

Alternatives to Ansible

Setup, Deployment, and Usage - Step-by-Step Example (Installing Nginx + MySQL + PHP on Ubuntu VPS)

Setting Up the Control Node (Ansible Control Node)

OS: Linux (Ubuntu/Debian/CentOS) or macOS. Windows requires WSL.

Install Ansible:

# Ubuntu/Debian

sudo apt update && sudo apt install ansible -y

# CentOS/RHEL (EPEL)

sudo yum install epel-release -y

sudo yum install ansible -y

# Verify

ansible --version

- Set Up SSH Keys: Generate a key pair on the control node (`ssh-keygen`) and copy the public key to target VPS (`ssh-copy-id user@your_vps_ip`) for passwordless access.

Creating Inventory

Create `inventory.ini`:

[webservers]

web1.example.com ansible_host=192.168.1.100 # Replace with your VPS IP/DNS

# web2 ansible_host=192.168.1.101 # Add more servers if needed

[databases]

db1.example.com ansible_host=192.168.1.100 # For this example, same server. Usually separate.

[all:vars]

ansible_user=ubuntu # SSH user

ansible_python_interpreter=/usr/bin/python3 # Specify Python 3 on target nodes

Test Connection:

ansible all -i inventory.ini -m ping

# Should return `pong` for each host

 Creating a Playbook

Create `setup_lamp.yml`:

- name: Install and Configure LAMP Stack on Web Servers

hosts: webservers # Apply to webservers group only

become: yes # Run tasks with root privileges (sudo)

vars: # Variables for the playbook

mysql_root_password: "secure_root_pass" # IN PRODUCTION, USE ansible-vault!

mysql_db_name: "myapp"

mysql_db_user: "appuser"

mysql_db_password: "secure_user_pass" # ENCRYPT THIS TOO!

domain_name: "myapp.example.com"

tasks: # List of tasks

# 1. Update package index

- name: Update apt package index

ansible.builtin.apt:

update_cache: yes

# 2. Install required system packages

- name: Install required system packages

ansible.builtin.apt:

name:

- curl

- software-properties-common

- unzip

- git

state: present

# 3. Install and configure firewall (ufw), allow SSH and HTTP/HTTPS

- name: Install and configure UFW

ansible.builtin.apt:

name: ufw

state: present

notify: Reload UFW # Notify handler

- name: Allow SSH through firewall

ansible.builtin.ufw:

rule: allow

port: '22'

proto: tcp

- name: Allow HTTP through firewall

ansible.builtin.ufw:

rule: allow

port: '80'

proto: tcp

- name: Allow HTTPS through firewall

ansible.builtin.ufw:

rule: allow

port: '443'

proto: tcp

# 4. Install MySQL Server

- name: Install MySQL Server

ansible.builtin.apt:

name: mysql-server

state: present

- name: Ensure MySQL is running and enabled

ansible.builtin.service:

name: mysql

state: started

enabled: yes

# 5. Configure MySQL (secure setup, create DB and user)

- name: Set MySQL root password (Idempotent)

ansible.builtin.mysql_user:

login_user: root

login_password: "" # Initial empty password

name: root

password: "{{ mysql_root_password }}"

host: localhost

check_implicit_admin: yes

state: present

- name: Remove anonymous MySQL users

ansible.builtin.mysql_user:

login_user: root

login_password: "{{ mysql_root_password }}"

name: ''

host_all: yes

state: absent

- name: Remove MySQL test database

ansible.builtin.mysql_db:

login_user: root

login_password: "{{ mysql_root_password }}"

name: test

state: absent

- name: Create application database

ansible.builtin.mysql_db:

login_user: root

login_password: "{{ mysql_root_password }}"

name: "{{ mysql_db_name }}"

state: present

encoding: utf8mb4

collation: utf8mb4_unicode_ci

- name: Create application database user with privileges

ansible.builtin.mysql_user:

login_user: root

login_password: "{{ mysql_root_password }}"

name: "{{ mysql_db_user }}"

password: "{{ mysql_db_password }}"

host: '%' # Or 'localhost' if app is on same server

priv: "{{ mysql_db_name }}.*:ALL"

state: present

# 6. Install PHP and required modules

- name: Add ondrej/php PPA for newer PHP versions

ansible.builtin.apt_repository:

repo: "ppa:ondrej/php"

state: present

- name: Install PHP and common extensions

ansible.builtin.apt:

name:

- php8.1-fpm # Specify desired version (7.4, 8.0, 8.1, 8.2)

- php8.1-mysql

- php8.1-curl

- php8.1-gd

- php8.1-mbstring

- php8.1-xml

- php8.1-zip

state: present

update_cache: yes

- name: Ensure PHP-FPM is running and enabled

ansible.builtin.service:

name: php8.1-fpm

state: started

enabled: yes

# 7. Install Nginx

- name: Install Nginx

ansible.builtin.apt:

name: nginx

state: present

- name: Ensure Nginx is running and enabled

ansible.builtin.service:

name: nginx

state: started

enabled: yes

# 8. Configure Nginx virtual host (using Jinja2 template)

- name: Create Nginx config directory if needed

ansible.builtin.file:

path: /etc/nginx/sites-available

state: directory

- name: Deploy Nginx virtual host configuration from template

ansible.builtin.template:

src: templates/nginx_vhost.conf.j2 # Path to template on Control Node

dest: /etc/nginx/sites-available/{{ domain_name }}.conf

owner: root

group: root

mode: '0644'

notify: Reload Nginx # Notify handler on config change

- name: Enable the site by creating symlink

ansible.builtin.file:

src: /etc/nginx/sites-available/{{ domain_name }}.conf

dest: /etc/nginx/sites-enabled/{{ domain_name }}.conf

state: link

- name: Remove default Nginx site

ansible.builtin.file:

path: /etc/nginx/sites-enabled/default

state: absent

notify: Reload Nginx

# 9. Deploy application code (example)

- name: Create web root directory

ansible.builtin.file:

path: /var/www/{{ domain_name }}

state: directory

owner: "{{ ansible_user }}"

group: www-data

mode: '0775'

- name: Clone Git repository (example)

ansible.builtin.git:

repo: "https://github.com/yourusername/yourrepo.git"

dest: /var/www/{{ domain_name }}

version: "main" # Branch or tag

clone: yes

update: yes

when: false # Uncomment and configure as needed

handlers: # Handlers triggered by notify

- name: Reload UFW

ansible.builtin.service:

name: ufw

state: reloaded

- name: Reload Nginx

ansible.builtin.service:

name: nginx

state: reloaded # Graceful reload without dropping connections

Create Nginx Template (`templates/nginx_vhost.conf.j2`):

server {

listen 80;

listen [::]:80;

server_name {{ domain_name }};

root /var/www/{{ domain_name }}/public; # Path to app's public folder

index index.php index.html index.htm;

location / {

try_files $uri $uri/ /index.php?$query_string;

}

location ~ \.php$ {

include snippets/fastcgi-php.conf;

fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Ensure PHP version matches!

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

include fastcgi_params;

}

location ~ /\.ht {

deny all;

}

# Additional settings (logs, gzip, etc.)

access_log /var/log/nginx/{{ domain_name }}-access.log;

error_log /var/log/nginx/{{ domain_name }}-error.log;
}

Run the Playbook:

ansible-playbook -i inventory.ini setup_lamp.yml

Ansible connects to VPS in the `webservers` group.

Executes tasks sequentially.

Outputs a detailed report (OK, CHANGED, FAILED) for each task on each host.

Usage and Next Steps

Ansible is a powerful, accessible, and elegant tool that transforms VPS management into a predictable, versioned, and scalable process. By describing infrastructure as code, it ensures consistency, repeatability, and reliability. Start with simple task automation, master core concepts (inventory, playbooks, tasks, templates, variables, roles), and gradually manage your entire infrastructure. Investing time in learning Ansible pays off by saving hours of manual work and preventing incidents.