In Setting up Vagrant I showed you Hello world project with Vagrant. Now I am going to show you how to set up a LAMP environment.

Provisioning

Provisioning is a way to install and configure software inside a virtual machine. Simplest usable way of doing so is using a shell script. That is what I am going to do.

However, that is not the only option. You can use more advanced automatic configuration and orchestration tools such as Ansible or Puppet if you are proficient with them.

Configuring Vagrant

First, we take a look at Vagrantfile. The minimum configuration contains box definition.

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

With a configuration like this, our only way of accessing VM is using SSH. Therefore, we create a private network, which allows host-only access to the machine using a specific IP.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
  config.vm.network "private_network", ip: "192.168.33.10"
end

Now we could manually edit /etc/hosts and map 192.168.33.10 address to a hostname project.v.martinvana.com. But life is too short to do things manually.

Therefore we install a vagrant-hostmanager plugin.

$ vagrant plugin install vagrant-hostmanager

Now we can let DHCP select an IP address and we set up a custom IP resolver.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
  config.vm.network "private_network", type: "dhcp"
  config.vm.hostname = "project.v.martinvana.com"

  if Vagrant.has_plugin?('vagrant-hostmanager')
		config.hostmanager.enabled = true
		config.hostmanager.manage_host = true
		config.hostmanager.ip_resolver = proc do
			`vagrant ssh -c "hostname -I"`.split()[1]
		end
	end
end

Since we are using a Virtualbox it is reasonable to limit resources it can consume. Allow using 1GB RAM and 1CPU which can use up to 50% of a single host CPU.

Vagrant.configure("2") do |config|
  # ...
  config.vm.provider "virtualbox" do |vb|
    vb.customize ["modifyvm", :id, "--cpuexecutioncap", "50"]
    vb.memory = 1024
    vb.cpus = 1
  end
  # ...
end

Finally, we got to the provisioning part of a configuration. Folder vagrant in our project is going to contain all provisioning files.

We could define an arbitrary number of provisioning scripts. Therefore I set up three:

  • install.sh – Install and configure software
  • load.sh – Actions to do on vagrant up
  • unprivileged.sh – Actions to do as a unprivileged user
Vagrant.configure("2") do |config|
  # ...
  config.vm.provision :shell, :path => "vagrant/install.sh"
  config.vm.provision :shell, :path => "vagrant/load.sh", run: "always"
  config.vm.provision :shell, :path => "vagrant/unprivileged.sh", privileged: false
  # ...
end

Content of install.sh is following:

#!/usr/bin/env bash

echo "----- Provision: Setting Prague timezone ..."
ln -sf /usr/share/zoneinfo/Europe/Prague /etc/localtime

echo "----- Provision: Add repositories ..."
# ...

echo "----- Provision: Re-synchronize the package index files from their sources ..."
apt-get update

echo "----- Provision: Install the newest versions of all packages currently installed on the system ..."
apt-get upgrade -y

# Available configurations
bash /vagrant/vagrant/apache/apache.sh
bash /vagrant/vagrant/mysql/mysql.sh
bash /vagrant/vagrant/php/php.sh
bash /vagrant/vagrant/utils/utils.sh

# Cleanup
apt-get -y autoremove

As you can see, I am not creating one huge install script but rather several small ones. load.sh and unprivileged.sh files are empty for now.

Apache

To set up Apache we need to configure a virtual host.

# File: vagrant/apache/sites-available/project.conf

<VirtualHost *:80>
  ServerAdmin webmaster@localhost
  DocumentRoot /var/www/www

  LogLevel warn

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# File: vagrant/apache/conf-available/project.conf

<Directory /var/www/>
  Options FollowSymLinks
  AllowOverride All
</Directory>

First, we install Apache, then we create a symbolic link to shared folder /vagrant, and finally, we configure a virtual host.

#!/usr/bin/env bash

# File: vagrant/apache/apache.sh

echo "----- Provision: Installing apache ..."
apt-get install -y apache2

