Setting up a LAMP development environment in Vagrant

I’ve been an avid MAMP user for many years before I moved to Vagrant. MAMP always did a pretty good job for a basic PHP/MySQL setup despite some of the downsides:

These disadvantages can cause annoyances but they are not insurmountable. It’s when you need more advanced technologies such as MongoDB, Elasticsearch, Redis, Kafka etc. that things start getting very hard to manage locally.

That’s where Vagrant comes into play…

What is Vagrant?

Vagrant lets you create and run Virtual Machines and make them directly accessible from your local system. Tools like MAMP install everything locally and try to get it to work as good as possible on you system, while Vagrant simply gives you a real Linux machine that you can configure just like you’d do with your production server.

Some of the many advantages of working with such environments:

Configuration of these VMs is automated by what are called provisioners. In this beginners guide I’ll show you the easiest way to get going by using the shell provisioning strategy. As the name suggests, this strategy builds your VM based on a series of self-written shell scripts.

I do want to mention that some knowledge of Linux commands might come in handy, but is not required. I won’t always elaborate on every command so if you’d like to know what each specific command does you can always use Google or simply ask me. I do explain some of the basic stuff just in case you’ve never set up a web server of your own.

Creating your Virtual Machine

Enough of the boring, theoretical stuff, let’s start with the fun part … 🙂

Download and install Vagrant and VirtualBox and create a new project directory somewhere on your system. Within that directory create a file called Vagrantfile:

Vagrant.configure("2") do |config|    config.vm.box = "hashicorp/bionic64"
end

Then, run:

vagrant up

You’ll see a whole lot is going on. Vagrant is now searching its box catalog for a box called hashicorp/bionic64, downloads it, and creates the VM on your system. You can see these boxes as VMs containing a certain operating system and possibly some pre-installed packages. The box we’ve chosen is the “official” Vagrant Ubuntu 18.04 64-bit box which is perfect for a local LAMP development environment.

If you now open VirtualBox, you’ll see a new machine was created:

Thanks to some hidden files in your project directory Vagrant now knows which folder is associated to which machine. It is therefore possible running multiple VMs together.

So it appears to be running, how can we test it? Simple! Run:

vagrant ssh

Cool! We’ve now ssh’ed our way into our Ubuntu machine!

By the way, VirtualBox is a so called provider for Vagrant, which is the software running your VM. There are a couple of others, but until now I haven’t found any reason to move away from VirtualBox.

Installing Apache

So all we have running now is a Linux VM. We could simply ssh to it and install our LAMP configuration manually, but someone else working on that project would then have to do the exact same thing. This wouldn’t really be much of an improvement compared to a local Mac or MAMP environment wouldn’t it?

So here’s where we’ll start using the shell script provisioners. Let’s start by installing Apache, and in the meantime also fine-tune our VM settings. Edit your Vagrantfile so it looks like this:

Vagrant.configure("2") do |config|
    config.vm.box = "hashicorp/bionic64"

    # Give our VM a name so we immediately know which box this is when opening VirtualBox, and spice up our VM's resources
    config.vm.provider "virtualbox" do |v|
        v.name = "Our amazing test project"
        v.memory = 4096
        v.cpus = 1
    end

    # Choose a custom IP so this doesn't collide with other Vagrant boxes
    config.vm.network "private_network", ip: "192.168.88.188"

    # Execute shell script(s)
    config.vm.provision :shell, path: "provision/components/apache.sh"
end

As you can see, we’ve added an apache.sh provisioner. We tell Vagrant to execute that script within our VM once it is created. Create provision/components/apache.sh:

#!/bin/bash

apt-get update
apt-get install -y apache2

Now we’ll need to restart the VM in order for the changes to be in effect. There are 3 different commands you can use for that. Let me briefly explain them so you know the difference:

vagrant reload Re-runs your Vagrantfile, so used for changes in your Vagrantfile
vagrant provision Re-runs your configured provisioners, so only used if you’ve changed any of them, or added new ones to the Vagrantfile
vagrant reload --provision Re-runs your Vagrantfile AND your provisioners, so used if there were changes in both files

Since we’ve changed our VM’s configuration and created a new provisioning file, we’ll need to run:

vagrant reload --provision

Let Vagrant do its magic, and once it’s finished, visit http://192.168.88.188/. Boom! You’re now looking at the Apache default page of your VM, which means it’s up and running!

The page you’re looking at is a simple index.html file that’s located within our VM in the /var/www/html directory, the so-called document root. This document root is the directory that’s available from the outside to your server.

