Skip to content

Instantly share code, notes, and snippets.

@mpneuried
Last active April 19, 2024 21:06
Show Gist options
  • Save mpneuried/0594963ad38e68917ef189b4e6a269db to your computer and use it in GitHub Desktop.
Save mpneuried/0594963ad38e68917ef189b4e6a269db to your computer and use it in GitHub Desktop.
Simple Makefile to build, run, tag and publish a docker containier to AWS-ECR
# Port to run the container
PORT=4000
# Until here you can define all the individual configurations for your app
# You have to define the values in {}
APP_NAME=my-super-app
DOCKER_REPO={account-nr}.dkr.ecr.{region}.amazonaws.com
# optional aws-cli options
AWS_CLI_PROFILE={aws-cli-profile}
AWS_CLI_REGION={aws-cli-region}
# import config.
# You can change the default config with `make cnf="config_special.env" build`
cnf ?= config.env
include $(cnf)
export $(shell sed 's/=.*//' $(cnf))
# import deploy config
# You can change the default deploy config with `make cnf="deploy_special.env" release`
dpl ?= deploy.env
include $(dpl)
export $(shell sed 's/=.*//' $(dpl))
# grep the version from the mix file
VERSION=$(shell ./version.sh)
# HELP
# This will output the help for each task
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: help
help: ## This help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help
# DOCKER TASKS
# Build the container
build: ## Build the container
docker build -t $(APP_NAME) .
build-nc: ## Build the container without caching
docker build --no-cache -t $(APP_NAME) .
run: ## Run container on port configured in `config.env`
docker run -i -t --rm --env-file=./config.env -p=$(PORT):$(PORT) --name="$(APP_NAME)" $(APP_NAME)
up: build run ## Run container on port configured in `config.env` (Alias to run)
stop: ## Stop and remove a running container
docker stop $(APP_NAME); docker rm $(APP_NAME)
release: build-nc publish ## Make a release by building and publishing the `{version}` ans `latest` tagged containers to ECR
# Docker publish
publish: repo-login publish-latest publish-version ## Publish the `{version}` ans `latest` tagged containers to ECR
publish-latest: tag-latest ## Publish the `latest` taged container to ECR
@echo 'publish latest to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):latest
publish-version: tag-version ## Publish the `{version}` taged container to ECR
@echo 'publish $(VERSION) to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# Docker tagging
tag: tag-latest tag-version ## Generate container tags for the `{version}` ans `latest` tags
tag-latest: ## Generate container `{version}` tag
@echo 'create tag latest'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):latest
tag-version: ## Generate container `latest` tag
@echo 'create tag $(VERSION)'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# HELPERS
# generate script to login to aws docker repo
CMD_REPOLOGIN := "eval $$\( aws ecr"
ifdef AWS_CLI_PROFILE
CMD_REPOLOGIN += " --profile $(AWS_CLI_PROFILE)"
endif
ifdef AWS_CLI_REGION
CMD_REPOLOGIN += " --region $(AWS_CLI_REGION)"
endif
CMD_REPOLOGIN += " get-login --no-include-email \)"
# login to AWS-ECR
repo-login: ## Auto login to AWS-ECR unsing aws-cli
@eval $(CMD_REPOLOGIN)
version: ## Output the current version
@echo $(VERSION)
### THIS IST THE VERSION WITH docker-compose
# import config.
# You can change the default config with `make cnf="config_special.env" build`
cnf ?= config.env
include $(cnf)
export $(shell sed 's/=.*//' $(cnf))
# import deploy config
# You can change the default deploy config with `make cnf="deploy_special.env" release`
dpl ?= deploy.env
include $(dpl)
export $(shell sed 's/=.*//' $(dpl))
# grep the version from the mix file
VERSION=$(shell ./version.sh)
# HELP
# This will output the help for each task
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: help
help: ## This help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help
# DOCKER TASKS
# Build the container
build: ## Build the release and develoment container. The development
docker-compose build --no-cache $(APP_NAME)
docker-compose run $(APP_NAME) grunt build
docker build -t $(APP_NAME) .
run: stop ## Run container on port configured in `config.env`
docker run -i -t --rm --env-file=./config.env -p=$(PORT):$(PORT) --name="$(APP_NAME)" $(APP_NAME)
dev: ## Run container in development mode
docker-compose build --no-cache $(APP_NAME) && docker-compose run $(APP_NAME)
# Build and run the container
up: ## Spin up the project
docker-compose up --build $(APP_NAME)
stop: ## Stop running containers
docker stop $(APP_NAME)
rm: stop ## Stop and remove running containers
docker rm $(APP_NAME)
clean: ## Clean the generated/compiles files
echo "nothing clean ..."
# Docker release - build, tag and push the container
release: build publish ## Make a release by building and publishing the `{version}` ans `latest` tagged containers to ECR
# Docker publish
publish: repo-login publish-latest publish-version ## publish the `{version}` ans `latest` tagged containers to ECR
publish-latest: tag-latest ## publish the `latest` taged container to ECR
@echo 'publish latest to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):latest
publish-version: tag-version ## publish the `{version}` taged container to ECR
@echo 'publish $(VERSION) to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# Docker tagging
tag: tag-latest tag-version ## Generate container tags for the `{version}` ans `latest` tags
tag-latest: ## Generate container `{version}` tag
@echo 'create tag latest'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):latest
tag-version: ## Generate container `latest` tag
@echo 'create tag $(VERSION)'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# HELPERS
# generate script to login to aws docker repo
CMD_REPOLOGIN := "aws ecr"
ifdef AWS_CLI_PROFILE
CMD_REPOLOGIN += "--profile $(AWS_CLI_PROFILE)"
endif
ifdef AWS_CLI_REGION
CMD_REPOLOGIN += "--region $(AWS_CLI_REGION)"
endif
CMD_REPOLOGIN += "get-login --no-include-email"
repo-login: ## Auto login to AWS-ECR unsing aws-cli
@eval $(CMD_REPOLOGIN)
version: ## output to version
@echo $(VERSION)
# INSTALL
# - copy the files deploy.env, config.env, version.sh and Makefile to your repo
# - replace the vars in deploy.env
# - define the version script
# Build the container
make build
# Build and publish the container
make release
# Publish a container to AWS-ECR.
# This includes the login to the repo
make publish
# Run the container
make run
# Build an run the container
make up
# Stop the running container
make stop
# Build the container with differnt config and deploy file
make cnf=another_config.env dpl=another_deploy.env build
# Example version script.
# Please choose one version or create your own
# Node.js: grep the version from a package.json file with jq
jq -rM '.version' package.json
# Elixir: grep the version from a mix file
cat mix.exs | grep version | grep '\([0-9]\+\.\?\)\{3\}' -o
@joriskoris
Copy link

