In this example we are going to use Vagrant and Ansible to build a LEMP (Linux, Nginx, MySQL, PHP-FPM) server on a single server. We will make sure that certain versions of Nginx (1.10.*), MySQL (5.7.*) and PHP (7.1.*) installed.


System


I am using Ansible 2.4.3.0 and Vagrant 1.9.5 on MacOS.


Structure


$ tree
.
├── Vagrantfile
├── lemp
├── provisioning
│   ├── host_vars
│   │   └── lemp
│   ├── hosts.yml
│   ├── roles
│   │   └── common
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── begin.yml
│   │   │   ├── end.yml
│   │   │   ├── main.yml
│   │   │   ├── mysql.yml
│   │   │   ├── nginx.yml
│   │   │   ├── php.yml
│   │   │   └── utility.yml
│   │   └── templates
│   │   ├── mysql.conf.j2
│   │   ├── mysql.php.j2
│   │   ├── ntp.conf.j2
│   │   ├── php.conf.j2
│   │   ├── site1.php.j2
│   │   ├── site1.vhost.j2
│   │   ├── site2.php.j2
│   │   └── site2.vhost.j2
│   └── site.yml
└── vagrant_hosts.yml

Tasks



Files


vagrant_hosts.yml


# Reflect any "hostname" and "ip" changes in "provisioning/hosts.yml" file

- name: This is for LEMP box
label: LEMP - 192.168.99.31
hostname: lemp
ip: 192.168.99.31

Vagrantfile


# -*- mode: ruby -*-
# vi: set ft=ruby :

require "yaml"

boxes = YAML.load_file("vagrant_hosts.yml")

Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"

ANSIBLE_RAW_SSH_ARGS = []
i = 0

boxes.each do |box|
ANSIBLE_RAW_SSH_ARGS << "-o IdentityFile=.vagrant/machines/#{box["hostname"]}/virtualbox/private_key"

config.vm.define box["hostname"] do |machine|
machine.vm.hostname = box["hostname"]
machine.vm.network "private_network", ip: box["ip"]
machine.vm.synced_folder box["hostname"], "/srv/www", create: true, nfs: true, mount_options: ["actimeo=2"]
machine.vm.provider :virtualbox do |virtualbox|
virtualbox.name = box["label"]
end
i += 1

if i == boxes.count
machine.vm.provision :ansible do |ansible|
ansible.raw_ssh_args = ANSIBLE_RAW_SSH_ARGS
ansible.verbose = "-vvv"
ansible.limit = "all"
ansible.inventory_path = "provisioning/hosts.yml"
ansible.playbook = "provisioning/site.yml"
end
end
end
end
end

provisioning/hosts.yml


# Reflect any "hostname" and "ip" changes in "../vagrant_hosts.yml" file

all:
hosts:
lemp:
ansible_host: 192.168.99.31

provisioning/site.yml


---
# This playbook sets up whole stack.

- name: Apply common configuration to "lemp" host
hosts: lemp
remote_user: root
become: yes
roles:
- common

provisioning/host_vars/lemp


---
# Variables listed here are applicable to "common" roles

localhost: 127.0.0.1

ansible_python_interpreter: /usr/bin/python3
locale: en_GB.UTF-8
language: en_GB:en

web_root: /srv/www
nginx_root: /etc/nginx

mysql_database: lemp
mysql_port: 3306
mysql_user: root
mysql_password: root
mysql_encoding: utf8mb4
mysql_collation: utf8mb4_unicode_ci

provisioning/roles/common/handlers/main.yml


---
# This playbook contains common handlers that can be called in tasks.

# sudo service ntp restart (whether running or not)
- name: Restart ntp
service:
name: ntp
state: restarted
enabled: yes

# sudo update-locale
- name: Update locale
shell: update-locale

# sudo service nginx restart (whether running or not)
- name: Restart nginx
service:
name: nginx
state: restarted
enabled: yes

# sudo service mysql restart (whether running or not)
- name: Restart mysql
service:
name: mysql
state: restarted
enabled: yes

# sudo service php7.1-fpm restart (whether running or not)
- name: Restart php
service:
name: php7.1-fpm
state: restarted
enabled: yes

provisioning/roles/common/tasks/begin.yml


---
# This playbook contains common actions that will be run on "lemp" hosts.

# sudo apt-get update
- name: Update apt packages
apt:
update_cache: yes
tags:
- system

# sudo locale-gen en_GB.UTF-8
- name: Install GB locale
locale_gen:
name: "{{ locale }}"
state: present
tags:
- locale