Obviously we don’t want to develop directly into our VM’s document root using vim (or my. So we’ll need to find a way for our project files to be available within our VM. To do this, you’ll only need to add one line to your Vagrantfile, add it right above the Apache provisioning line:

config.vm.synced_folder ".", "/var/www", create: true, nfs: true, mount_options: ["actimeo=2"]

What this does is link our whole project folder to the /var/www/ directory on our VM. Now create a html directory and put an  index.html file in there:

<html>
<head>
    <title>Wassup</title>
</head>
<body>
    <p>How cool is this?</p>
</body>
</html>

Then, run:

vagrant reload

Let Vagrant do it’s things, refresh the page and … there it is! You are now looking at the page you just created.

Installing PHP

Time to install PHP and include some generally-used PHP libraries. Create a new php.sh file, don’t forget adding it to your Vagrantfile:

#!/bin/bash

sudo apt-get install software-properties-common
sudo LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get install -y php7.4 php7.4-bcmath php7.4-bz2 php7.4-cli php7.4-curl php7.4-intl php7.4-json php7.4-mbstring php7.4-opcache php7.4-soap php7.4-xml php7.4-xsl php7.4-zip libapache2-mod-php7.4

sudo service apache2 restart

Also add a phpinfo.php file to your html directory:

<?php
phpinfo();

Run:

vagrant provision

Once Vagrant is finished, visit http://192.168.88.188/phpinfo.php, which should successfully display the PHP info page! Easy, right?

One thing you’ll probably do in any new PHP setup is level up the max. memory usage, post size, and upload size. Again, we don’t want to do it manually, we want it to be configured correctly from the moment we clone our project and start up our VM. To do this, add these lines before sudo service apache2 restart in php.sh:

sed -i 's/max_execution_time = .*/max_execution_time = 60/' /etc/php/7.4/apache2/php.ini
sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/7.4/apache2/php.ini
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 1G/' /etc/php/7.4/apache2/php.ini
sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/7.4/apache2/php.ini

I guess this speaks for itself, what this does is find certain lines in php.ini and replace them with the values we want.

Run vagrant provision again and check your phpinfo.php file. Search for any of the 4 directives and you’ll see that the PHP settings have successfully been changed!

Installing MySQL

Next up is MySQL. Create a mysql.sh file:

#!/bin/bash

DBHOST=localhost
DBNAME=mydb
DBUSER=myuser
DBPASSWD=password

# Install MySQL
apt-get update

debconf-set-selections <<< "mysql-server mysql-server/root_password password $DBPASSWD"
debconf-set-selections <<< "mysql-server mysql-server/root_password_again password $DBPASSWD"

apt-get -y install mysql-server-5.7

# Create the database and grant privileges
CMD="mysql -uroot -p$DBPASSWD -e"

$CMD "CREATE DATABASE IF NOT EXISTS $DBNAME"
$CMD "GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'%' IDENTIFIED BY '$DBPASSWD';"
$CMD "FLUSH PRIVILEGES;"

# Allow remote access to the database
sudo sed -i "s/.*bind-address.*/bind-address = 0.0.0.0/" /etc/mysql/mysql.conf.d/mysqld.cnf

sudo service mysql restart

In order to use MySQL in PHP, we also can’t forget the PHP MySQL extension, so add php7.4-mysql to the long sudo apt-get install line in php.sh.

Run vagrant provision again and while Vagrant is doing it’s thing create a db.php file in your html folder:

<?php
$conn = mysqli_connect("localhost", "myuser", "password", "mydb");

if (!$conn) {
    die("Error: " . mysqli_connect_error());
}

echo "Connected!";

Visit http://192.168.88.188/db.php and if all went well you should be seeing the “Connected!” message.

Installing phpMyAdmin

PhpMyAdmin could always come in handy on a LAMP development environment, so create another script file called phpmyadmin.sh and add it to your Vagrantfile:

#!/bin/bash

sudo apt-get update

debconf-set-selections <<< "phpmyadmin phpmyadmin/dbconfig-install boolean true"
debconf-set-selections <<< "phpmyadmin phpmyadmin/app-password-confirm password $DBPASSWD"
debconf-set-selections <<< "phpmyadmin phpmyadmin/mysql/admin-pass password $DBPASSWD"
debconf-set-selections <<< "phpmyadmin phpmyadmin/mysql/app-pass password $DBPASSWD"
debconf-set-selections <<< "phpmyadmin phpmyadmin/reconfigure-webserver multiselect none"

sudo apt-get install -y phpmyadmin php-mbstring php-gettext

sudo phpenmod mbstring
sudo systemctl restart apache2

You’ve guessed it, run vagrant provision and visit http://192.168.88.188/phpmyadmin/, log in with myuser/password and there we go… easy as pie.

Accessing your database from outside the VM

Not everyone likes phpMyAdmin. To access your database with a GUI you’ll need to use a SSH connection. How to set this up depends on the software you’re using, but in general these are the things you’ll need to configure:

Host 127.0.0.1
Username myuser (which we’ve defined in mysql.sh)
Password password (which we’ve also defined in mysql.sh)
SSH Host 127.0.0.1
SSH User vagrant
SSH Key Can be found in this location in your project directory: /.vagrant/machines/default/virtualbox/private_key
SSH Port 2222

Setting up a domain name

We’re on a roll here! But I personally prefer working with real domain names instead of IP addresses. It’s much easier to remember but also necessary if your application or website should behave differently depending on the domain name (e.g. login.mydomain and www.mydomain), something we can’t achieve with a single IP address.

To get this working our Ubuntu server will need a custom Virtual Host file. Again, we’re not going to create it directly on our VM and make every user to the same thing, we’re going to automate this. Lets first create the Virtual Host file within our project: provision/config/apache/vhosts/mytestproject.local.com.conf

<VirtualHost *:80>
    ServerName mytestproject.local.com
    ServerAlias www.mytestproject.local.com

    DocumentRoot /var/www/html

    <Directory /var/www/html>
        # Allow .htaccess rewrite rules
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

Don’t worry too much about it if you don’t know what this file does. Simply put, it defines which folder is the document root when the server is being visited by the domain names we just invented (myproject.local.com and www.myproject.local.com). If you want multiple domain names available from different directories, you can always create more of these files.

Now, vhost files such as these need to be located in the /etc/apache2/sites-available directory on our VM, and each of those files need to be enabled. We do this by editing our apache.sh file:

#!/bin/bash

apt-get update
apt-get install -y apache2

# Copy the vhost config file
cp /var/www/provision/config/apache/vhosts/mytestproject.local.com.conf /etc/apache2/sites-available/mytestproject.local.com.conf

# Disable the default vhost file
a2dissite 000-default

# Enable our custom vhost file
a2ensite mytestproject.local.com.conf

# Restart for the changes to take effect
service apache2 restart

Once again run vagrant provision. While all scripts are being executed we can do the one manual action that needs to be done on our system to make this work. We’ll need to edit our local hosts file (on Mac, it’s /etc/hosts) and add:

192.168.88.188 mytestproject.local.com
192.168.88.188 www.mytestproject.local.com

This tells our system where to look when visiting these domain names.

Save the file, and once Vagrant is finished, visit http://www.mytestproject.local.com/ or http:/mytestproject.local.com/. Much wow! We are now accessing our server through our improvised domain names! 🤘

Installing Composer

Although you could run Composer locally, it wouldn’t be a very good idea. You don’t want Composer to base itself on your anyone’s local system. It might throw errors when certain PHP extensions are not installed, or it might install dependencies based on a wrong PHP version. That’s why we’ll always want to install/update dependencies from within our VM.

Installing Composer is pretty easy, simply add composer to the long sudo apt-get install list in php.sh and run vagrant provision.

Let’s test if it works by installing the Money library:

vagrant ssh
cd /var/www
composer require moneyphp/money

Works like a charm!

Installing Yarn

Chances are you’ll need to compile some assets so having Yarn installed on your VM can always come in handy. Create a yarn.sh file and add it to your Vagrantfile :

#!/bin/bash

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

sudo apt-get update && sudo apt-get install -y yarn

Run vagrant provision one final time. Test if it works the same way we did with Composer:

vagrant ssh
cd /var/www
yarn add --dev bootstrap

Works perfectly!

Now let’s add one last minor improvement. When ssh’ing to your VM you always need to run cd /www/var to go to your project directory. We can easily fix that by adding to the Vagrantfile:

config.vm.provision :shell, inline: "echo 'cd /var/www' >> /home/vagrant/.profile"

We’re finished! 🎉

That’s it! In relatively little time you’ve created a nice boilerplate that you can use in any PHP/MySQL project and you’ve now got a basic understanding on how to easily set up a local, re-usable environment. I’ve created a repository on GitHub containing the full example.

In future posts I’ll be talking about more advanced provisioners and features. So be sure to keep checking back 🙂.