Spreading our Seed: Drupal Development Server Management for OSX

For about a year now we've been working on a Drush-based server management tool called Seed. I've become quite accustomed to it. A few weeks ago, I decided that I wanted to install it on my home development environment which runs on OSX. Since it was developed originally for a Linux system I knew it should be *fairly* easy to modify it to work on OSX. Here's how to do it:

I wrote this tutorial specifically for OSX Mountain Lion (10.8.x). I imagine it will be pretty much exactly the same for Lion (10.7.x) but I can't say for sure. As far as a preliminary knowledge base, you'll need to be familiar with the command line and drush. Additionally, knowing a bit about apache and apache configuration will be helpful in case you need to debug anything along the way.

The Development Environment

Not everyone is going to have their development environment set up exactly as I set mine up. A lot of these components could be installed through any number of means. Additionally, you may be using a packaged solution like XAMPP or MAMP. It doesn't necessarily matter how you've set up your dev environment, but you do need to know where everything is installed. I've set up my local development environment using as many of the components that come bundled with OSX (Mountain Lion) as I can. For missing pieces I've installed homebrew. I'll give a quick rundown of what I've done to get my dev environment up and running.

Git

You'll need git installed to leverage some of seed's project repo management features. You also need git for homebrew to work. By default, Mountain Lion doesn't come with git installed. Apple recommends installing XCode and enabling their "Command Line Tools" from there to get git. Which is exactly what I did. Although, I'm sure you could just run brew install git and get roughly the same results.

Homebrew

Installing homebrew is pretty straightforward. There are instructions on their site: http://brew.sh. Homebrew also requires ruby, but that also comes pre-installed on Mountain Lion.

One thing to note about Homebrew is that it installs everything into /usr/local/Cellar/ and typically uses a folder structure of /usr/local/Cellar/[package-name]/[version-number]

PHP & Apache

PHP and Apache come with Mountain Lion so I didn't have to do anything to install these.

  • PHP: Should just work. Type php --version to be sure.
  • Apache is installed in: /etc/apache2

MYSQL

Homebrew makes it pretty easy to install mysql.

$ brew install mysql

For a more detailed explanation of installing mysql using homebrew, read this post. Also, you may need to symlink the required socket file. It should be either at /tmp/mysql.sock or /var/mysql/mysql.sock. Whichever one is missing the mysql.sock file, just create a symlink to the existing one. On my machine, I was missing the socket file in /var/mysql so I had to do the following from the command line:

$ mkdir /var/mysql
$ ln -s /tmp/mysql.sock /var/mysql/mysql.sock

Drush

There's a few ways to install drush. I chose to use homebrew. It doesn't really matter how it's installed. All that matters is that you know where it's installed.

$ brew install drush

When installing drush with homebrew, the files will end up here: /usr/local/Cellar/drush/[version-number]/libexec/ where [version-number] is the drush version. On my system it's /usr/local/Cellar/drush/6.0.0/libexec/

DNSmasq