can i see the compose file?

@mpneuried
Copy link
Author

@joriskoris
I use the compose file version when i have to start other services like memcached for local testing/development with the project.
In case of Node i also redefine the Dockerfile to have a compiling of Coffee-Script/Type-Script/ES6 ... within the same server environment and copy it to the local machine. For releasing the final container i use the compiled files and copy them into a blank docker container without the required build tools.

Here examples of my Docker and Compose files:

Dockerdev

FROM node:8-alpine
MAINTAINER mpneuried

# build tools for native dependencies
RUN apk add --update make gcc g++ python git

WORKDIR /usr/src/

ADD package.json /usr/src/package.json
RUN npm install --silent

# place all build deps here

CMD [ "echo", "command to build your project" ]

docker-compose.yml

my-super-app:
    build: .
    dockerfile: "Dockerdev"
    env_file:
        - ./config.env
    volumes:
        - "./_src:/usr/src/_src"
        - "./lib:/usr/src/lib"
        - "./services:/usr/src/services"
        # more volumes
    links:
        - memcached
    ports:
        - "1337:4000"
memcached:
    image: memcached

Dockerfile

FROM node:8-alpine
MAINTAINER mpneuried

RUN mkdir -p /usr/src/
WORKDIR /usr/src/

COPY package.json /usr/src/package.json
COPY lib/ /usr/src/lib
COPY services/ /usr/src/services
# more volumes

RUN npm install --production --silent

EXPOSE 8002

CMD [ "node", "--max-old-space-size=128", "services/server.js" ]

@mpneuried
Copy link
Author

@kamalhussain You're right, the --no-cache in make release is in fact useless ;-)

@dcurletti
Copy link

This is awesome, thank you!

@nikhilw
Copy link

nikhilw commented Jan 3, 2018

Thanks for this, this is great.
One thing though, your help text for tag-latest and tag-version is exchanged; in both makefiles (docker and docker-compose).

@shapeofarchitect
Copy link

Hi Folks, what is the benefit it serves to maintain additional makefile when a separate service can manage their own Dockerfile and can be spin up by anyone , I would appreciate some reference docs or insight on the benefits to go down the road to maintain 2 files for a single service , in the kubernetes model it would just be a run time that would matter so how does this approach benefit ?

@v1k0d3n
Copy link

v1k0d3n commented Jan 14, 2019

@shapeofarchitect there are a bunch of articles on the benefits of makefiles or some build system like Bazel, so I won't go into a lot of detail; mainly for the sake of time, since I'm only passing through the interwebs and happened to run into this gist.

