Skip to content

Instantly share code, notes, and snippets.

@ginkgomzd
Last active December 14, 2021 15:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ginkgomzd/fe8c722fbde2eced78ef9b0a59cc2c7e to your computer and use it in GitHub Desktop.
Save ginkgomzd/fe8c722fbde2eced78ef9b0a59cc2c7e to your computer and use it in GitHub Desktop.
Makefile to wrap docker-compose commands
-include service/${TASK}.conf
ifdef STACK
STACK := $(strip ${STACK})
STACK_NAME := $(shell echo "${STACK}" | tr A-Z a-z)
STACK_ID := $(shell echo "${STACK}" | tr a-z A-Z)
include stack/${STACK_NAME}.conf
endif
ifdef ${STACK_ID}_STACK
STACK_SERVICES += ${${STACK_ID}_STACK}
endif
.DEFAULT_GOAL := help
export WORKING_DIR
if-file = $(if $(wildcard $1/$2), $1/$2)
# # #
# /dev/null to avoid error from `cat` if no env files are used.
# precedence of .env files by location or declaration:
# ENV_INCLUDES > stack/${STACK_ID}.env > stack/ > service/
define stack-env-includes
/dev/null \
$(foreach svc,${STACK_SERVICES} ${TASK},$(call if-file,service,${svc}.env)) \
$(foreach stk,${STACK_SERVICES} ${STACK_ID} ${TASK},$(call if-file,stack,${stk}.env)) \
${ENV_INCLUDES}
endef
.%.env:
@ cat ${stack-env-includes} >$@
stack-env-file = .${STACK_NAME}.env
--env-file = $(if $(wildcard ${stack-env-file}),--env-file=${stack-env-file})
include-if = $(if $(wildcard $1/$2),-f $1/$2)
define stack-config-includes
$(foreach type,stack network volume config,$(call include-if,${type},${STACK_NAME}.yml))\
$(foreach svc,${STACK_SERVICES},$(call include-if,service,${svc}.yml))
endef
%.yml:
@ docker-compose --project-directory . ${stack-config-includes} config > $@ 2>/dev/null
# # #
# set and customize docker-compose commands
# and implement custom actions
# # #
custom-actions := down rund orphans services
define set-action
$(filter-out ${custom-actions},$*)\
$(if $(filter down,$*),$(if ${TASK},rm --force --stop,down))\
$(if $(filter orphans,$*),down --remove-orphans)\
$(if $(filter rund,$*),run -d)\
$(if $(filter run,$*),--rm)\
$(if $(filter up,$*),-d)\
$(if $(filter services,$*),config --services)
endef
define set-run-cmd
$(if $(filter rund run exec,$*),\
$(if ${RUN_CMD},\
$(if $(filter,1,$(words ${RUN_CMD})),${RUN_CMD},'${RUN_CMD}')\
)) ${CMD_ARGS}
endef
# # #
# docker-compose wrapper
# # #
dkc-%: .${STACK_NAME}-compose.yml $(if $(wildcard service/${TASK}.yml), service/${TASK}.yml) | ${stack-env-file}
docker-compose ${--env-file} $(foreach f,$^,-f $f) \
$(set-action) ${DK_CMP_OPTS} \
$(if ${WORKING_DIR},$(if $(filter rund run exec,$*),--workdir ${WORKING_DIR})) \
$(if $(filter-out config,$*),${TASK}) $(set-run-cmd)
# # #
# Aliases
# # #
build: dkc-build
config: dkc-config
create: dkc-create
down: dkc-down
events: dkc-events
exec: dkc-exec
logs: dkc-logs
pause: dkc-pause
restart: dkc-restart
rm: dkc-rm
run: dkc-run
services: dkc-services
start: dkc-start
stop: dkc-stop
top: dkc-top
unpause: dkc-unpause
up: dkc-up
# custom decomposer cmds:
orphans: dkc-orphans
rund: dkc-rund
# # #
# HALP
# # #
define HELP_TXT :=
DECOMPOSING COMPOSE DEFINITIONS
Container composition files define service stacks constructed of
orchestrated tasks. Docker compose assumes one stack in a working directory
and one stack per composition file. Utilizing multiple stacks in different
configurations of similar services causes duplication of service and global
object declarations.
A decomposed docker-compose YAML file is therefore split into files in the stack/
and services/ folders, in .yml, .env, and .conf files. The purposes of these
files are:
- .env are aggregated into one file passed with `--env-file`
- stack/...yml defines top-level stack entities other than services
- service/...yml for each service, automatically aggregated
- stack/...conf to configure the execution of decomposer
TIP: `make dkc-config` to review the generated docker-compose.yml
ENV VARS
- COMPOSER_PROJECT_NAME [ derives the network name ]
- STACK_SERVICES [ $${STACK_ID}_STACK is appended if defined ]
- ENV_INCLUDES
- STACK [ derives STACK_ID (upper-case) and STACK_NAME (lower-case) ]
- TASK
- DK_CMP_OPTS [ options to docker-compose command ]
- RUN_CMD [ container commands ]
- CMD_ARGS [ options to RUN_CMD ]
EXAMPLES
STACK must (almost) always be defined. Set and export it in your session
for convenience. Setting TASK in your session has the potential to be risky.
STACK=lamp make up
STACK=lamp TASK=shell make run
make TASK=mysqld top
make config CMD_ARGS=--services
make build TASK=php8-apache DK_CMP_OPTS='--no-cache'
make exec TASK=php8-apache RUN_CMD='php -i'
[alias for run -d]
make rund TASK=shell RUN_CMD='php' CMD_ARGS="-r 'phpinfo();'"
ALIASES
Any docker-compose commands are available invoked with the prefix, "dkc-".
Invoke using the prefix if files or directories conflict with command names,
or if an alias does not exist. Most docker-compose commands are aliased
so they can be invoked simply e.g., `make build`, `make up`, etc.
MORE ON GETTING ORIENTED TO DECOMPOSER
For more on folder structure, environment variables and configuration files,
run `make dcp-orientation`.
MORE ON GETTING STARTED WITH DOCKER COMPOSE
If you are a developer still new to all of this infrastructure as code world,
run `make dkc-rtfm` for some sign-posts.
endef
define DCP_ORIENTATION
ORIENTATION TO DECOMPOSER
CONCEPTS
- Stack: -
abstraction of a compose-file (docker-compose.yml), or a collection of
services that share a network namespace.
- Service: -
a docker-compose service
- Task: -
a docker-compose service conventionally not part of a stack, declared
to be used with docker-compose run.
FOLDERS
- stack/ -
define stacks in .conf, .env, and .yml files, named by stack-name.
- <stack>.env will be passed in --env-file to docker-compose.
- <stack>.conf is included in make and effects decomposer
- <stack>.yml defines top-level docker-compose entities like volumes, config, etc.
- service/ -
Service declarations, intended to be composed into stacks, so excluding
any other top-level compose-file entities. NB: paths, such as build-context,
should be relative to the project directory.
DEFINING STACKS
A Stack requires a list of services, defined in the service/ folder, and
.env files, and a top-level .yml file for the entities
other than the services, e.g. networks and volumes.
Stack definitions are placed into the stack/ folder using the stack-name and
either a .conf, .yml, or .env extension. Environment files for services may
be placed in either the stack/ or services/ folders.
Environment files for services will be automatically aggregated, but
additional environment files can be added from the ENV_INCLUDES variable.
A stack named "web", would then have a web.conf file that defines the services
in a WEB_STACK variable, and any additional ENV_INCLUDES.
If defined, ${STACK_ID}_STACK is appended to the STACK_SERVICES list. The
suggested convention is to define ".._STACK" in your stack-conf file and
use STACK_SERVICES for ad hoc command line invocations.
ENVIRONMENT FILE PRECEDENCE
As multiple environment files are aggregated, later settings will
supersede earlier settings. This can be a double-edged sword, so attention
should be given to the precedence of automatic aggregation.
In order of precedence (reverse order of inclusion):
- variables from your executing shell environment
- exported variables in the stack/$${STACK}.conf
- files listed in ENV_INCLUDES (increasing order of precedence)
- stack/$${STACK}.env
- files automatically included (not from ENV_INCLUDES) in stack/
- files automatically included in service/
Observe that any declaration can be overridden by adding a file to the end
of the ENV_INCLUDES list. If services have env files, you can override them
in the stack/ folder.
If TASK is defined, the so-named env file rises in precedence over it's
peers in the folder but NOT over the stack/STACK.env or the ENV_INCLUDES files.
endef
define DKC_RTFM
MORE ON GETTING STARTED WITH DOCKER COMPOSE
If you are new-ish to docker-compose: here are the key concepts you need to
use decomposer.mk successfully. Because decomposer makes defaults explicit,
it can help you get a firmer grip on docker-compose by shedding light on
some of the magic.
- Project Name and Networks -
The current directory becomes the working path for building containers, and
is the default value of COMPOSER_PROJECT_NAME. By default, containers are
joined to the network created automatically by adding the suffix "_default"
to the COMPOSER_PROJECT_NAME.
https://stackunderflow.dev/p/network-namespaces-and-docker/
Services in your stack are automatically added to the routing services of the
network, so your services in a stack can all find each other just using the
name of the service. Neat.
Links are just aliases for hosts on the network.
- Volumes -
The two basic kinds of volumes to learn about are bind mounts and
named volumes. Bind mounts should be used sparingly, but are essential for
sharing code with your host machine and your containers. Your code will go in
a bind mount so you can write code and immediately run it in a container. Bind
mounts are not actually volumes, which are managed by docker, but they are
declared under volumes of a service declaration.
Named volumes are faster than bind mounts if you are not using Linux. Volumes
are not removed by default by docker-compose down; pass the -v flag to do so.
- Tasks, Services, and Stacks -
In cloud architecture, a Task is a unit of Container provisioning. A Service
definition in docker is based on a container image and each instance of the
container is referred to as a task. A set of services deployed together is
known as a Stack.
- Up, Start, Run, or Exec -
Up and Down are the docker-compose commands to create and run, and stop and
destroy sets of Services known as a Stack. Networks are removed by down,
unless they are still in-use. Volumes are not removed by default.
Start works either with a stack or a specified service, but does not try to
create containers and will fail if a container does not exist.
Run creates a new container, building the image if necessary, and can run a
command specified. Two important options to the run command are -d (--detach)
and --rm ("Remove"). Without --rm, your container will be preserved after
the command stops executing and will be left in an exited status. Detach
backgrounds the task so you can keep using your terminal. The stack command,
"down" will be the easiest way to clean-up stopped containers.
Exec runs a command in a running container. By default, it will create an
interactive terminal.
Cheat Sheet:
Build => images
Create => containers, build if needed
Start => Services (run), create if needed
Restart => Services
Run => Services, start with more options
Exec => running services, will not start
Pause => running services, and command execution
Unpause => resumes running command
Kill => SIGKILL by default but used to send process -s SIGNAL
Stop => services, does not remove
Remove => services, removes containers
Down => services, stop and remove
- Composing Composer Files -
Composer yaml files are meant to be composed. By supplying multiple Compose
files, the default docker-compose.yml is intended to be a base configuration
for your project. You can then implement different scenarios by adding overrides
and extensions to your base service definitions. This probably works great for
automation, but gets klunky quickly for a developer workflow and leads to a
lot of typing. Decomposer exploits this feature and makes it trivial to define
alternate modes as separate stacks and keeping declarations concise.
To create overrides, you need to provide the context of your YAML snipit so it can
be readily merged with Compose files passed to the command previously. So,
you will always provide a top-level declaration, such as "services:", "networks:",
"volumes:", "configs:", etc.
The second important concept is the environment file. By default, .env will
be used but an alternate canbe specified with --env-file option.
https://docs.docker.com/compose/env-file/
Tips and Gotchas:
- Variables from the environment override the env-file.
- Variables are not automatically provided to containers
- Variables are only resolved in YAML values.
- More on Variable Substitution, including defaults:
https://docs.docker.com/compose/compose-file/compose-file-v3/#variable-substitution
- Additional env-files can be declared in a YAML array.
DOCKER COMPOSE REFERENCES
- https://docs.docker.com/compose/reference/envvars/
- https://docs.docker.com/compose/compose-file/compose-file-v3/
- https://docs.docker.com/compose/reference/
endef
help:
$(info $(HELP_TXT))
dcp-orientation:
$(info $(DCP_ORIENTATION))
dkc-rtfm:
$(info $(DKC_RTFM))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment