Stemar Vagrant boxes

A system for concurrently running Vagrant boxes with flexible configuration

64bit open-source Linux distribution Oracle VirtualBox virtual machine with virtual 64GB HDD, 3GB RAM from Bento.
Bash provisioning script that adds a LAMP stack and custom configuration.
Directory tree of multiple Vagrant virtual machines to run and manage them independently and concurrently.

Rocky Linux logo

Rocky Linux

Rocky Linux is an open-source enterprise operating system designed to be 100% compatible with Red Hat Enterprise Linux® (RHEL).

Rocky Linux started when Red Hat announced that they would discontinue development of CentOS in December 2020.

Rocky Linux 9

This Vagrant box contains stable versions:

Rocky Linux Apache MariaDB
9.1 2.4 10.6
PHP Ruby Python
8.2 3.1.3 3.9
Rocky Linux 8

This Vagrant box contains stable versions:

Rocky Linux Apache MariaDB
8.7 2.4 10.6
PHP Ruby Python
8.2 3.1.3 3.9
CentOS logo

CentOS

CentOS is an open-source enterprise operating system compatible with Red Hat Enterprise Linux® (RHEL).

CentOS is discontinued; it was available from May 2004 to November 2021.

CentOS 8

This Vagrant box contains stable versions:

CentOS Apache MariaDB PHP Python
8.5 2.4 10.6 7.4 2.7
3.6
CentOS 7

This Vagrant box contains stable versions:

CentOS Apache MariaDB PHP Python
7.9 2.4 10.6 7.4 2.7
3.6
CentOS 6

This Vagrant box contains stable versions:

CentOS Apache MySQL PHP Python
6.10 2.2 5.1.73 5.3.3 2.6
Debian logo

Debian

Debian is a GNU/Linux distribution composed of free and open-source software.

Debian 12

This Vagrant box contains stable versions:

Debian Apache MariaDB
12 2.4 10.11
PHP Python Node.js
8.2 3.11 18.13
Debian 11

This Vagrant box contains stable versions:

Debian Apache MariaDB PHP Python
11 2.4 10.6 8.2 3.9
Debian 10

This Vagrant box contains stable versions:

Debian Apache MariaDB PHP Python
10 2.4 10.6 8.1 2.7
3.7
Ubuntu logo

Ubuntu

Ubuntu is a Linux distribution based on Debian and composed mostly of free and open-source software.

Ubuntu 22.04

This Vagrant box contains stable versions:

Ubuntu Apache MariaDB PHP Python
22.04 2.4 10.6 8.2 3.10
Ubuntu 20.04

This Vagrant box contains stable versions:

Ubuntu Apache MariaDB PHP Python
20.04 2.4 10.6 7.4 3.8
Ubuntu 18.04

This Vagrant box contains stable versions:

Ubuntu Apache MariaDB PHP Python
18.04 2.4 10.6 7.4 2.7
3.6

Provisioning in Bash

When you add or remove Linux OS packages to your desktop or laptop, you must use Bash commands to execute their download and installation.

If you have to add or remove packages on a Linux server, you must use Bash commands too.

So the provisioning script provision.sh is written in Bash for familiarity.

No need to learn Chef, Puppet or Ansible.

No need to overengineer the provisioning script.

The Bash commands in provision.sh can even be copied and pasted as is in another machine; not Chef, Puppet or Ansible commands.

Flexible configuration

You can add, edit and remove OS packages and repositories from the provision.sh file.

You can add, edit and remove settings.yaml values. The settings.yaml values feeds Vagrantfile that builds the virtual machine.

Multiple Vagrant boxes

No need to create one global Vagrantfile with multiple boxes in it, as explained here: Multi-Machine.

If you clone many boxes in the same parent directory ~/VM:

mkdir -p ~/VM && cd $_
git clone --depth=1 https://github.com/stemar/vagrant-debian-11.git debian-11
git clone --depth=1 https://github.com/stemar/vagrant-rockylinux-8.git rockylinux-8
git clone --depth=1 https://github.com/stemar/vagrant-ubuntu-20-04.git ubuntu-20-04
~/VM
├── debian-11
│   ├── config
│   │   ├── MariaDB.list
│   │   ├── adminer.conf
│   │   ├── adminer.php
│   │   ├── bash_aliases
│   │   ├── localhost.conf
│   │   ├── php.ini.htaccess
│   │   ├── php.list
│   │   └── virtualhost.conf
│   ├── LICENSE
│   ├── README.md
│   ├── Vagrantfile
│   ├── provision.sh
│   └── settings.yaml
├── rockylinux-8
│   ├── config
│   │   ├── 00-mpm.conf
│   │   ├── MariaDB.repo
│   │   ├── adminer.conf
│   │   ├── adminer.php
│   │   ├── bashrc
│   │   ├── gemrc
│   │   ├── localhost.conf
│   │   ├── localhost.crt
│   │   ├── localhost.key
│   │   ├── php.ini.htaccess
│   │   └── virtualhost.conf
│   ├── LICENSE
│   ├── README.md
│   ├── Vagrantfile
│   ├── provision.sh
│   └── settings.yaml
└── ubuntu-20-04
    ├── config
    │   ├── MariaDB.list
    │   ├── adminer.conf
    │   ├── adminer.php
    │   ├── bash_aliases
    │   ├── localhost.conf
    │   ├── php.ini.htaccess
    │   └── virtualhost.conf
    ├── LICENSE
    ├── README.md
    ├── Vagrantfile
    ├── provision.sh
    └── settings.yaml

You can vagrant up each of them in their own tab in your terminal, but before they can run concurrently, modify their :host port in settings.yaml.

For example:

  rockylinux-8 debian-11 ubuntu-20-04
SSH 2200 2201 2202
HTTP 8000 8001 8002
MySQL 33060 33061 33062

Each box can be booted or halted, modified, provisioned independently.

When you provision a box for the first time, the .vagrant directory will be created under its parent. It will not be created globally for all boxes.

~/VM
...
└── ubuntu-20-04
    ├── .vagrant
    ├── config
    └── ...

Example settings.yaml:

---
:machine:
  # https://app.vagrantup.com/bento/boxes/rockylinux-8
  :box: bento/rockylinux-8 # 64GB HDD
  :memory: 3072 # 3GB RAM
  :cpus: 1
  :hostname: rockylinux-8
  :timezone: Canada/Pacific

:forwarded_ports:
# SSH
- :id: ssh
  :host: 2200
  :guest: 22
# HTTP
- :host: 8000
  :guest: 80
# MySQL
- :host: 33060
  :guest: 3306

:synced_folder:
  :host: ~/Code
  :guest: /home/vagrant/Code

:copy_files:
- :source: ~/.ssh
  :destination: /home/vagrant/.ssh
- :source: ~/.gitconfig
  :destination: /home/vagrant/.gitconfig

:php_error_reporting: E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT

# :ruby_version: 3.1.3
...

settings.yaml

This file feeds values to Vagrantfile so you should edit both files at the same time.

:machine

:machine has the values for some Bash settings, virtual hardware, and the box to download.

:forwarded_ports

:forwarded_ports has arrays of host/guest values: the :host value is the port of the physical machine while the :guest value is the port of the virtual machine.

To avoid port collision, you must change these port values before running more than one Vagrant box at the same time.

:synced_folder

:synced_folder has paths of the synchronized directories: the :host value is the path of your projects on the physical machine while the :guest value is the path of your projects inside the virtual machine.

:copy_files

:copy_files has arrays of source/destination values: the :source value is the path of the file you want to copy from the physical machine while the :destination value is the path of the file you want to copy to the virtual machine.

These files are not synchronized; they are copied at provisioning only.

Here we want to clone configuration files so that the same configuration applies on the host and the guest, instead of having different configurations for Git and Subversion in and out of the VM.

:php_error_reporting

:php_error_reporting is the PHP error reporting value in PHP constants which feeds the config/php.ini.htaccess file to display PHP errors on a page in development.

:ruby_version

:ruby_version is present if the box contains Ruby. Uncomment the line to install Ruby.

Vagrantfile

This Ruby file builds the Vagrant box from the values of settings.yaml.

The VM is built from a Bento VirtualBox download.

Some Bash variables are passed to the provision.sh script.

This page explains the configuration settings: Vagrant Machine Settings

Example Vagrantfile:

require 'yaml'
dir = File.join(File.dirname(File.expand_path(__FILE__)))
settings = YAML.load_file("#{dir}/settings.yaml")

Vagrant.require_version ">= 2.0.0"
Vagrant.configure("2") do |config|
  config.vm.define settings[:machine][:hostname]

  config.vm.box = settings[:machine][:box]

  config.vm.provider "virtualbox" do |vb|
    vb.name   = settings[:machine][:hostname]
    vb.memory = settings[:machine][:memory]
    vb.cpus   = settings[:machine][:cpus]
  end

  config.vm.hostname = settings[:machine][:hostname]

  settings[:forwarded_ports].each do |port_options|
    config.vm.network :forwarded_port, **port_options
  end

  config.vm.synced_folder settings[:synced_folder][:host], settings[:synced_folder][:guest], owner: "vagrant", group: "vagrant"

  settings[:copy_files].each do |file_options|
    config.vm.provision :file, **file_options
  end unless settings[:copy_files].nil?

  config.vm.provision :shell, path: "provision.sh", env: {
    "FORWARDED_PORT_80"   => settings[:forwarded_ports].find{|port| port[:guest] == 80}[:host],
    "GUEST_SYNCED_FOLDER" => settings[:synced_folder][:guest],
    "PHP_ERROR_REPORTING" => settings[:php_error_reporting],
    "TIMEZONE"            => settings[:machine][:timezone]
  }
end

Example php.ini.htaccess:

# http://php.net/manual/en/configuration.changes.php
# http://php.net/manual/en/ini.list.php

# Development environment error settings
php_flag display_startup_errors on
php_flag display_errors on
php_flag html_errors on
php_flag ignore_repeated_errors off
php_flag ignore_repeated_source off
php_flag report_memleaks on
php_flag track_errors on
php_value docref_root 0
php_value docref_ext 0
php_flag log_errors off
php_value log_errors_max_len 0
php_value error_reporting PHP_ERROR_REPORTING_INT

# Application settings
php_value upload_max_filesize 512M
php_value post_max_size 512M
php_value memory_limit 512M

php.ini.htaccess

This file will be copied to /var/www/.htaccess to set php.ini settings for a development environment.

PHP in this VM is run as an Apache module, not as FastCGI. So we use .htaccess to set php.ini settings.

It allows PHP to display errors on the page, turn off error logging, set error_reporting to the value from settings.yaml, and set decent memory size values.

virtualhost.conf

You can create DocumentRoot VirtualHost blocks one by one for all the projects you want Apache to serve.

But you could use a VirtualDocumentRoot VirtualHost block to dynamically serve any project that have a common name for their public directory.

In the example on the right:

  • http://example.com.localhost:8000 is served from its public directory.
  • http://domain.com.localhost:8000 is served from its www directory.

The order of all DocumentRoot VirtualHost blocks above a VirtualDocumentRoot VirtualHost block is essential for Apache.

DocumentRoot

Create any number of DocumentRoot VirtualHost blocks in the virtualhost.conf file with the GUEST_SYNCED_FOLDER variable for the VM's projects location.

This page explains the DocumentRoot Directive and its options: Apache DocumentRoot Directive

VirtualDocumentRoot

  • If you name your projects directory (GUEST_SYNCED_FOLDER) /home/vagrant/Code, and each project is either a domain name or a repository name; you could call the projects directory /home/vagrant/Projects or any name you like.
  • If the public directory to be served by the VM's web server is called www; you could call it public or any name you like.
~/Code
├── domain.com
│   ├── app
│   │   └── ...
│   ├── vendor
│   │   └── ...
│   ├── www
│   │   └── ...
│   └── ...
└── app.example.org
    ├── app
    │   └── ...
    ├── bin
    │   └── ...
    ├── vendor
    │   └── ...
    └── www
        └── ...