so I think what you're saying is that services like Dockerhub have auto-build/web-hook methods of providing artifacts and publishing these artifacts (am I correct in assuming this is what you're asking)? if so, consider this:

not all docker registries have auto-build/webhook methods that can take advantage of the --build-args feature in docker. for example, Quay, which has been our preference, continually says that they're releasing the feature, but I think that acquisition after acquisition has pushed down the priorities over the last couple of years, unfortunately.

so let's say that your docker registry of choice does not take advantage of the --build-args flags, as is our case. also, let's say that you have some dockerfile that looks like this (a very simple golang-based project):

FROM golang:1.11

ARG GOOS=linux
ARG GOARCH=amd64

ENV GOOS=$GOOS
ENV GOARCH=$GOARCH

WORKDIR /go/src/github.com/v1k0d3n/myapp
COPY . .

RUN go get -d -v ./...

RUN CGO_ENABLED=0 go build -ldflags '-w -s' -a -installsuffix cgo -o /myapp

FROM scratch AS build
COPY --from=0 /myapp /myapp

VOLUME /data

ENTRYPOINT [ "/myapp" ]
CMD [ "--help" ]

the first thing you'll probably recognize is that we're leveraging docker's multi-stage build process. you likely already know this, but for others landing on this gist who may not, it allows us to build a golang application from a private repo, use our ssh keys to pull down other private go-lang based dependencies we may require (our example application does) and then throw away the build environment/keys, leaving only the application binary.

there's really no good way to do this well with a build-system like Dockerhub (i could be wrong about this since I haven't checked in a while). golaang makes things a big tricky, since we need to build the version information into the application using -ldflags option at buildtime. if I were to do this all via bash, say build, package, and push that application to Quay, i could possibly do something like:

export VERSION="v0.2.4"
export COMMIT="$(git rev-parse --short HEAD)"
export REGISTRY="quay.io"
export NAMESPACE="v1k0d3n"

if [ $(git tag -l "${VERSION}") ]; then
  echo "Git tag ${VERSION} already exists. Continuing..."
else
  echo "Git tag \"${VERSION}\" not found! Creating tag based on commit \"${COMMIT}\"."
  git tag ${VERSION}
  git push origin ${VERSION}
fi

docker build --build-arg COMMIT="${COMMIT}" --build-arg VERSION="${VERSION}" -t "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}" .
docker build --build-arg COMMIT="${COMMIT}" --build-arg VERSION="${VERSION}" -t "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}-${COMMIT}" .
docker push "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}"
docker push "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}-${COMMIT}"

but makefiles have an additional logic that is a bit more build-oriented than bash in the fact that it can track which targets require rebuilding, etc. overall, my point is that with simple dockerfiles; sure you have an extremely valid point. once you start getting into more complex application builds using docker, the solution isn't as simple or straightforward. the person can create the dockerfile, if they have access to the repo, sure, but what if we want to (in this case) create a docker image and also build an artifact for the system that user may be on? in our case example above this application needs to run on MacOS, and Linux. I want a single build file to handle all of this logic based on a command structure.

make build
make test
make push

And a Makefile is the best way that users and CI platforms can accomplish this.

@fd98279
Copy link

fd98279 commented Apr 24, 2019

Do you get this error for make run?

run:
@echo "Run docker image $(APP_NAME) at port $(PORT)"
docker run -d -i -t --rm --env-file=./config.env -p=$(PORT):$(PORT) --name="$(APP_NAME)" $(APP_NAME)

vagrant@linux:$ make run
make: Circular 8888 <- 8888 dependency dropped.

-p argument PORT:PORT is causing make to treat that line a circular dependency

@fd98279
Copy link

fd98279 commented Apr 24, 2019

Escaping colon like this worked: $(PORT):$(PORT)

@SpyPower
Copy link

SpyPower commented Jul 8, 2019

Escaping colon like this worked: $(PORT):$(PORT)

You mentioned on https://gist.github.com/mpneuried/0594963ad38e68917ef189b4e6a269db#gistcomment-2896932 that you used -p $(PORT):$(PORT) and it caused make to treat that line as a circular dependency and then you mentioned on the quoted text exactly the same as a solution. Can you please clarify?

@SamyCoenen
Copy link

Isn't it a best practice to specify all as your default target instead of help https://www.gnu.org/prep/standards/html_node/Standard-Targets.html#Standard-Targets

@cbrunnkvist
Copy link

@SpyPower that error must have been due to @fd98279 forgetting to tab-indent the line properly. Unless indented, make would think the whole line is a target: dependencies kind of line and get confused by the colon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment