MAMP Pro to DDEV


MAMP Pro to DDEV with Docker

If you’re like me, you’ve used MAMP Pro for years because you work on a Mac and it’s easy to get a PHP site running in a few clicks.

You first approached the command line as one would a wounded bear, but you’ve increasingly found it to be inviting and liberating. You’ve poked at Vagrant, been wooed at the idea of Docker, and you’ve soldiered on with MAMP because you haven’t found a way past it that sticks day to day. You may be wary of those who espouse command-line-first tools, because the tingle of excitement is normally followed by hours of stumbling over broken dependencies of which zero were obvious.

Take heart! I’ve finally found my local development champion in DDEV and I’ve been happily using it for more than a month. Let me reiterate: happily. More than a month. Not a single moment of weakness when I had to go back to MAMP just to do that one thing. DDEV isn’t perfect, but it’s been a welcome MAMP replacement and I’m going to try and convince you to try it.

DDEV? #

DDEV isn’t a prepackaged Mac app like MAMP, but a command line tool that manages Docker containers for development.

Like MAMP, it’s easy to work with and it comes with the innards you’ll need for working with LAMP/LEMP apps. Unlike MAMP, configuration lives quietly within each project (which you can version if you want!) and the machinery is all powered by Docker containers. That difference is important because those containers are more efficient than individual virtual machines like you might run with VirtualBox and Vagrant. They’re not as efficient as MAMP, but each project container runs its own software stack and can be fully customized—very much unlike MAMP. These containers are also fully isolated, so (like MAMP) they don’t have to care about whatever software you’re running natively on your Mac.

DDEV is free and actively maintained, as are similar systems like Lando, Docksal and Devilbox. The purpose of this post isn’t to compare options, but I’ll at least mention that I stuck with DDEV because almost every problem I encountered had an answer, whether by design or documentation or mention in a support issue. It required the fewest changes to my workflow and could be tailored most easily wherever I was forced to evolve. Each of the above was easy to get started with, and DDEV kept making sense as I went.

Pure Docker containers (or docker-compose directives) would be great, but I haven’t developed enough proficiency to actually ditch MAMP and use Docker that closely day to day. DDEV provided just enough to get me over that wall. If you’ve already considered using Docker for local development, spin up a DDEV project and examine the difference between the yaml you define and the docker-compose.yaml DDEV generates for you. That difference, very precisely, is DDEV’s appeal: it abstracts what might otherwise be a bit too complicated for daily use.

DDEV Pros #

  • Great for Craft CMS, Statamic, Laravel, and ExpressionEngine projects. Pick Apache or nginx in your config, choose a PHP version (back to 5.6), and ddev start to spin up the project.
  • Assumes you’ll want to be working locally (and offline) just like MAMP, and automatically attempts to manage /etc/hosts changes for your *.ddev.local or custom development domain.
  • The standard PHP setup happens to come with all Craft requirements, including ImageMagick and GD. Default Apache + nginx setups will play nice with clean URLs, and you can season to taste easily with your own .htaccess, nginx.conf, and/or php.ini.
  • Easy database management: quick commands for taking snapshots, importing dumps, and launching PHPMyAdmin or Sequel Pro.
  • Crazy ability to customize if you’re willing to experiment a bit with Docker and Linux. Also a nice way to learn about Docker since you can peek behind the DDEV curtain to see how it works, and further customization will mean getting closer to Docker and not some odd contraption a Gandalfish character maintains from a remote mountaintop with spotty satellite internet.
  • An alpha GUI if you just can’t break the habit.
  • Regularly updated, pretty thoroughly documented, and supported by friendly and responsive folks.
  • Working on a project and actually getting stuff done without MAMP will make you feel a keen sense of triumph.

DDEV Cons #

  • Hyperkit, which is required to run Docker on a Mac, has often taken an absurd amount of CPU on both (Mojave) Macs I use regularly. Your mileage may vary, and I’m counting on this eventually being improved.
  • Docker downloads entire images as it needs them, so it can quickly take more disk space than MAMP if you tend to customize your environments. This can be managed, but it could be a deal-killer if you work on a laptop without ample free space.
  • If you develop Craft 3 plugins and use symbolic links that reach outside your current project, the complexities of volume mounts mean you’ll want to rsync files into your project. (Or deal with either an incredible performance hit or a solution I’ve not yet found.)
  • Though every site gets https support URL by default, the self-signed certificate means you’ll have to live with browsers being cranky. This is a huge drag when you’re working with an app like PageLayers that doesn’t allow you to ignore self-signed certificates.

What it Looks Like #

Once you’ve installed DDEV and Docker for Mac, you’ll run ddev config from your project root to initialize some .yaml in a project-relative .ddev folder.

Rather than loading MAMP and switching on its server, you’ll just ddev start from a project root. If you forget how to access your site or database, just ddev describe:

ddev start and ddev describe 

You can use ddev stop to shut down that project, or ddev restart to turn it off and on again. While you’re working, it can be handy to run ddev snapshot and ddev restore-snapshot for jumping between database states.

There are a few more utilities that can be useful as well…

$ ddev -h
This Command Line Interface (CLI) gives you the ability to interact with the ddev to create a development environment.

Usage:
  ddev [command]

Available Commands:
  auth-pantheon    Provide a machine token for the global pantheon auth.
  config           Create or modify a ddev project configuration in the current directory
  describe         Get a detailed description of a running ddev project.
  exec             Execute a shell command in the container for a service. Uses the web service by default.
  help             Help about any command
  hostname         Manage your hostfile entries.
  import-db        Pull the database of an existing project to the dev environment.
  import-files     Pull the uploaded files directory of an existing project to the default public upload directory of your project.
  list             List projects
  logs             Get the logs from your running services.
  pull             Pull files and database using a configured provider plugin.
  remove           Remove the development environment for a project.
  restart          Restart the development environment for a project.
  restore-snapshot Restore a project's database to the provided snapshot version.
  sequelpro        This command is not available since sequel pro.app is not installed
  snapshot         Create a database snapshot for one or more projects.
  ssh              Starts a shell session in the container for a service. Uses web service by default.
  start            Start a ddev project.
  stop             Stop the development environment for a project.
  version          print ddev version and component versions

Flags:
  -h, --help          help for ddev
  -j, --json-output   If true, user-oriented output will be in JSON format.

Use "ddev [command] --help" for more information about a command.

If looking at ddev -h output makes you uncomfortable, you can go and download the alpha DDEV UI. It’s not nearly as full-featured as MAMP’s UI, but it’ll let you see all your projects with their status and give you the ability to start, restart, and stop each one.

Screenshot of DDEV alpha UI.

DDEV's alpha UI, for those that must. 

If you’re still reading I’ll assume this is theoretically interesting. Let’s look a little closer at what’s going on and start up some Craft projects.

Getting Started #

You’ll need to install Docker and DDEV. If you’re on a Mac with Homebrew installed, you can run brew cask install docker and then brew tap drud/ddev && brew install ddev.

If that went well, choose an existing project and open a command prompt at its root. In VS Code, this is a quick ^+~. Initialize your project with ddev config, which will ask for three things:

  1. The project name, which defaults to the parent folder and is used for a *.ddev.local domain.
  2. The web root.
  3. The project type, which is php unless you’re using any of the listed options.

This will create a .ddev folder within your project:

.ddev
- .gitignore
- config.yaml

The config file is pretty simple, with a commented section afterwards (which I’ve omitted here) explaining some bits and pieces you may want to customize.

APIVersion: v1.3.0
name: my-project
type: php
docroot: public
php_version: "7.1"
webserver_type: nginx-fpm
router_http_port: "80"
router_https_port: "443"
xdebug_enabled: false
additional_hostnames: []
additional_fqdns: []
provider: default

Not too scary, right? Run ddev start to get the project running. DDEV will ask for your Mac password if it needs to edit /etc/hosts for a custom local domain.

You’ll probably want to import a database. Drop a .sql or .sql.gz file someplace near your project and use ddev import-db and interactively provide a path. You can also do this in one command with ddev import-db --src=db.sql, or open PHPMyAdmin with the link listed in ddev describe if you’d just rather use a GUI.

If you’re curious about what’s going on behind the scenes, look no further than the docker-compose.yaml file that was quietly created in your .ddev folder. You may already know what this is, and appreciate that DDEV generated it for you. If you’ve never seen any of this docker-compose business before, this is a configuration fed more directly to Docker that tells it what to do. You don’t have to look at or change anything here, and you probably shouldn’t change any part of it, but it’s a fun place to look if you want to get a better feel for how everything works.

Screenshot of docker-compose.yaml generated by DDEV

This is what DDEV wrote to Docker while you weren't looking. 

Sample Craft 3 Setup #

I added a few more things to my standard Craft 3 project setup. It takes a few more seconds to copy in files and make an edit, but it’s still quicker to get a project going than it would be with MAMP. I keep the .ddev folder versioned (without changing its gitignore) so another developer can check out the project, ddev start, and be on her way—so these steps only apply to when I’m first adding DDEV to a codebase.

First, I add a simple setup script hook. Tell config.yaml to run a shell script after everything’s started:

hooks:
  post-start:
  - exec: .ddev/scripts/setup-craft-3.sh

This will run setup-craft-3.sh, which I’ve dropped into .ddev/scripts:

#!/bin/bash

# We're running from \web and need to move up a level
cd ..

# Search for .env to see whether we've already finished local setup
ENV_FILE=".env"

if [ -f "$ENV_FILE" ]
then
    echo "Environment file found. Leaving it alone!"
else
    echo "Environment file not found. Setting up project."
    npm install
    composer install
    cp .ddev/.env.ddev.example .env
    ./craft setup/security-key
fi

I keep Craft 3 environment settings in a .env file, and this script just checks to see whether one already exists. If so, it doesn’t do anything. If not, it copies one into place and sets Craft’s security key.

Contents of .ddev/.env.ddev.example:

# The environment Craft is currently running in ('dev', 'staging', 'production', etc.)
ENVIRONMENT="dev"

# The secure key Craft will use for hashing and encrypting data
SECURITY_KEY=""

# The database driver that will be used ('mysql' or 'pgsql')
DB_DRIVER="mysql"

# The database server name or IP address (usually this is 'localhost' or '127.0.0.1')
DB_SERVER="db"

# The database username to connect with
DB_USER="db"

# The database password to connect with
DB_PASSWORD="db"

# The name of the database to select
DB_DATABASE="db"

# The database schema that will be used (PostgreSQL only)
DB_SCHEMA="public"

# The prefix that should be added to generated table names (only necessary if multiple things are sharing the same database)
DB_TABLE_PREFIX=""

Edit 12/9: Note that there’s no DB_PORT specified here. DDEV chooses a new MySQL port every time your project spins up, so we need to let it expose its own DB_PORT environment variable (as it does by default).

Even more excitingly, I add a file specifically named docker-compose.environment.yaml:

version: '3.6'

services:
  web:
    environment:
      - ENVIRONMENT=local
      - DB_DRIVER=mysql
      - DB_SERVER=db
      - DB_USER=db
      - DB_PASSWORD=db
      - DB_DATABASE=db
      - DB_SCHEMA=public
      - DB_TABLE_PREFIX=
#      - REDIS_HOST=$DDEV_HOSTNAME

This sets environment variables for the server, which you can of course customize however you’d like. Note the last commented-out line, which just reminds me to enable redis for environments that use it.

There’s redundancy in the .env and environment variables above, but this is all you need to add to have your Craft 3 projects up and running smoothly with DDEV! A modern Laravel or Statamic project would look very similar.

You can go on to specify whether you need apache or nginx, provide a custom nginx config, and more. I’ll just set this right here.

Sample Craft 2 Setup #

Older projects may not rely on environment variables for setup, but hopefully you’ve joined me in embracing multi-environment configurations for Craft 2, ExpressionEngine, and that old custom thing you clobbered together.

Craft 2 still works great with PHP7+, so you can add docker-compose.environment.yaml to your .ddev folder to supply database settings:

version: '3.6'

services:
  web:
    environment:
      - MYSQL_HOST=db
      - MYSQL_USER=db
      - MYSQL_PASSWORD=db
      - MYSQL_DATABASE=db

One more step, however, is to grab those settings using getenv() in craft/config/db.php. Mine look mostly like this:

<?php

return [

    '*' => [
        'server'      => getenv('MYSQL_HOST')     ?: 'localhost',
        'database'    => getenv('MYSQL_DATABASE') ?: 'local-db-name',
        'user'        => getenv('MYSQL_USER')     ?: 'local-db-user',
        'password'    => getenv('MYSQL_PASSWORD') ?: 'local-db-password',
        'tablePrefix' => 'craft',

        // to be compatible with MySQL 5.7, requires 2.6.2949 or greater
        'initSQLs'     => ["SET SESSION sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';"],   
    ],

    'REVIEW' => [
        'user'        => '',
        'password'    => '',
        'database'    => '',
    ],

    'STAGING' => [
        'user'        => '',
        'password'    => '',
        'database'    => '',
    ],

    'PRODUCTION' => [
        'user'        => getenv('MYSQL_USER'),
        'password'    => getenv('MYSQL_PASSWORD'),
        'database'    => getenv('MYSQL_DATABASE'),
    ],

];

Ancient Projects #

There might be a dusty old project or two that have you eyeing MAMP again, but don’t do it! You can edit your DDEV project configuration to go back in time to PHP 5.6 with php_version: "5.6", and if you’re lucky enough to be using multi-environment configuration you can set up yourproject.ddev.local with DDEV’s database settings. If you haven’t noticed already, the database name, username, and password are all db. Sort of easy to remember, and okay since we’re just working locally.

DDEV So Far #

I’ve actually enjoyed working with DDEV. If after all this glowing praise you’re left wondering why you wouldn’t want to use it instead of MAMP, I’ll give you my top two reasons:

  1. Hyperkit’s performance can be a dumpster fire. Emphasis on fire, which may actually start if you’re working on a laptop wearing highly flammable pants. CPU usage can be nuts. I’ve had the best luck limiting Docker to 2 CPUs, giving it 10GB of memory, and 1GB of swap. Also limiting mounted directories to ~/.composer, ~/.ddev, and ~/Documents/git (where projects live) and making sure Docker’s storage uses a .raw disk image.
  2. You can’t neatly symlink Craft 3 plugins outside your project via composer. This is because of how file mounts work. You can symlink plugins from within the project just fine, but if you’re working in ~/Projects/foo with DDEV your composer symlink to ~/Projects/craft-plugin won’t work. I’ve either temporarily moved the plugin into the project or put rsync on watch.

I’m thinking that each issue will be smoothed out one way or another, and overall I’m still happy I’ve made the switch. Let me know what you think if you end up trying it!

* * *