# sudo update-locale LANG=en_GB.UTF-8
# sudo update-locale LC_ALL=en_GB.UTF-8
# sudo update-locale LANGUAGE=en_GB:en
- name: Set locale
command: update-locale "{{ item }}"
with_items:
- LANG="{{ locale }}"
- LC_ALL="{{ locale }}"
- LANGUAGE="{{ language }}"
notify: Update locale
tags:
- locale

# sudo timedatectl set-timezone Europe/London
- name: Set time zone to Europe/London
timezone:
name: Europe/London
tags:
- time

# sudo apt-get install ntp
- name: Install ntp
apt:
name: ntp
state: present
update_cache: yes
tags:
- ntp

# sudo cp provisioning/common/templates/ntp.conf.j2 /etc/ntp.conf
- name: Configure ntp file and restart
template:
src: ntp.conf.j2
dest: /etc/ntp.conf
notify: Restart ntp
tags:
- ntp

provisioning/roles/common/tasks/end.yml


---
# This playbook contains common actions that will be run on "lemp" hosts.

# sudo apt-get autoclean
- name: Remove useless apt packages from the cache
apt:
autoclean: yes
tags:
- system

# sudo apt-get autoremove
- name: Remove dependencies that are no longer required
apt:
autoremove: yes
tags:
- system

provisioning/roles/common/tasks/main.yml


---
# This playbook contains common actions that will be run on "lemp" hosts.

# Import begin tasks
- name: Import begin tasks
include_tasks: begin.yml

# Import utility tasks
- name: Import utility tasks
include_tasks: utility.yml

# Import nginx tasks
- name: Import nginx tasks
include_tasks: nginx.yml

# Import mysql tasks
- name: Import mysql tasks
include_tasks: mysql.yml

# Import php tasks
- name: Import php tasks
include_tasks: php.yml

# Restart services
- name: Restart services
command: /bin/true
notify:
- Restart nginx
- Restart mysql
- Restart php
tags:
- nginx
- mysql
- php

# Import end tasks
- name: Import end tasks
include_tasks: end.yml

provisioning/roles/common/tasks/mysql.yml


---
# This playbook contains mysql actions that will be run on "lemp" hosts.

# sudo apt-get install *
- name: Install mysql and packages
apt:
name: "{{ item }}"
state: present
update_cache: yes
with_items:
- mysql-server=5.7.*
- python3-mysqldb
tags:
- mysql

# CREATE DATABASE lemp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- name: Create a new database
mysql_db:
name: "{{ mysql_database }}"
encoding: "{{ mysql_encoding }}"
collation: "{{ mysql_collation }}"
state: present
tags:
- mysql

# Remove all anonymous user accounts
- name: Remove all anonymous user accounts
mysql_user:
name: ''
host_all: yes
state: absent
tags:
- mysql

# Create "root" with all privileges
- name: Create "root" user with all privileges
mysql_user:
name: "{{ mysql_user }}"
password: "{{ mysql_password }}"
priv: '*.*:ALL'
state: present
tags:
- mysql

# sudo cp provisioning/common/templates/mysql.conf.j2 /etc/mysql/mysql.conf.d/mysqld.cnf
- name: Create mysql configuration file
template:
src: mysql.conf.j2
dest: /etc/mysql/mysql.conf.d/mysqld.cnf
tags:
- mysql

# sudo cp provisioning/common/templates/mysql.php.j2 /srv/www/site*/mysql.php
- name: Create site index.php files
template:
src: mysql.php.j2
dest: "{{ web_root }}/{{ item }}/mysql.php"
with_items:
- site1
- site2
tags:
- mysql

provisioning/roles/common/tasks/nginx.yml


---
# This playbook contains nginx actions that will be run on "lemp" hosts.

# sudo apt-get install nginx (v.1.10.*)
- name: Install nginx
apt:
name: nginx=1.10.*
state: present
update_cache: yes
tags:
- nginx

# sudo mkdir /srv/www/lemp
- name: Create web root
file:
path: "{{ web_root }}/{{ item }}"
state: directory
with_items:
- site1
- site2
tags:
- nginx

# sudo cp provisioning/common/templates/site*.vhost.j2 /etc/nginx/sites-available/site*
- name: Create site configuration files
template:
src: "{{ item }}.vhost.j2"
dest: "{{ nginx_root }}/sites-available/{{ item }}"
with_items:
- site1
- site2
tags:
- nginx

# sudo ln -s /etc/nginx/sites-available/site* /etc/nginx/sites-enabled/site*
- name: Create site symbolic link
file:
src: "{{ nginx_root }}/sites-available/{{ item }}"
dest: "{{ nginx_root }}/sites-enabled/{{ item }}"
state: link
with_items:
- site1
- site2
tags:
- nginx

