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!