~/Projects
├── bookkeeping_app
│   ├── app
│   │   └── ...
│   ├── bin
│   │   └── ...
│   ├── config
│   │   └── ...
│   ├── public
│   │   └── ...
│   └── ...
└── management_app
    ├── templates
    │   └── ...
    ├── vendor
    │   └── ...
    └── public
        └── ...

Using ServerAlias *.localhost and
VirtualDocumentRoot /home/vagrant/Code/%-2+/www
allow Apache to serve any of the following outside the VM, as long as these projects all have www as its public directory:

  • http://domain.com.localhost:8000
  • http://app.example.org.localhost:8000
  • http://any.number.of.sub.domains.example.com.localhost:8000

If you want to change the ServerAlias to ServerAlias *.dev, then:

  • http://domain.com.dev:8000
  • http://app.example.org.dev:8000
  • http://any.number.of.sub.domains.example.com.dev:8000

It's good to have a ServerAlias value of star dot something to avoid confusing the browser's cache to serve a public site vs. a VM local site. Keeping the standard name *.localhost makes it clear in the browser's address bar.

Keeping the port visible in the URI is also useful when you have two VMs running and you want to serve two projects from two different ports in two browser tabs.

No need to add entries in /etc/hosts.

The second directory tree example above would use VirtualDocumentRoot /home/vagrant/Code/%-2+/public since all its projects use public as their public directory.

This page explains the VirtualDocumentRoot Directive and its options: Apache Module mod_vhost_alias

Example virtualhost.conf:

# ------------------
# Using DocumentRoot
# ------------------
# https://httpd.apache.org/docs/2.4/mod/core.html#documentroot
#
# GUEST_SYNCED_FOLDER/
# └── example.com/
#     ├── app/
#     │   └── ...
#     └── public/  <= DocumentRoot
#         └── ...
#
<VirtualHost *:80>
    # http://example.com.localhost:FORWARDED_PORT_80 => DocumentRoot
    ServerName example.com.localhost
    DocumentRoot GUEST_SYNCED_FOLDER/example.com/public

    <Directory GUEST_SYNCED_FOLDER/example.com>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog /var/log/httpd/error_log
    CustomLog /var/log/httpd/access_log combined
</VirtualHost>

# -------------------------
# Using VirtualDocumentRoot
# -------------------------
# https://httpd.apache.org/docs/2.4/mod/mod_vhost_alias.html
#
# GUEST_SYNCED_FOLDER/
# └── domain.com/
#     ├── app/
#     │   └── ...
#     └── www/     <= VirtualDocumentRoot
#         └── ...
#
<VirtualHost *:80>
    # http://domain.com.localhost:FORWARDED_PORT_80 => VirtualDocumentRoot
    ServerAlias *.localhost
    VirtualDocumentRoot GUEST_SYNCED_FOLDER/%-2+/www
    UseCanonicalName Off

    <Directory GUEST_SYNCED_FOLDER>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog /var/log/httpd/error_log
    CustomLog /var/log/httpd/access_log combined
</VirtualHost>

bashrc | bash_aliases

Add or edit any lines in these files.

Example bash_aliases (Debian-based OS):

export HISTCONTROL=ignoreboth:erasedups
alias ll="ls -lAFh --group-directories-first"
alias nano="nano -cw"

Example bashrc (RHEL-based OS):

export HISTCONTROL=ignoreboth:erasedups
export PS1="[\u@\H \w]$ "
alias ll="ls -lAFh --group-directories-first"
alias nano="nano -cw"

# Make rbenv load automatically
export RBENV_ROOT="${HOME}/.rbenv"
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init -)"

MariaDB and Adminer

MariaDB and Adminer are both set with no password for username root so you can avoid writing a password a zillion times through development.

To change the MariaDB version, go to the Download MariaDB Server page and create a new repository configuration, then overwrite config/MariaDB.repo or config/MariaDB.list depending on the OS.

Adminer is served at http://localhost:8000/adminer.php. It comes with plugins: login-password-less, dump-json and pretty-json-column.