This last tool is optional. When we add new projects with seed, we'll be creating virtual host entries for each one with a domain along the lines of project.username.localdev (I'm using the top-level-domain .localdev because we already use .dev internally on our central dev environment). For each one of these virtual hosts, we would usually need to add a new entry to our /etc/hosts file so that the domain resolves to our localhost server (since the hosts file doesn't understand wildcards). However, with dnsmasq, we can set it up to resolve all requests to the top-level domain .localdev to our localhost ip (127.0.0.1). To install dnsmasq, we'll just turn to our trusty homebrew once again:

$ brew install dnsmasq

For installation instructions for dnsmasq, check out this concise post.

A note about dnsmasq on OSX: You should be able to use any top-level-domain except for .local. "local" is reserved by OS X for mDNS. If you run scutil --dns you'll see it near the top of the list

Installing Seed

Seed comes with some installation instructions in an INSTALL.txt file. These instructions are technically for a linux server, but some of the things in it will apply to our OSX installation as well.

Firstly, we'll need to setup our .drush folder (located in your home folder ~/.drush). The following is a list of commands to run from the command line. Lines beginning with '#' are comments and are there just to explain the following line(s) of code:

$ sudo mkdir ~/.drush/cache/default
$ sudo chmod -R 0775 ~/.drush/cache
# Replace chriseastwood with your user name
$ sudo chown -R chriseastwood:staff ~/.drush

Next, we're going to install seed into our .drush folder and symlink it to our drush installation:

# Go to your .drush folder
$ cd ~/.drush
# Clone the Repository
$ git clone https://bitbucket.org/evan_fuseinteractive/seed.git
# Create the symlink from
$ sudo ln -s /Users/chriseastwood/.drush/seed /usr/local/Cellar/drush/6.0.0/libexec/commands/seed
# Clear the drush cache
$ drush cc drush
# Install Seed. All this really does is make it so you can run seed commands by just typing 'seed command' instead of 'drush seed-command'
$ sudo drush seed-install

Setting up Seed

The next thing we need to do is setup the seed.info file. The seed.info file is just a php configuration file (.ini) where you setup some base variables for seed to work. This is where your knowledge of your development environment setup is needed. One thing to note about the info file is that you will be provided with some tokens to help with the dynamic creation of folders and filenames:

  • [USER]: The user calling the seed command
  • [PROJECT]: The project name the seed command is acting on.
  • [THIS.<variable>]: This is a way to reference variables defined within the info file itself. Most notably we use it to reference the defined global_user: [THIS.global_user]

Here's my seed.info file with some comments added to explain each part. (comments are lines that start with a semi-colon).

; You probably won't need to change your server port, but if you do this is
; where you'll do it
server_port = 80
 
; This is the filename template of vhost files created by seed. This will also
; be the local url of the site. I'm using the top-level-domain "localdev"
; because we already use "dev" internally
server_vhost_template = [PROJECT].[USER].localdev
 
; This is just a path where seed can store server information. It will store
; it in a folder named ".drush-server"
server_path = /Users/[THIS.global_user]
 
; The location to save drush alias files to. By default drush won't look for
; aliases in this folder, but I've added the path to my own drushrc.php file.
; See http://drush.ws/examples/example.aliases.drushrc.php for more info
alias_path = /Users/[THIS.global_user]/.drush/aliases
 
; MYSQL Credentials. I created a user with full privileges named "chris" for
; the purposes of this demo
mysql_host = localhost
mysql_user = chris
mysql_pass = chris
 
; The global user is sort of like the seed "superuser". The global user really
; only comes into play in a multi-user dev environment. It's a way for
; individual users to specify something should be global, rather than just for
; their own user.
global_user = chriseastwood
global_group = staff
 
; The following is where you set up your seed directories. These are essentially
; templates for where to keep your project, as well as db dumps, logs and files.
; We'll be making good use of some of our dynamic tokens here.
;
; This is the directory naming structure for all you projects.
project_directory = /Users/[USER]/Sites/drupal/www/[PROJECT]
;
; The directory structure where all your database dumps will be saved.
; Database dumps aren't user specific so we're just going to put them in the
; global user's directory. My local dev environment only has one user so the
; global user is one and the same, but this future proofs it for if I decide to
; go to a multi-user dev environment at some point
db_dump_directory = /Users/[THIS.global_user]/Sites/drupal/databases/[PROJECT]
;
; The directory structure where all your project logs will be saved
log_directory = /Users/[THIS.global_user]/Sites/drupal/logs/[PROJECT]
;
; The directory structure where your 'files' folders will be saved. With seed,
; your files folders are kept outside of the main drupal install and symlinked
; from sites/default (more on the files directory later)
files_directory = /Users/[THIS.global_user]/Sites/drupal/files/[PROJECT]
 
; Your global project directory. The template is usually the same as
; project_directory but with the global user instead of just [USER]
project_global_directory = /Users/[THIS.global_user]/Sites/drupal/www/[PROJECT]
 
; The global database user. In my case this is just the mysql user I created
project_global_db_user = chris
 
; This is the template for forming the repo url for git clone.
project_repo_template = git@bitbucket.org:git_fuseinteractive/[PROJECT].git
 
; A command template for initializing a new repo. This template is for
; bitbucket specifically but could be changed for github or some other hosted
; git provider (provided they have some method of making api calls from the
; command line - e.g. curl)
repo_init_cmd = "curl --request POST --user chris_fuseinteractive:password https://api.bitbucket.org/1.0/repositories/ --data name=[PROJECT] --data scm=git --data is_private=True"
 
; Set the enabled plugins for your seed install. These plugins are still sort
; of work in progress so I'm not going to go into too much detail.
plugins[] = bitbucket
; plugins[] = branches
; plugins[] = quality
; plugins[] = checklist
 
; The following variables are command templates for performing some API requests
; with Bitbucket. These are only used if the bitbucket plugin is enabled.
; The only thing you'll really need to change here is the bitbucket username
bitbucket_new_key_cmd = "curl --request POST --user chris_fuseinteractive:password https://api.bitbucket.org/1.0/ssh-keys/ --data label='[Seed] [USER]' --data key=[KEY]"
bitbucket_del_key_cmd = "curl --request DELETE --user chris_fuseinteractive:password https://api.bitbucket.org/1.0/ssh-keys/[KEY]/"

A Note about Directories:

I've set up all my project files to be in /Users/username/Sites/drupal . The only reason I did this is because the /Users/username/Sites folder gets an special icon in Finder. Alternatively you could do something more linuxy like /home/username/www for project folders, /home/username/databases for dbs etc. I've also been toying with the idea of using something like Google Drive or Dropbox as a simple "cloud" storage solution for my databases and files directories.

Configuring Apache

The next thing we need to do is set up our apache httpd configuration. If you've already started apache using sudo apachectl start, run sudo apachectl stop to kill it. We're going to be conceding control of apache over to seed. When we're done you'll start/stop/restart your server using seed start/stop/restart.

When you start your server using seed start, seed starts apache using it's own httpd.conf file instead of the one in /etc/apache2/. The configuration file it uses is generated from a template file kept in ~/.drush/seed/templates/httpd.tpl.php. We're going to need to modify this a little to work with our Mac's apache setup. Here's the modified file with some extra comments to highlight the changes.

/**
 * This is the default httpd.conf template for Drush Server running on Ubuntu.
 *
 * Available Variables:
 * - host
 * - port
 * - uri
 * - conf_path
 * - base_path
 * - log_path
 * - doc_root
 */
?>
#
# Generated by drush-server.
#
ServerRoot /etc/apache2
 
######
# Required modules
#
# Include mods-enabled/*.load <-- #### Comment this line out
# Include mods-enabled/*.conf <-- #### Comment this line out
######
 
#######
# Include our original httpd.conf file.
# We're including this because it has some basic apache configuration directives
# that we know works on our mac. Additionally it loads all the modules we'll be
# needing. You could alternatively just manually copy only the directives you
# want to keep intact here, but I found this to just be easier. One thing to
# note is that if you do include the original httpd.conf file as we are here,
# you'll need to comment out "Listen 80" in it as we declare that later in this
# file
#
Include /etc/apache2/httpd.conf
#######
 
<IfModule php5_module>
  AddType application/x-httpd-php .php
  AddType application/x-httpd-php-source .phps
  <IfModule dir_module>
    DirectoryIndex index.html index.php
  </IfModule>
</IfModule>
 
ExtendedStatus On
<Location /server-status>
  # Turn of rewrite rules or else Drupal's .htaccess rewrite rules will clobber
  # this location.
  <IfModule rewrite_module>
    RewriteEngine off
  </IfModule>
  SetHandler server-status
  Order Deny,Allow
  Allow from all
</Location>
 
######
# Point to the Ubuntu file
# TypesConfig /etc/mime.types <-- #### Comment this out - file does not exist on osx
######
 
######
# Use the www-data:www-data user:group
# Change this to your global user name with the group 'staff'
User chriseastwood
Group staff
######
 
#
# Custom configuration built by drush
#
PidFile <?php print $conf_path; ?>/httpd.pid
LockFile <?php print $conf_path; ?>/accept.lock
 
ServerName <?php print $host; ?>
 
<?php foreach ($ports as $port): ?>
Listen <?php print $port ."\n"; // Save me from the line-break monster! ?>
<?php endforeach; ?>
<IfModule ssl_module>
  Listen 443
</IfModule>
 
ErrorLog <?php print $log_path; ?>/error_log
LogFormat "%h %l %u %t \"%r\" %>s %b" combined
CustomLog <?php print $log_path; ?>/access_log combined
 
#
# Use name-based virtual hosting.
#
<?php foreach ($ports as $port): ?>
NameVirtualHost *:<?php print $port ."\n"; // Save me from the line-break monster! ?>
<?php endforeach; ?>
<IfModule ssl_module>
  NameVirtualHost *:443
</IfModule>
 
Include <?php print $conf_path; ?>/sites/

NOTE: Don't forget to comment out "Listen 80" from your original /etc/apache2/httpd.conf file since it's already being declared in seed's httpd.conf file!

The Virtualhost Templat

Additionally, seed comes with a vhost template (~/.drush/seed/templates/vhost.tpl.php) that you could alter depending on your needs. For the purposes of this demo (and probably for most setups), we're going to leave this template in it's stock form. However, the template does have a block for ssl support which means you may need to create a dummy ssl certificate. It's specifically looking for /etc/apache2/dev.crt and /etc/apache2/dev.key. Here's an uncommented set of commands you can run from the command line to generate the necessary files. There are some prompts. It's up to you if you want to use passphrases. I didn't.

$ cd /etc/apache2
$ sudo ssh-keygen -f dev.key
$ sudo openssl req -new -key dev.key -out dev.csr
$ sudo openssl x509 -req -days 365 -in dev.csr -signkey dev.key -out dev.crt

Start Using Seed!

And that's it! Seed is set up. Open your command line and start seed:

your-mac:~ username$ seed start

Now create a new project with seed init:

your-mac:~ username$ seed init
Project name: d7sandbox
Enter a number.
 [0]  :  Cancel
 [1]  :  Clone an existing project repository
 [2]  :  Create a new project
 [3]  :  Copy project from another user
2
[Project] Successfully created directory /Users/chriseastwood/Sites/drupal/www/d7sandbox
Use git? (y/n): y
Initialize new remote: git@bitbucket.org:git_fuseinteractive/d7sandbox.git ? (y/n): y
[Project] Initialized git repository
[Project] Initialized git remote
[Project] Added remote git@bitbucket.org:git_fuseinteractive/d7sandbox.git
Symlink the files directory? (y/n): n
[Database] MySQL user chris already exists.
[Database] Created MySQL database d7sandbox for user chris
Create database d7sandbox_chriseastwood? (y/n): n
Create a settings.php file with the database d7sandbox? (y/n): n
Create Drush alias? (y/n): y
User [chriseastwood]:
Web root path [/Users/chriseastwood/Sites/drupal/www/d7sandbox]:
Files path [/Users/chriseastwood/Sites/drupal/www/d7sandbox/sites/default/files]:
Database dump path [/Users/chriseastwood/Sites/drupal/databases/d7sandbox]:
URI [d7sandbox.chriseastwood.localdev]:
[Alias] Could not read the alias file /Users/chriseastwood/.drush/aliases/d7sandbox.aliases.drushrc.php. This is not a fatal error however it may result in aliases not being created.
[Alias] Created new alias: @d7sandbox.dev.chriseastwood
Host name [d7sandbox.chriseastwood.localdev]:
Document root [/Users/chriseastwood/Sites/drupal/www/d7sandbox]:
[Added] d7sandbox.chriseastwood.localdev -> /Users/chriseastwood/Sites/drupal/www/d7sandbox
Server stopped.
No errors found in the server configuration.
Server started with the following virtual hosts:
 d7sandbox.chriseastwood.localdev  ->  /Users/chriseastwood/Sites/drupal/www/d7sandbox
Server restarted.

Note the non-fatal error when creating the alias. This is because seed tries to read the alias file (presumably to check for existence) before creating it. In this case the error can be ignored. Our alias is created correctly.

Also, don't forget to add an entry in you /etc/hosts file for each virtual host you create if you decided not to install dnsmasq.

Here's another example of using seed init to initialize an existing project that's already in version control.

your-mac:~ username$ seed init
Project name: existingproject
Enter a number.
 [0]  :  Cancel
 [1]  :  Clone an existing project repository
 [2]  :  Create a new project
 [3]  :  Copy project from another user
1
[Project] Successfully created directory /Users/chriseastwood/Sites/drupal/www/existingproject
Repository URL [git@bitbucket.org:git_fuseinteractive/existingproject.git]:
[Project] Cloning repository git@bitbucket.org:git_fuseinteractive/existingproject.git
[Project] This could take awhile depending on the size of the repository...
Cloning into '/Users/chriseastwood/Sites/drupal/www/existingproject'...
remote: Counting objects: 5799, done.
remote: Compressing objects: 100% (4444/4444), done.
remote: Total 5799 (delta 1119), reused 5799 (delta 1119)
Receiving objects: 100% (5799/5799), 15.90 MiB | 1.55 MiB/s, done.
Resolving deltas: 100% (1119/1119), done.
Checking out files: 100% (4224/4224), done.
[Project] Cloning repository complete
Symlink the files directory? (y/n): y
Target directory [/Users/chriseastwood/Sites/drupal/files/existingproject]:
Symlink the files directory for existingproject (user chriseastwood) to the files directory /Users/chriseastwood/Sites/drupal/files/existingproject? (y/n): y
[Project] Symlinked /Users/chriseastwood/Sites/drupal/www/existingproject/sites/default/files to /Users/chriseastwood/Sites/drupal/files/existingproject
[Database] MySQL user chris already exists.
[Database] Created MySQL database existingproject for user chris
Create database existingproject_chriseastwood? (y/n): n
Create a settings.php file with the database existingproject? (y/n): y
MySQL username to connect to this database [chris]:
MySQL password to connect to this database: chris
Create Drush alias? (y/n): y
User [chriseastwood]:
Web root path [/Users/chriseastwood/Sites/drupal/www/existingproject]:
Files path [/Users/chriseastwood/Sites/drupal/www/existingproject/sites/default/files]:
Database dump path [/Users/chriseastwood/Sites/drupal/databases/existingproject]:
URI [existingproject.chriseastwood.localdev]:
[Alias] Could not read the alias file /Users/chriseastwood/.drush/aliases/existingproject.aliases.drushrc.php. This is not a fatal error however it may result in aliases not being created.
[Alias] Could not read the alias file /Users/chriseastwood/.drush/aliases/existingproject.aliases.drushrc.php. This is not a fatal error however it may result in aliases not being created.
Password:
[Alias] Created new alias: @existingproject.dev.chriseastwood
Host name [existingproject.chriseastwood.localdev]:
Document root [/Users/chriseastwood/Sites/drupal/www/existingproject]:
[Added] existingproject.chriseastwood.localdev -> /Users/chriseastwood/Sites/drupal/www/existingproject
Server stopped.
No errors found in the server configuration.
Server started with the following virtual hosts:
 existingproject.chriseastwood.localdev  ->  /Users/chriseastwood/Sites/drupal/www/existingproject
 taymor.chriseastwood.localdev    ->  /Users/chriseastwood/Sites/drupal/www/taymor
Server restarted.

Next we're going to load the database (which we've put into our databases folder - /Users/chriseastwood/Sites/drupal/databases/existingproject/)

