diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml index b1f767d7..9fd19b9e 100644 --- a/.github/linters/.markdown-lint.yml +++ b/.github/linters/.markdown-lint.yml @@ -7,3 +7,11 @@ MD013: heading_line_length: 100 # check code blocks? code_blocks: false + +# MD033/html - Inline HTML +MD033: + allowed_elements: ["details", "summary", "br", "small"] + +# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content +MD024: + allow_different_nesting: true diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile new file mode 100644 index 00000000..9f94a367 --- /dev/null +++ b/dockerfiles/Dockerfile @@ -0,0 +1,16 @@ + +FROM python:slim + +WORKDIR /myapp + +WORKDIR /app + +EXPOSE 80 443 + + + +CMD ["never used"] + +CMD ["python", "main.py"] + + diff --git a/dockerfiles/entrypoint/README.md b/dockerfiles/entrypoint/README.md new file mode 100644 index 00000000..c7c25471 --- /dev/null +++ b/dockerfiles/entrypoint/README.md @@ -0,0 +1,338 @@ +# Dockerfile ENTRYPOINT + +Table of Contents + +- [Lecture 1: What's an ENTRYPOINT?](#lecture-1-whats-an-entrypoint) + - [Why would you want this?](#why-would-you-want-this) + - [Exercise](#exercise) + - [Summary](#summary) +- [Lecture 2: USING ENTRYPOINT and CMD in the CLI](#lecture-2-using-entrypoint-and-cmd-in-the-cli) + - [Exercise](#exercise-1) + - [4 Rules](#4-rules) + - [Gotchas](#gotchas) + - [Exercise 2](#exercise-2) + - [Summary](#summary-1) +- [Lecture 3: Using ENTRYPOINT and CMD in Docker Compose](#lecture-3-using-entrypoint-and-cmd-in-docker-compose) + - [Exercise](#exercise-2) + - [Summary](#summary-2) +- [Quiz](#quiz) +- [Assignment: Build a curl image](#assignment-build-a-curl-image) + +## Lecture 1: What's an ENTRYPOINT? + +You previously learned the Dockerfile `CMD` instruction. That's the command that the container runs on start, by default. There's also the `ENTRYPOINT` instruction, which can also run things on start, but why do we have both of them and how are they different? + +Like `CMD`, the `ENTRYPOINT` instruction only runs on container start, not on image build. You can only have one of each, and you need at least one or the other so a container knows how to start. If you have two `CMD` instructions, the last one wins. Same with the `ENTRYPOINT` instruction. + +Every time you start a container, docker takes `ENTRYPOINT` and `CMD` and combines them into one long command with a space between them (e.g. `ENTRYPOINT` + [Space] + `CMD`). The benefits of this may not be immediately obvious, but it's a powerful feature that allows you to create custom images for command-line tools or for running pre-launch scripts as a container starts. + +For example, the official MySQL image makes use of an `ENTRYPOINT` to run a script before starting the MySQL daemon, which it does by copying the script into the image, making the `ENTRYPOINT`, and then having the `CMD` run the usual `mysqld` daemon. Here's a snippet from the Dockerfile: + +```dockerfile +COPY docker-entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["mysqld"] +``` + +The resulting command that Docker would execute on container start would then be: + +```shell +docker-entrypoint.sh mysqld +``` + +i.e., `ENTRYPOINT` + [space] + `CMD` + +### Why would you want this? + +Combining `ENTRYPOINT` and `CMD` provides you better flexiblity in creating Dockerfiles for command-line tools and scripts. Simply use an `ENTRYPOINT` as your base foundation, then add additional defaults using a `CMD` statement to build upon the `ENTRYPOINT`. Fortunately, if you want to make modifications to the defaults at runtime, you can override the command without replacing the `ENTRYPOINT`. + +Here's another example. The nginx image also uses a `docker-entrypoint.sh` for its `ENTRYPOINT`. + +[full example](https://github.com/nginxinc/docker-nginx/blob/2decc81a019b5df087c9162d3621b1c9beb3104f/mainline/debian/Dockerfile) + +By default, the image appends a command to the `ENTRYPOINT` which results in nginx running in the foreground. The `ENTRYPOINT` is used to add desired setup before nginx starts, like silencing the logs if an environment variable is set. Applying setup configuration is a great example for where you might want to use an `ENTRYPOINT`. + +### Exercise + +Let's create a custom image that uses the nginx binary directly as the `ENTRYPOINT` and configure nginx to print its help text with an additional command argument. + +[entrypoint-1 example](/dockerfiles/entrypoint-1/Dockerfile) + +Setting `ENTRYPOINT` will remove the `CMD` instruction defined in the nginx base image. So keep that in mind, if you need this, the `CMD` instruction must be redefined in the current image. + +Build and the image. + +```bash +docker build -t testnginx . +docker run --rm testnginx +``` + +Output: + +```shell +nginx version: nginx/1.21.4 +Usage: nginx [-?hvVtTq] [-s signal] [-p prefix] + [-e filename] [-c filename] [-g directives] + +Options: + -?,-h : this help + -v : show version and exit + -V : show version and configure options then exit + -t : test configuration and exit + -T : test configuration, dump it and exit + -q : suppress non-error messages during configuration testing + -s signal : send signal to a master process: stop, quit, reopen, reload + -p prefix : set prefix path (default: /etc/nginx/) + -e filename : set error log file (default: /var/log/nginx/error.log) + -c filename : set configuration file (default: /etc/nginx/nginx.conf) + -g directives : set global directives out of configuration file +``` + +### Summary + +So to recap an `ENTRYPOINT` allows you to configure the default executable for the container which can be extended with additional `CMD` options. Doing so allows you to create custom Dockerfiles for command-line tools and your own scripts. You'll get more practice in future lectures and learn how to modify `ENTRYPOINT` and `CMD` at runtime. + +Resources + +- +- + +## Lecture 2: USING ENTRYPOINT and CMD in the CLI + +You might be wondering if Docker provides a way to modify the `ENTRYPOINT` without creating a new image? Maybe you want to override the `ENTRYPOINT` at runtime the same way you can override the default `CMD` of an image. + +The `docker run` command has an optional `--entrpoint` flag for this. An entrypoint supplied in the command-line will overwrite any `ENTRYPOINT` defined in the Dockerfile. + +```shell +docker run --entrypoint IMAGE [command] [ARG...] +``` + +### Exercise + +Let's see an example that modifies both default entrypoint and command. In the previous lecture we saw how we could print nginx help text by creating a custom image. You can do the same thing in the command-line. + +Run: + +```shell +docker run --rm --entrypoint nginx nginx:1.21.4 -h +``` + +### 4 Rules + +There's 4 rules that describe how `CMD` and `ENTRYPOINT` interact. + +4 Rules For `CMD` & `ENTRYPOINT` + +1) Dockerfile should specify at least one of the `CMD` or `ENTRYPOINT` commands. +2) `ENTRYPOINT` should be defined when using the container as an executable. +3) `CMD` should be used as a way of defining default arguments for an ENTRYPOINT command or for executing an ad-hoc command in a container. +4) `CMD` will be overridden when running the container with alternative arguments. + +### Gotchas + +When you combine the `ENTRYPOINT` and the `CMD` [the resulting command may vary](https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact) based on usage of either _shell_ or _exec_ form in the Dockerfile. If you use shell form with `ENTRYPOINT`, any `CMD` or run command-line arguments will be ignored and `ENTRYPOINT` will be started as a subcommand of `/bin/sh -c` which doesn't pass Unix signals like `SIGTERM` from `docker stop `. + +### Exercise 2 + +Let's see a fun example in action. + +[entrypoint-2 example](dockerfiles/../entrypoint-2/Dockerfile) + +We have a silly bash script that iterates over the lyrics of Rick Astley's "Never gonna give you up". If `--link` or `-l` flags are passed to the `ENTRYPOINT` the script will print the YouTube link for the song. However, it shouldn't until we modify the Dockerfile because currently the `ENTRYPOINT` is using shell form. + +If you inspect the bash script, you'll see it makes use of a `trap`. A `trap` is used to capture most Unix signals when they occur and allows you to execute a command in response. In practice, this is often used to clean up resources before the script exits. + +> Note! `trap` cannot capture `SIGKILL` and `SIGSTOP`. [Learn more.](https://stackoverflow.com/questions/58139053/catch-sigstop-with-sigkill-before-gracefully) + +The `trap` syntax is: `trap [command] [signal...]` where one or more signal are separated by a space. + +We'll use `trap` to confirm using the `ENTRYPOINT` with shell form prevents Unix signals from being passed to our script. We'll also confirm that `CMD` commands and command-line arugments will be ignored. + +Let's run it. + +```bash +docker build -t rick . +docker run rick # CMD ["--link"] is ignored 😱 +docker run rick -l # command-line arguments are ignored +``` + +If you try to stop the containers with `docker stop ` the trap will not print the shutting down text. This confirms the `SIGTERM` signal is not passed to the script. Also, since the resulting commands never recieve the link flags this confirms the containers are tied to what we declared in the `ENTRYPOINT` using shell form. + +```shell +never gonna give you up +never gonna let you down +never gonna run around and desert you +never gonna make you cry +never gonna say goodbye +never gonna tell a lie and hurt you +``` + +Update the `ENTRYPOINT` in the Dockerfile to use exec form, then rebuild the image and try the commands again. This time the link flags should be appended to the `ENTRYPOINT`. Furthermore, the trap in the script will capture the `SIGTERM` triggered by `docker stop` as expected. Use `docker ps` to grab the container ID. + +```bash +docker ps +docker stop db532d2ef5d6 +``` + +The result: + +```shell +YouTube link: https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstley +never gonna give you up +never gonna let you down +never gonna run around and desert you +never gonna make you cry +never gonna say goodbye +shutting down +``` + +### Summary + +You're not forced to create a new image to make changes to the entrypoint. +Using the `--entrypoint` flag is an alternate approach while using `docker run`. + +Comamand-line arguments will be appended to the end of any `ENTRYPOINT`, unless the `ENTRYPOINT` is using shell form, in which case, all command-line arguments and Unix signals will be ignored by the `ENTRYPOINT`. + +Resources + +- +- +- + +## Lecture 3: Using ENTRYPOINT and CMD in Docker Compose + +We've seen how we can use entrypoint and command together in our Dockerfiles and with the `docker run` command, but how does it work with `docker-compose`? + +Fortunately, `docker-compose` version 2 and 3 support `entrypoint:` and `command:`. + +You can override the default command and entrypoint. List values are also supported by both keys. + +```shell +entrypoint: /app/entrypoint.sh +command: ["command", "--flag"] +``` + +### Exercise + +Let's modify the same example from previous lessons. Imagine you want to inspect the container filesystem of the nginx image. + +[entrypoint-3](/dockerfiles) + +```shell +docker-compose up +``` + +Output: + +```bash +[+] Running 1/1 + ⠿ Container test-nginx-1 Recreated 0.1s +Attaching to test-nginx-1 +test-nginx-1 | total 16 +test-nginx-1 | drwxr-xr-x 2 root root 4096 Nov 17 13:20 . +test-nginx-1 | drwxr-xr-x 3 root root 4096 Nov 17 13:20 .. +test-nginx-1 | -rw-r--r-- 1 root root 497 Nov 2 14:49 50x.html +test-nginx-1 | -rw-r--r-- 1 root root 615 Nov 2 14:49 index.html +test-nginx-1 exited with code 0 +``` + +> __! Note__ +> +> Using `entrypoint:` overrides both the default `ENTRYPOINT` on the image and removes any default `CMD`. +In other words, if there's a `CMD` instruction in the Dockerfile __it will be ignored__. So remember to set `command:` if this is not what you want. + +### Summary + +We can use `entrypoint:` and `command:` in our compose files to overwrite the `ENTRYPOINT` and `CMD` in our Dockerfiles. Remember that `entrypoint:` overrides both the default `ENTRYPOINT` and `CMD` of the image. + +Resources + +- +- + +--- + +## Quiz + +
+ +1. __When would you want to use both a CMD and ENTRYPOINT?__ + + A) When you want to configure the default executable for the container and extend it with additional command options. + + B) When the `ENTRYPOINT` is being sassy. + + C) When you want to use docker-compose. + + D) When you want to use a shell script. + +
+ See Quiz Answers +
+ + - __Answser: A__ + + B) Incorrect. This was joke. + + C) Incorrect. You can override entrypoints and commands in docker cli and docker-compose. + + D) Incorrect. Entrypoints don't have to be shell scripts to take advantage of commands. + +
+
+ +2. __Why does the resulting command vary when combining ENTRYPOINT and CMD?__ + + A) `ENTRYPOINT` and `CMD` behavior is unpredictable. + + B) Using _exec form_ and _shell form_ in the same Dockerfile creates varying results. + + C) Using _shell form_ in an `ENTRYPOINT` creates varying results. + + D) Using _exec form_ in an `ENTRYPOINT` creates varying results. + +
+ See Quiz Answers +
+ + - __Answser: C__ + + A) Incorrect. There are rules that govern how `ENTRYPOINT` and `CMD` interact. + + B) Incorrect. Using _exec form_ and _shell form_ in the same Dockerfile is not the true cause of variation. + + D) Incorrect. Variation has nothing to do with using _exec form_ in an `ENTRYPOINT`. + +
+
+ +3. __Which of the following statements are true?__ + + A) Using `entrypoint`: in a compose file requires the image to have `ENTRYPOINT` declared. + + B) Using `entrypoint:` in a compose file requires you to also set `command:`. + + C) `entrypoint:` in a compose file _only_ overwrites the `ENTRYPOINT` in the image. + + D) `entrypoint:` in a compose file overwrites the `ENTRYPOINT` and `CMD` in the image. + +
+ See Quiz Answers +
+ + - __Answser: D__ + + A) Incorrect. Using `entrypoint`: does not require the image to have `ENTRYPOINT` declared. + + B) Incorrect. Using `entrypoint:` does not require you to also set `command:`. + + C) Incorrect. `entrypoint:` overwrites the `ENTRYPOINT` and the `CMD` in the image. + +
+
+ +--- + +## Assignment: Build a curl image + +[see assignment-1](/dockerfiles/entrypoint-assignment-1) diff --git a/dockerfiles/entrypoint/entrypoint-1/Dockerfile b/dockerfiles/entrypoint/entrypoint-1/Dockerfile new file mode 100644 index 00000000..f02ef650 --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-1/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox:latest + +ENTRYPOINT ["hostname"] diff --git a/dockerfiles/entrypoint/entrypoint-3/Dockerfile b/dockerfiles/entrypoint/entrypoint-3/Dockerfile new file mode 100644 index 00000000..6dfb090b --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-3/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 +#FROM chainguard/node +FROM node:slim + +WORKDIR /app + +# Add Tini +ENV TINI_VERSION=v0.19.0 +ADD --chmod=555 https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-arm64 /tini +# ENTRYPOINT ["/tini", "--"] + +COPY . . + +RUN npm ci + +# ENTRYPOINT [""] + +ENTRYPOINT ./cmd.sh +#CMD ["node", "index.js"] \ No newline at end of file diff --git a/dockerfiles/entrypoint/entrypoint-3/cmd.sh b/dockerfiles/entrypoint/entrypoint-3/cmd.sh new file mode 100755 index 00000000..788632e9 --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-3/cmd.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +node ./index.js \ No newline at end of file diff --git a/dockerfiles/entrypoint/entrypoint-3/index.js b/dockerfiles/entrypoint/entrypoint-3/index.js new file mode 100644 index 00000000..3345487e --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-3/index.js @@ -0,0 +1,23 @@ +// this script is simple, it prints the date and time exactly every second to console + +const { exec } = require('child_process'); + +const printDateTime = () => { + const now = new Date(); + console.log(now.toISOString()); +}; + +printDateTime(); + +setInterval(printDateTime, 1000); + +// capture SIGTERM and SIGINT and exit gracefully +// process.on('SIGTERM', () => { +// console.log('SIGTERM signal received: exiting'); +// process.exit(0); +// }); + +// process.on('SIGINT', () => { +// console.log('SIGINT signal received: exiting'); +// process.exit(0); +// }); diff --git a/dockerfiles/entrypoint/entrypoint-3/package-lock.json b/dockerfiles/entrypoint/entrypoint-3/package-lock.json new file mode 100644 index 00000000..068bf0fa --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-3/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "entrypoint-3", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "entrypoint-3", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/dockerfiles/entrypoint/entrypoint-3/package.json b/dockerfiles/entrypoint/entrypoint-3/package.json new file mode 100644 index 00000000..18a98e5b --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-3/package.json @@ -0,0 +1,11 @@ +{ + "name": "entrypoint-3", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "" +} diff --git a/dockerfiles/entrypoint/entrypoint-cmd-1/Dockerfile b/dockerfiles/entrypoint/entrypoint-cmd-1/Dockerfile new file mode 100644 index 00000000..c07cc5ed --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-cmd-1/Dockerfile @@ -0,0 +1,10 @@ +FROM ubuntu:latest + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["curl"] + +CMD ["--help"] diff --git a/dockerfiles/entrypoint/entrypoint-cmd-2/Dockerfile b/dockerfiles/entrypoint/entrypoint-cmd-2/Dockerfile new file mode 100644 index 00000000..fef2a4fb --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-cmd-2/Dockerfile @@ -0,0 +1,8 @@ +FROM python:slim +USER www-data +WORKDIR /var/www/html +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +ENTRYPOINT ["./startup.sh"] +CMD ["python", "app.py"] diff --git a/dockerfiles/entrypoint/entrypoint-cmd-2/app.py b/dockerfiles/entrypoint/entrypoint-cmd-2/app.py new file mode 100644 index 00000000..773b1a3c --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-cmd-2/app.py @@ -0,0 +1,10 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route('/') +def hello_world(): + return 'Hello, World!' + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8000) \ No newline at end of file diff --git a/dockerfiles/entrypoint/entrypoint-cmd-2/requirements.txt b/dockerfiles/entrypoint/entrypoint-cmd-2/requirements.txt new file mode 100644 index 00000000..8ab6294c --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-cmd-2/requirements.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/dockerfiles/entrypoint/entrypoint-cmd-2/startup.sh b/dockerfiles/entrypoint/entrypoint-cmd-2/startup.sh new file mode 100755 index 00000000..fe34e010 --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-cmd-2/startup.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +# do some startup stuff +echo "Checking permissions on /var/www/html/upload" +mkdir -p /var/www/html/upload +chown -R www-data:www-data /var/www/html/upload + +# Start the application set in CMD +exec "$@" \ No newline at end of file diff --git a/dockerfiles/entrypoint/entrypoint-rick/Dockerfile b/dockerfiles/entrypoint/entrypoint-rick/Dockerfile new file mode 100644 index 00000000..c2602bed --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-rick/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu:20.04 +COPY /rick.sh / +ENTRYPOINT /rick.sh +CMD ["--link"] + +# CMD and UNIX signals are ignored here because +# shell form, ENTRYPOINT /rick.sh, is used instead of +# exec form, ENTRYPOINT ["/rick.sh"]. diff --git a/dockerfiles/entrypoint/entrypoint-rick/rick.sh b/dockerfiles/entrypoint/entrypoint-rick/rick.sh new file mode 100755 index 00000000..8838341a --- /dev/null +++ b/dockerfiles/entrypoint/entrypoint-rick/rick.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# capture SIGTERM signal, print message and exit +trap "echo shutting down && exit;" SIGTERM + +if [[ $1 == "--link" || $1 == "-l" ]] ; then + echo "YouTube link: https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstley" +fi + +for i in {-1..102} +do + nv="never gonna" + if [[ $(( ("$i" + 1) % 6)) == "0" ]] ; then echo "$nv give you up" ; fi + if [[ $(( ("$i" + 1) % 6)) == "1" ]] ; then echo "$nv let you down" ; fi + if [[ $(( ("$i" + 1) % 6)) == "2" ]] ; then echo "$nv run around and desert you" ; fi + if [[ $(( ("$i" + 1) % 6)) == "3" ]] ; then echo "$nv make you cry" ; fi + if [[ $(( ("$i" + 1) % 6)) == "4" ]] ; then echo "$nv say goodbye" ; fi + if [[ $(( ("$i" + 1) % 6)) == "5" ]] ; then echo "$nv tell a lie and hurt you" ; fi + sleep 2 +done + +exec "$@" \ No newline at end of file diff --git a/docs/images/entrypoint-cmd-4rules.png b/docs/images/entrypoint-cmd-4rules.png new file mode 100644 index 00000000..da042e93 Binary files /dev/null and b/docs/images/entrypoint-cmd-4rules.png differ diff --git a/docs/images/entrypoint-cmd-compose.png b/docs/images/entrypoint-cmd-compose.png new file mode 100644 index 00000000..69daa734 Binary files /dev/null and b/docs/images/entrypoint-cmd-compose.png differ diff --git a/docs/images/entrypoint-cmd.png b/docs/images/entrypoint-cmd.png new file mode 100644 index 00000000..7c2e231c Binary files /dev/null and b/docs/images/entrypoint-cmd.png differ diff --git a/docs/images/entrypoint-flag.png b/docs/images/entrypoint-flag.png new file mode 100644 index 00000000..2474cc80 Binary files /dev/null and b/docs/images/entrypoint-flag.png differ diff --git a/swarm-stack-1/example-voting-app-stack.yml b/swarm-stack-1/example-voting-app-stack.yml index ccce2888..67782453 100644 --- a/swarm-stack-1/example-voting-app-stack.yml +++ b/swarm-stack-1/example-voting-app-stack.yml @@ -1,4 +1,4 @@ -version: "3.9" +version: "3.13" services: redis: @@ -27,6 +27,7 @@ services: image: bretfisher/examplevotingapp_vote ports: - 5000:80 + - 5002:80 networks: - frontend depends_on: