At Velir, I've spearheaded the usage of Docker for local development since 2017. One of the most invaluable tools to lowering the barrier of entry for people was adding Ahoy to our toolbox.
What is Ahoy?
Ahoy is a command line tool that allows you to create simple to use commands to orchestrate otherwise complex or overly verbose commands. This is extremely useful to onboard people who are not as familiar or comfortable with the command line to a project, while enforcing consistency and predictability. It is YAML based and can be used in any project, and is available for MacOS, Linux, and Windows.
Using Ahoy
Ahoy shines for abstracting complex tasks behind simple commands.
For example, telling a developer to 'first, install Drupal' when setting up a project. How do they do that? Consider:
- Do they use the browser UI install, and have to know all the connection settings to enter?
- Do you expect them to know the Drush site install command with all the right arguments to pass?
- Do they know how to use Drush inside a Docker container?
- Is this their first time using Drupal and Drush?
It's daunting!
With Ahoy you can simplify that to typing "ahoy install-drupal". You can also call other Ahoy commands inside of commands. So to wrap that up into the install-drupal command, we can do something like the following:
drush:
cmd: docker-compose exec php /var/www/html/vendor/bin/drush "$@"
usage: Run Drush commands in the php container.
install-drupal:
cmd: |
if [ ! -f ./docroot/sites/myproject.localhost/settings.php ]; then
ahoy drush si -y --db-url=mysql://drupal:drupal@mariadb/drupal --site-name=myproject -r="/var/www/html/docroot" --sites-subdir=myproject
else
echo "Settings file detected in sites/myproject.localhost, Drupal is already installed."
fi
ahoy drush @myproject.local status
usage: Installs Drupal to the local database.
This command checks if a settings file is defined in the local development directory, if not, it calls the Drush site-install command with all the arguments set for development. Finally, it calls Drush status command, which will list a development URL and some other information for the user.
This is a productivity boon for everyone on the team. We've amassed a pretty decent backlog of commands to make development easier for us on projects. Using Ahoy has been successful in onboarding clients, contractors and team members who have little or no background in development, instilling confidence in their ability to participate without first having deep technical knowledge in a variety of areas.
Going Further
What if we wanted to use those commands on other projects? Can you see any potential issues with the above?
First, both commands make the following assumptions:
- 'docker-compose' is the command to use to execute Drush in the PHP container
- 'myproject' is the name of the project
- 'myproject.localhost' is the project URL
These values are hardcoded. If we copied and pasted one Ahoy file to another project, there is no guarantee it will work the same. Maybe some developers have installed Mutagen, and their commands need to use 'mutagen compose' instead of 'docker-compose' to execute.
How can we support this without hardcoding values into the commands defined in .ahoy.yml? Wouldn't it be great if we could make Ahoy know all of this before it executes anything? Well, you can!
Ahoy supports the option of overriding its default entrypoint command, which is defined as the following:
# You can now override the entrypoint. This is the default if you don't override it.
# {{cmd}} is replaced with your command and {{name}} is the name of the command that was run (available as $0)
entrypoint:
- bash
- "-c"
- '{{cmd}}'
- '{{name}}'
Now we have a way to change the behavior to fit these requirements mentioned above:
- Support the use of environment variables
- Support users who are using Mutagen
- Support users who are not using Mutagen
- Run some quality-of-life preflight checks for the user
- Provide useful errors if something is missing
Override the entrypoint
With that in mind, lets override the entrypoint in our .ahoy.yml file in the project. To do this, I am going to have Ahoy execute a shell script to achieve our goal.
First, lets change the entrypoint definition in .ahoy.yml:
# Run a shell script instead of the default entrypoint.
entrypoint:
- ./.docker/entrypoint.sh
You can create that file anywhere you want in your project, Ahoy will be able to find it. In this example, it is stored in the project root.
Now, we can fill in the shell script with what we need:
#!/usr/bin/env bash
if [ ! -f .env ]; then
echo "No .env file found. Please copy .env.example to .env, and make any adjustments and run again."
exit 1;
fi
docker_state=$(docker info > /dev/null 2>&1)
if [[ $? -ne 0 ]]; then
echo "Docker does not seem to be running. Start the Docker application and try again."
exit 1
fi
if ! command -v mutagen &> /dev/null; then
export DOCKER_COMMAND="docker-compose"
else
if [[ `mutagen version` != *"beta"* ]]; then
echo "Mutagen was detected, but does not appear to be a beta channel release. Please install the beta channel release of Mutagen as specified in the README.";
exit 1;
fi
export DOCKER_COMMAND="mutagen compose"
fi
export $(grep -v '^#' .env | xargs )
bash -e -c "$@"
If you are not familiar with shell scripts, thats okay. It does the following:
- Checks if there is a .env file defined, if not, it stops execution and tells the user what to do.
- Checks if Docker is running - if its not running, it stops execution and tells the user to start Docker.
- Detects if Mutagen is installed, and (at the time of this writing) checks if it is a beta release. If so, it creates an environment variable with the value 'mutagen compose', otherwise the value is 'docker-compose'.
- Loads values from the .env file and exports them to environment variables.
- Finally, it calls bash and passes all input - adding the default behavior back in at the end
Now every command you run with Ahoy will do all of the above. Its time to change our Ahoy command definitions to take advantage of this. Revisiting the drush and install-drupal commands, we can now change it to the following:
drush:
cmd: ${DOCKER_COMMAND} exec php /var/www/html/vendor/bin/drush "$@"
usage: Run Drush commands in the php container.
install-drupal:
cmd: |
if [ ! -f ./docroot/sites/$PROJECT_BASE_URL/settings.php ]; then
ahoy drush si -y --db-url=mysql://drupal:drupal@mariadb/drupal --site-name=$PROJECT_NAME -r="/var/www/html/docroot" --sites-subdir=$PROJECT_BASE_URL
else
echo "Settings file detected in sites/$PROJECT_BASE_URL, Drupal is already installed."
fi
ahoy drush @$PROJECT_NAME.local status
usage: Installs Drupal to the local database.
As an added example, what if we wanted to support a .env.local file to override values in the .env file? It is not uncommon to have .env files checked in to a project repository. Certain users may need different values for environment variables, we need to support their ability to do this.
Assuming you have added .env.local to the project gitignore file, we can do just that by adding a few lines before calling bash:
export $(grep -v '^#' .env | xargs )
if [ -f .env.local ] && [ -s .env.local ]; then
VARS=$(grep -v '^#' .env.local | xargs )
if [ -n "$VARS" ]; then
export $VARS;
fi
fi
If the .env.local file exists and has values, then we export them, effectively overriding existing values or adding new ones.
This is much better! The commands no longer contain hardcoded values specific to different projects, and we can drop them into other projects with the entrypoint.sh file and it will run without fail. We are able to support developers who are running Mutagen and fallback to docker-compose as a default executor, and provide a much smoother experience overall.