Patterns
In a nutshell, Make calls either Docker or Compose which then runs a Command
inside a container. A project does not need to follow only 1 pattern. For
instance, make targetA can call Compose and make targetB, Docker.
Compose
Section titled “Compose”In this pattern, Make calls Compose which then runs the Command inside a container.
Examples
Section titled “Examples”Here are some examples illustrating Compose with different commands.
Make calls Compose which then calls another Make target inside a Docker container. This requires the Docker image (that runs the command) to have Make installed.
version: "3"services: alpine: image: alpine volumes: - .:/opt/app working_dir: /opt/appecho: docker compose run --rm alpine make _echo
_echo: echo 'Hello, World!'make echoMake calls Compose which executes a shell/bash command inside a Docker container.
version: "3"services: alpine: image: alpineecho: docker compose run --rm alpine sh -c 'echo Hello, World!'make echoShell file
Section titled “Shell file”Make calls Compose which executes a shell/bash command inside a Docker container. The following example uses a shell file that mimics Make:
#!/usr/bin/env shdeps(){ printf "deps\n"}
test(){ printf "test\n"}
for target in "$@"do case "$target" in (deps) deps ;; (test) test ;; (*) printf "Usage: $0 {deps|test}\n" exit 2 ;; esacdonechmod +x make.shversion: "3.8"services: alpine: image: alpine volumes: - .:/opt/app working_dir: /opt/apptest: docker compose run --rm alpine make.sh deps testmake testLanguages like Go, Python, JavaScript, Ruby, etc can be used as an alternative to shell/bash scripts. The following example uses Go to echo ‘Hello, World!‘.
package main
import "fmt"
func main() { fmt.Println("Hello, World!")}version: "3"services: golang: image: golang:alpine volumes: - .:/opt/app working_dir: /opt/appecho: docker compose run --rm golang go run main.gomake echoThere are many languages and tools out there to make task implementation easy such as Gulp and Rake. Those tools can easily be integrated to the 3 Musketeers. The following is simply a NodeJS example which echos “Hello, World!” by invoking npm.
{ "name": "helloworld", "description": "echos 'Hello, World!'", "scripts": { "echo": "echo 'Hello, World!'" }}version: "3"services: node: image: node:alpine volumes: - .:/opt/app working_dir: /opt/appecho: docker compose run --rm node npm run echomake echoDocker
Section titled “Docker”Make calls directly Docker instead of Compose. Everything that is done with Compose can be done with Docker. Using Compose helps to keep the Makefile clean.
echo: docker run --rm alpine echo 'Hello, World!'make echoDocker-in/outside-of-Docker (DinD/DooD)
Section titled “Docker-in/outside-of-Docker (DinD/DooD)”There are many articles and videos talking about Docker-in-Docker (DinD) and Docker-outside-of-Docker (DooD) with pros and cons. This section describes a pattern that can be applied to both. A Docker container contains Make, Docker, and Compose which communicates to a Docker daemon, whether it is on a host (DooD) or inside another container (DinD). In such case, an image like flemay/musketeers can be used.
An example of DooD is the 3 Musketeers demo generated with VHS.
Another example is GitLab CI which allows access to Docker daemon within a Docker container. A pipeline configuration would look like the following:
image: flemay/musketeers:latestservices: - docker:dindvariables: DOCKER_HOST: "tcp://docker:2375"
stages: - test
test: stage: test script: - make testSome references:
- Jérôme Petazzoni’s excellent blog post on using Docker-in-Docker outlines some of the pros and cons of doing so (and some nasty gotchas you might run into).
- Docker-in-Docker: Containerized CI Workflows (DockerCon 2023)