Standardizing a development environment with Docker is challenging for a variety of reasons, the most common being that Docker simply is not the best of friends with Windows; particularly versions before Windows 10.

Regardless, when you’re running a modern development team with engineers and designers who have varying preferences of workstation, your environment needs to support them all.

In the past, we used Vagrant + Virtualbox + Ansible to build a virtual machine and we went from there. These days using Docker we can support an even deeper level of abstraction where you can run the environment natively, or in scenarios like Windows or on remote servers for demo purposes or what-not, we’ll run on a virtual machine or remote server.

All that to say we can now run docker+vagrant together to support this type of workflow. This way if you want to load up an environment on something where Docker isn’t supported well, you can run it through a virtual-machine with Vagrant, and otherwise just natively run docker-compose and be on your way.

First, our docker-compose:

# Build a containerized application for litwicki

version: '2.1'

services:

  db:
    image: mysql
    container_name: "litwicki_db"
    restart: always
    ports:
       - ${MYSQL_PORT}
    environment:
       MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
       MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
       MYSQL_DATABASE: ${MYSQL_DATABASE}
       MYSQL_USERNAME: ${MYSQL_USERNAME}
       MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    networks:
      litwicki:
        aliases:
          - db.litwicki.dev

  phpmyadmin:
    image: 'phpmyadmin/phpmyadmin'
    container_name: "phpmyadmin"
    restart: always
    ports:
       - '8080:80'
    depends_on:
      - db
    links:
      - db
    environment:
        MYSQL_USERNAME: ${MYSQL_USERNAME}
        MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
        PMA_HOST: db #this needs to match the container_name of the db container
        UPLOAD_SIZE: ${UPLOAD_SIZE}
    networks:
      litwicki:
        aliases:
          - phpmyadmin.litwicki.dev

  api:
    image: "litwicki/imagename"
    container_name: "litwicki_api"
    restart: always
    ports:
       - '80:80'
    volumes:
      - ./api:/var/www/litwicki:Z
    environment:
      VIRTUAL_HOST: ${API_HOSTNAME}
      VIRTUAL_PORT: ${API_PORT}
      PUID: "id -u $USER"
      PGID: "id -g $USER"
      PHP_UPLOAD_MAX_FILESIZE: ${UPLOAD_SIZE}
      PHP_POST_MAX_SIZE: ${UPLOAD_SIZE}
      PHP_MEM_LIMIT: ${PHP_MEM_LIMIT}
      ENABLE_XDEBUG: ${DEBUG}
      DISPLAY_PHP_ERRORS: ${DEBUG}
    depends_on:
      - db
    links:
      - db
    networks:
      litwicki:
        aliases:
          - api.litwicki.dev

networks:
  litwicki:
    driver: bridge

Now, our Vagrantfile:

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

ip_address = "10.1.1.20"
public_ip_address = ""
appname = "litwicki"
hostname = appname + ".dev"

# ========================================================================

Vagrant.configure(2) do |config|

  # For a complete reference, please see the online documentation at
  # https://docs.vagrantup.com.

  Vagrant.require_version ">= 1.9.0"

  # Set the name of the VM. See: http://stackoverflow.com/a/17864388/100134
  config.vm.define appname

  required_plugins = %w(vagrant-vbguest vagrant-docker-compose vagrant-hostsupdater)

  plugins_to_install = required_plugins.select { |plugin| not Vagrant.has_plugin? plugin }
  if not plugins_to_install.empty?
    puts "Installing plugins: #{plugins_to_install.join(' ')}"
    if system "vagrant plugin install #{plugins_to_install.join(' ')}"
      exec "vagrant #{ARGV.join(' ')}"
    else
      abort "Installation of one or more plugins has failed. Aborting."
    end
  end

  config.vm.box = "ubuntu/xenial64"
  
  # Networking configuration.
  config.vm.hostname = hostname
  config.vm.network :private_network,
    ip: ip_address,
    auto_network: ip_address == '0.0.0.0' && Vagrant.has_plugin?('vagrant-auto_network')

  unless public_ip_address.empty?
    config.vm.network :public_network,
      ip: public_ip_address != '0.0.0.0' ? public_ip_address : nil
  end

  # If a hostsfile manager plugin is installed, add all server names as aliases.
  aliases = ["api." + hostname, "admin." + hostname, "www." + hostname, "app." + hostname, "db." + hostname]
  if Vagrant.has_plugin?('vagrant-hostsupdater')
    config.hostsupdater.aliases = aliases
  elsif Vagrant.has_plugin?('vagrant-hostmanager')
    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.hostmanager.aliases = aliases
  end

  # from VVV
  # SSH Agent Forwarding
  #
  # Enable agent forwarding on vagrant ssh commands. This allows you to use ssh keys
  # on your host machine inside the guest. See the manual for `ssh-add`.

  config.ssh.forward_agent = true

  # Configuration options for the VirtualBox provider.
  config.vm.provider :virtualbox do |v|
    v.customize ["modifyvm", :id, "--memory", 1024]
    v.customize ["modifyvm", :id, "--cpus", 2]
    v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
    v.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
    v.customize ['modifyvm', :id, '--ioapic', 'on']
  end

  # NFS is waaaaay faster than the standard rsync, and this way we determine the
  # root directory structure to replicate production as well.
  # config.vm.synced_folder 'api/api', "/var/www/litwicki/api", :nfs => true

  compose_env = Hash.new
    if File.file?('.env')
        # read lines "var=value" into hash
        compose_env = Hash[*File.read('.env').split(/[=\n]+/)]
        # ignore keys (lines) starting with #
        compose_env.delete_if { |key, value| key.to_s.match(/^#.*/) }
    end

    config.vm.provision :docker
    config.vm.provision :docker_compose,
      project_name: appname,
      yml: "/vagrant/docker-compose.yml",
      env: compose_env,
      rebuild: true,
      run: "always"

  config.vm.post_up_message = "litwicki DEV ENVIRONMENT COMPLETE!"

end

Third, we’ll want to abstract our configuration values to .env

MYSQL_USERNAME=root
MYSQL_PASSWORD=litwicki
MYSQL_ROOT_PASSWORD=litwicki
MYSQL_PORT=3306
MYSQL_DATABASE=litwicki

UPLOAD_SIZE=100M
PHP_MEM_LIMIT=128M
DEBUG=1

API_HOSTNAME=api.litwicki.dev
API_PORT=80
API_ENVIRONMENT=dev

Finally, application configuration

There will be things you’ll want to configure specific to your application and web-server to take this to the finish line, but if you’ve gotten this far, you know how to do the rest!