echo "----- Provision: Setup /var/www to point to /vagrant ..."
if ! [ -L "/var/www" ]; then
	rm -rf "/var/www"
	ln -fs "/vagrant" "/var/www"
fi

# Apache / Virtual Host Setup
echo "----- Provision: Install Apache configurations ..."
rm -rf /etc/apache2/sites-enabled/*
if ! [ -L "/etc/apache2/sites-available" ]; then
	if ! [ -L "/etc/apache2/sites-available/project.conf" ]; then
		ln -s "/vagrant/vagrant/apache/sites-available/project.conf" "/etc/apache2/sites-available/project.conf"
	fi
	a2ensite -q project.conf
fi

if ! [ -L "/etc/apache2/conf-available/project.conf" ]; then
	rm -f "/etc/apache2/conf-available/project.conf"
	ln -s "/vagrant/vagrant/apache/conf-available/project.conf" "/etc/apache2/conf-available/project.conf"
fi
a2enconf -q project.conf

To verify that everything went as expected we create a HTML file.

$ echo "Hello world!" > www/index.html

Now we can view the page at http://project.v.martinvana.com/.

The last thing we do is to make sure we restart Apache on vagrant up in order to server configuration file to take effect.

#!/usr/bin/env bash

# File: vagrant/load.sh

echo "----- Provision: Restarting Apache ..."
service apache2 restart

PHP

To install PHP we first have to add PPA with PHP 7.1.

# File: vagrant/install.sh

# ...
echo "----- Provision: Add repositories ..."
add-apt-repository ppa:ondrej/php

# ...

Now we can install PHP, its extensions, and Composer in a similar fashion as in the case of Apache.

#!/usr/bin/env bash

# File: vagrant/php/php.sh

echo "----- Provision: Installing php ..."
apt-get install -y \
        php7.1 \
        php7.1-xdebug \
        php7.1-zip \
        php7.1-mysql \
        libapache2-mod-php7.1

echo "----- Provision: Installing composer ..."
EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig)
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');")

if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
then
  >&2 echo 'ERROR: Invalid installer signature'
else
  php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer
fi

rm composer-setup.php

To verify that everything went as expected we create a PHP file.

$ echo $'<?php\nphpinfo();' > www/info.php

At http://project.v.martinvana.com/info.php we can view the PHP information page.

MySQL

The only thing we have to take care of during MySQL installation is setting the root password.

#!/usr/bin/env bash

# File: vagrant/mysql/mysql.sh

echo "----- Provision: Installing mysql ..."
# Username: root
# Password: root
debconf-set-selections <<< 'mysql-server mysql-server/root_password password root'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password root'

apt-get install -y \
        mysql-server

To verify MySQL (and also Composer) installation I created a simple project with Adminer database management tool.

{
    "name": "vanam/vagrant-kickstarter",
    "type": "project",
    "require": {
        "php": ">=7.1.0",
        "ext-mysql": "*",
        "ext-zip": "*",

        "dg/adminer-custom": "^1.9"
    }
}

Now you should be able to log in as root user.

With composer project in place, we also want to automate dependency updating. For this, we use the unprivileged.sh file.

#!/usr/bin/env bash

echo "----- Provision: Moving to '/vagrant' directory ..."
cd "/vagrant"

echo "----- Provision: Installing composer dependencies ..."
composer install --no-interaction

Pitfalls

  • You need a stable internet connection during provisioning.
  • Until you call vagrant up --provision or vagrant provision changes will not be applied.
  • Provisioning files should be idempotent (have the same effect if run multiple times).
  • Every configuration change must be written down in a script otherwise it will be lost.
  • Installation scripts might fail. For advanced workflows use configuration and orchestration tools such as Ansible or Puppet.

Conclusion

As you can see, configuring a first (LAMP) virtual machine is not hard if you have previous knowledge of GNU/Linux systems. There is minimum Vagrant configuration and the rest is just a system configuration.

Full source code is available on Github.


Sources