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:
- Agentless:
- Requires no special software (agents) on managed nodes (your VPS). Uses existing protocols (SSH for Linux/Unix, WinRM for Windows).
- Push Model:
- Management is initiated from the "control node" (Ansible control node), which connects to target nodes via SSH to execute tasks.
- Readable and Writable:
- Configurations are written in YAML (a simple markup language). Core concepts:
- Inventory:
- File(s) describing your servers (hosts), their IPs/names, and groups (e.g., `webservers`, `dbservers`).
- Playbooks:
- YAML files defining *automated processes* (plays) and *tasks* to execute on hosts.
- Tasks:
- Specific actions (Ansible modules) performed on hosts (install packages, copy files, restart services).
- Modules:
- Pre-built functional "building blocks" (over 3000 built-in!). Examples: `apt`/`yum` (package management), `copy`/`template` (file management), `service` (service management), `mysql_db` (MySQL database management).
- Roles:
- A way to structure and reuse sets of tasks, variables, files, and templates for specific purposes (e.g., `nginx` or `postgresql` roles).
- Templates:
- Files (typically Jinja2-based) that dynamically generate configurations based on variables (e.g., Nginx config with a domain from a variable).
- Variables:
- Used to parameterize playbooks and roles (software versions, paths, passwords, settings).
- Inventory:
- Configurations are written in YAML (a simple markup language). Core concepts:
Advantages of Ansible
- Low Entry Barrier:
- Easier to start compared to Chef/Puppet due to YAML and no agents.
- Idempotency:
- Built into most modules, making repeated playbook runs safe.
- Large Community and Ecosystem:
- Rich documentation, thousands of modules, and pre-built roles on Ansible Galaxy.
- Versatility:
- Manages not only OS configurations but also clouds (AWS, Azure, GCP), network devices, containers (Kubernetes), and applications.
- Security:
- No agents reduce attack surface. Integrates with `ansible-vault` for encrypting secrets (passwords, keys).
- CI/CD Integration:
- Playbooks easily integrate with Jenkins, GitLab CI, and others.
Disadvantages and Limitations
- Performance on Large Fleets:
- Push model via SSH can bottleneck when managing thousands of servers simultaneously (mitigated by Tower/AWX or dynamic inventory).
- Limited "Intelligence" on Target Nodes:
- Complex data processing logic is harder than in agent-based systems (Chef/Puppet), though possible via `command`/`shell` or Jinja2 filters.
- Debugging Complex Scenarios:
- Debugging large playbooks or Jinja2 templates can be challenging.
- Dependency on SSH and Python:
- Requires working SSH and Python (2.7 or 3.5+) on target nodes for most modules.
Why Use Ansible for VPS?
- Fast and Predictable Deployment:
- Installing LAMP/LEMP stacks, Node.js, Docker, PostgreSQL, Redis, etc., becomes a single-step operation.
- Environment Consistency:
- Ensures all servers in a group (`production`, `staging`) are identically configured.
- Reproducibility:
- Easily recreate a server after failure or deploy a new one.
- Version-Controlled Configuration:
- All setup code is stored in Git.
- Simplified Complex Tasks:
- Automates firewall setup, backups, and monitoring.
- Frees Admin Time:
- Focus on infrastructure improvements, not routine tasks.
Alternatives to Ansible
- Chef:
- Powerful, mature tool. Requires an agent (Chef Client) and server (Chef Infra Server). More complex to start but highly flexible for complex, dynamic environments.
- Puppet:
- A pioneer. Agent-based (Puppet Agent), uses a declarative DSL. Strong for maintaining long-term consistency in large infrastructures. More complex than Ansible.
- SaltStack (Salt Project):
- Agent-based (salt-minion) or agentless (salt-ssh). Fast and flexible. Uses YAML and Jinja2 (like Ansible) but with a different state approach. Can be complex for basic tasks.
- Terraform:
- Not a direct competitor! Terraform focuses on cloud infrastructure orchestration (creating VPS, networks, load balancers). Ansible focuses on configuring existing servers. Often used together: Terraform provisions infrastructure, Ansible configures it.
- Scripts (Bash/Python):
- Simple for one-off tasks but lack idempotency, are hard to maintain, and don’t scale well.
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
- Testing:
- Open your VPS IP or configure DNS for `domain_name` in a browser. You should see Nginx’s default page or your app.
- Re-running:
- Run the playbook again (`ansible-playbook ...`). Most tasks should return `ok` (idempotency), with changes applied only where needed.
- Secret Management:
- Replace plaintext passwords! Use `ansible-vault`:
-
ansible-vault encrypt_string 'secure_root_pass' --name 'mysql_root_password'
-
- Replace plaintext passwords! Use `ansible-vault`:
- Copy encrypted output to `vars/vault.yml`, include it in the playbook with `vars_files`, and use `--ask-vault-pass` when running.
- Structuring:
- Split large playbooks into Roles (e.g., `common`, `mysql`, `php`, `nginx`, `app`). Use `ansible-galaxy init role_name` to create role structures.
- Ansible Galaxy:
- Use pre-built roles for common software (e.g., `geerlingguy.mysql`, `geerlingguy.nginx`). Install with `ansible-galaxy install role_name`.
- Dynamic Inventory:
- Use scripts (Python, Bash) or cloud provider plugins to auto-fetch host lists from clouds (AWS, DigitalOcean, etc.).
- Testing Playbooks:
- Use Molecule and Testinfra to write tests for your roles.
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.