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 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.
- 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 startto 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.
- 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
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.
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.
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:
- The project name, which defaults to the parent folder and is used for a *.ddev.local domain.
- The web root.
- The project type, which is
phpunless 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.
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'), ], ];
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:
- 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.
- 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!