# sudo nano /etc/hosts
- name: Configuring hosts file
lineinfile:
path: /etc/hosts
line: "{{ localhost }} {{ item }}.com"
with_items:
- site1
- site2
tags:
- nginx

provisioning/roles/common/tasks/php.yml


---
# This playbook contains php-fpm actions that will be run on "lemp" hosts.

# sudo cp provisioning/common/templates/site*.php.j2 /srv/www/site*/index.php
- name: Create site index.php files
template:
src: "{{ item }}.php.j2"
dest: "{{ web_root }}/{{ item }}/index.php"
with_items:
- site1
- site2
tags:
- php

# sudo apt-get install software-properties-common
- name: Install software-properties-common
apt:
name: software-properties-common
state: present
tags:
- php

# sudo add-apt-repository ppa:ondrej/php
- name: Add repository into sources list
apt_repository:
repo: ppa:ondrej/php
state: present
update_cache: yes
tags:
- php

# sudo apt-get install *
- name: Install PHP and common modules
apt:
name: "{{ item }}"
state: present
with_items:
- php7.1
- php7.1-cli
- php7.1-common
- php7.1-json
- php7.1-opcache
- php7.1-mysql
- php7.1-mbstring
- php7.1-mcrypt
- php7.1-zip
- php7.1-fpm
tags:
- php

# sudo cp provisioning/common/templates/php.conf.j2 /etc/php/7.1/fpm/pool.d/www.conf
- name: Create php configuration file
template:
src: php.conf.j2
dest: /etc/php/7.1/fpm/pool.d/www.conf
tags:
- php

provisioning/roles/common/tasks/utility.yml


---
# This playbook contains utility actions that will be run on "lemp" hosts.

# sudo apt-get install nano
- name: Install nano
apt:
name: nano
state: present
update_cache: yes
tags:
- nano

provisioning/roles/common/templates/mysql.conf.j2


[mysqld_safe]
socket = /var/run/mysqld/mysqld.sock
nice = 0

[mysqld]
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = {{ mysql_port }}
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql

skip-external-locking

bind-address = {{ localhost }}

key_buffer_size = 16M
max_allowed_packet = 16M
thread_stack = 192K
thread_cache_size = 8

myisam-recover-options = BACKUP

query_cache_limit = 1M
query_cache_size = 16M

log_error = /var/log/mysql/error.log

expire_logs_days = 10
max_binlog_size = 100M

provisioning/roles/common/templates/mysql.php.j2


<?php

if (!$conn = mysqli_connect("{{ localhost }}", "{{ mysql_user }}", "{{ mysql_password }}")) {
exit('Connection failed: '.mysqli_connect_error().PHP_EOL);
}

echo 'Successful database connection!'.PHP_EOL;

provisioning/roles/common/templates/ntp.conf.j2


driftfile /var/lib/ntp/drift

# Specify UK NTP servers.
server 0.uk.pool.ntp.org
server 1.uk.pool.ntp.org
server 2.uk.pool.ntp.org
server 3.uk.pool.ntp.org

# Use Ubuntu's NTP server as a fallback.
server ntp.ubuntu.com

# Local users may obtain data from NTP servers.
restrict {{ localhost }}
restrict ::1

provisioning/roles/common/templates/php.conf.j2


[www]
user = www-data
group = www-data

listen = /run/php/php7.1-fpm.sock
listen.owner = www-data
listen.group = www-data

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

provisioning/roles/common/templates/site1.php.j2


<?php

echo 'Site 1'.PHP_EOL;

provisioning/roles/common/templates/site1.vhost.j2


server {
listen 80;
listen [::]:80;

server_name site1.com;
root /srv/www/site1;
index index.php index.html;

# Pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

# Deny access to .htaccess files
location ~ /\.ht {
deny all;
}

# Return 404 for all other files not matching the .php files
location / {
try_files $uri $uri/ =404;
}

error_log /var/log/nginx/site1_error.log;
access_log /var/log/nginx/site1_access.log;
}

provisioning/roles/common/templates/site2.php.j2


<?php

echo 'Site 2'.PHP_EOL;

provisioning/roles/common/templates/site2.vhost.j2


server {
listen 80;
listen [::]:80;

server_name site2.com;
root /srv/www/site2;
index index.php index.html;

# Pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

# Deny access to .htaccess files
location ~ /\.ht {
deny all;
}

# Return 404 for all other files not matching the .php files
location / {
try_files $uri $uri/ =404;
}

error_log /var/log/nginx/site2_error.log;
access_log /var/log/nginx/site2_access.log;
}

Build