your-mac:~ username$ seed db-load
Project name: existingproject
User name [chriseastwood]:
Load in to database [existingproject_chriseastwood]: existingproject
Backup the database first? (y/n): n
Which DB should be loaded?
 [0]  :  Cancel
 [1]  :  existingproject.sql
1
[Database] Deleted MySQL database existingproject
[Database] Created MySQL database existingproject for user chriseastwood
[Database] Back up /Users/chriseastwood/Sites/drupal/databases/existingproject/existingproject.sql loaded in to database existingproject

And I'm up and running and ready to go!

Upgrading Seed

There is one caveat with the way I've set up Seed on my local system. I've edited core files of seed so when I go to upgrade, my configurations will be overwritten. For now the simplest solution is to move my customizations out of the seed folder, and then create symlinks to them. This way when I upgrade seed, at least my files will still be safe

$ mkdir ~/.seed
$ mv ~/.drush/seed/seed.info ~/.seed/
$ mv ~/.drush/seed/templates/httpd.tpl.php ~/.seed/
$ ln -s ~/.seed/seed.info ~/.drush/seed/seed.info
$ ln -s ~/.seed/httpd.tpl.php ~/.drush/seed/templates/httpd.tpl.php

In the future, however, we're working on setting up a way to supply your own seed overrides without having to create symlinks. Keep an eye out for that in future releases!

Conclusion

So there it is: Seed. On your Mac. Your one stop shop for all your drupal server management needs. And somehow I managed to get through the whole tutorial without a single horticultural pun (except for the title). Now go forth and let Seed form the roots that helps your projects grow bigger, stronger, and faster!