$ vagrant up
...
PLAY [Apply common configuration to "lemp" host] *******************************
TASK [Gathering Facts] *********************************************************
TASK [common : Import begin tasks] *********************************************
TASK [common : Update apt packages] ********************************************
TASK [common : Install GB locale] **********************************************
TASK [common : Set locale] *****************************************************
TASK [common : Set time zone to Europe/London] *********************************
TASK [common : Install ntp] ****************************************************
TASK [common : Configure ntp file and restart] *********************************
TASK [common : Import utility tasks] *******************************************
TASK [common : Install nano] ***************************************************
TASK [common : Import nginx tasks] *********************************************
TASK [common : Install nginx] **************************************************
TASK [common : Create web root] ************************************************
TASK [common : Create site configuration files] ********************************
TASK [common : Create site symbolic link] **************************************
TASK [common : Configuring hosts file] *****************************************
TASK [common : Import mysql tasks] *********************************************
TASK [common : Install mysql and packages] *************************************
TASK [common : Create a new database] ******************************************
TASK [common : Remove all anonymous user accounts] *****************************
TASK [common : Create "root" user with all privileges] *************************
TASK [common : Create mysql configuration file] ********************************
TASK [common : Create site index.php files] ************************************
TASK [common : Import php tasks] ***********************************************
TASK [common : Create site index.php files] ************************************
TASK [common : Install software-properties-common] *****************************
TASK [common : Add repository into sources list] *******************************
TASK [common : Install PHP and common modules] *********************************
TASK [common : Create php configuration file] **********************************
TASK [common : Restart services] ***********************************************
TASK [common : Import end tasks] ***********************************************
TASK [common : Remove useless apt packages from the cache] *********************
TASK [common : Remove dependencies that are no longer required] ****************
RUNNING HANDLER [common : Restart ntp] *****************************************
RUNNING HANDLER [common : Update locale] ***************************************
RUNNING HANDLER [common : Restart nginx] ***************************************
RUNNING HANDLER [common : Restart mysql] ***************************************
RUNNING HANDLER [common : Restart php] *****************************************
PLAY RECAP *********************************************************************
lemp : ok=38 changed=24 unreachable=0 failed=0

Results


System


$ timedatectl
Local time: Sun 2018-03-14 10:51:48 GMT
Universal time: Sun 2018-03-14 10:51:48 UTC
RTC time: Sun 2018-03-14 10:51:46
Time zone: Europe/London (GMT, +0000) # This was "Etc/UTC (UTC, +0000)" before
Network time on: yes
NTP synchronized: yes # This was "no" before
RTC in local TZ: no

$ locale
LANG=en_GB.UTF-8 # This was "en_US.UTF-8" before
LANGUAGE=en_GB:en # This was empty before
LC_CTYPE="en_GB.UTF-8"
LC_NUMERIC="en_GB.UTF-8"
LC_TIME="en_GB.UTF-8"
LC_COLLATE="en_GB.UTF-8"
LC_MONETARY="en_GB.UTF-8"
LC_MESSAGES="en_GB.UTF-8"
LC_PAPER="en_GB.UTF-8"
LC_NAME="en_GB.UTF-8"
LC_ADDRESS="en_GB.UTF-8"
LC_TELEPHONE="en_GB.UTF-8"
LC_MEASUREMENT="en_GB.UTF-8"
LC_IDENTIFICATION="en_GB.UTF-8"
LC_ALL=en_GB.UTF-8 # This was empty before

ubuntu@lemp:~$ php -v
PHP 7.1.15-1+ubuntu16.04.1+deb.sury.org+2 (cli) (built: Mar 6 2018 11:10:13) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.1.15-1+ubuntu16.04.1+deb.sury.org+2, Copyright (c) 1999-2018, by Zend Technologies

ubuntu@lemp:~$ nginx -v
nginx version: nginx/1.10.3 (Ubuntu)

ubuntu@lemp:~$ mysql -uroot -p
Enter password:
Server version: 5.7.21-0ubuntu0.16.04.1 (Ubuntu)

Results within the Vagrant is listed below. Any non existent URL access will produce default "404 Not Found" fallback page.


$ curl site1.com            # Serves site1.com from /srv/www/site2
$ curl site1.com/mysql.php # Serves site1.com from /srv/www/site2

$ curl site2.com # Serves site2.com from /srv/www/site1
$ curl site2.com/mysql.php # Serves site2.com from /srv/www/site1

$ curl 127.0.0.1 # Serves localhost from /var/www/html (fallback)
$ curl lemp # Serves localhost from /var/www/html (fallback)
$ curl localhost # Serves localhost from /var/www/html (fallback)

If you go to http://192.168.99.31 within the host OS, you will see default Nginx page which is served from /var/www/html. Any non existent URL access will produce default "404 Not Found" fallback page.