Course contents
Lesson 3 — Your first Dockerfile
By Dorian Chávez · founder and CEO of Hábil ·
It is 6:40 on a Friday evening and the window to update the server closes at 7. The plan was simple: stop the old revisor, start the new one, go home. You type docker stop and the terminal goes silent. One second. Three. Seven. At ten, it finally comes back. You try it on the next copy and again: exactly ten seconds of silence. You check the logs and the line the revisor always writes when it shuts down, apagado limpio, is missing; the last report was left half-written. Someone on the team finds the container's state and reads it out loud: ExitCode=137. Nobody killed it on purpose, and yet it was killed. The culprit is not in the revisor's code, which knows how to shut down properly. It is in a three-word line at the end of its Dockerfile, one that looks harmless and that many tutorials write exactly that way. This lesson teaches you to write that line, and all the others, knowing what each one does.
Time: 2 × 45 min
What you build: the first image of the revisor, curso-docker-revisor:1.0.0, in a single stage, with its Dockerfile and its .dockerignore. It runs unprivileged, shuts down in a fraction of a second, and carries a version in its name.
What you learn: what a Dockerfile is and what each of its basic instructions does (FROM, WORKDIR, COPY, RUN, USER, EXPOSE, CMD); the difference between the exec form and the shell form, measured with a stopwatch; what the build context is and what .dockerignore is for; why the first Dockerfile already includes USER; how to name and tag an image with a version; and how to check a Dockerfile before building it with docker build --check.
Where you are coming from. From Lesson 2 you bring a feel for the day to day: you start a container with docker run, look at it with ps, logs, inspect, and exec, publish its port only on 127.0.0.1, and delete it without leaving leftovers. There you built the test service's image with a single command, without asking what was in its Dockerfile. From Lesson 0 you bring the map: a container is a process; an image is the template it is born from; and code 137 means the kernel killed the process with signal 9. That last idea is going to come back here in a different guise. From the course you also bring the revisor's code, in programas/revisor/, exactly as the course delivers it: an HTTP service written in Go that does not have a Dockerfile yet. You do not need to know Go: you will not change a single line of the program.
What this lesson does not do. It does not make the image small: the one that comes out of here weighs more than a gigabyte, and that is fine for now, because seeing it big is the best way to understand later why it is worth shrinking it (the course plan leaves that for Lesson 5). Nor does it order the Dockerfile so that rebuilding is fast, or explain the layers one by one: that is the subject of Lesson 4. It does not connect the revisor to other containers (which is why, in this lesson, the revisor checks itself), and it does not give it health checks or secrets. A single stage, a single image, everything in plain view.
About the runs. All outputs are real and were obtained on October 21, 2026 on a Mac with Colima, which starts a virtual machine with Ubuntu 24.04.4, the same base as Linux Mint 22, the course's reference system. The scripts were run inside that virtual machine, with Docker 29.2.1 and Buildx v0.31.1 (the first line of docker build --check says building with "default" instance, as it will on your Mint). That same machine was being used by other runs at the same time, and was heavily loaded, so, to avoid collisions, the scripts were launched with a different name prefix and a different port: they have the variables PREFIJO (default curso-docker-) and PUERTO (default 8080), and they were run with PREFIJO=curso-docker-l03- and PUERTO=18030. In the outputs where names and port appear, the lesson shows the default values, which are the ones you will see: curso-docker-revisor instead of curso-docker-l03-revisor and 8080 instead of 18030 (and the width of the tables, which depends on the longest name, was left as it came out); that is the only change, and the .salida.txt files keep the run as it was. What does not change with the variables, because it happens inside the container, appears as is: the internal port 8080. There are eight programs: the usual scripts fig03_02, fig03_04, fig03_05, fig03_07 and fig03_08, plus fig03_09 (the "In your folder" setup of 3.6.1, assembled in a temporary folder and with the tag 1.0.0-carpeta, so as not to touch yours), fig03_12 (the two Dockerfiles with defects for the exercises, fig03_10.Dockerfile and fig03_11.Dockerfile) and fig03_13 (how far a .dockerignore pattern reaches). The scripts' t helper falls back to python3 when the system's date does not give milliseconds, as is the case on macOS; on Mint and on the virtual machine it is not needed. The times in this lesson (build, shutdown) came out with the machine loaded, so on your Mint they should be lower or similar; the 10 seconds of the shell form do not depend on load. The course was planned with Docker Engine 29.8.2, and the latest version published that day was 29.9.2, from October 20, 2026 (release notes); nothing in this lesson changes between those versions. What does change on your Mint, and it is flagged where it appears: the process numbers (PIDs), the times, and, if your machine is Intel or AMD, the architecture (amd64 instead of arm64), which is not printed here but does decide which variant of the Go image gets downloaded. The programs are in programas/03-primer-dockerfile/, each one with its output next to it; estado/ keeps the Dockerfile and the .dockerignore as they end up when finished.
By the end you will be able to
- Write a single-stage Dockerfile for an existing program and say, line by line, whether that instruction acts when building the image or when running the container.
- Build an image with a name and a version tag, and explain why the course's
revisoris calledcurso-docker-revisor:1.0.0and notrevisororrevisor:latest. - Show with
docker exec ... idthat your container does not run asroot, and explain whatUSER 10001:10001changes. - Write a
.dockerignoreand verify, by looking inside the image, that a.envfile did not make the trip. - Measure how long
docker stoptakes withCMDin exec form and in shell form, and explain withdocker topwhy one takes a fraction of a second and the other a little over 10 s with code 137. - Run
docker build --checkbefore building and read theJSONArgsRecommendedwarning.
The why before the how
In Lesson 2 you worked with an image that was handed to you ready-made. It works, but there is a catch: if you do not know how it was put together, you do not know what it contains or how it is going to behave. Does it run as root? Does it carry your configuration files inside? Does it shut down when you ask it to, or does it wait to be killed? Those three questions are not answered by looking at the running container; they are answered by reading its recipe.
An image's recipe is called a Dockerfile. It is a text file with instructions, one per line, that Docker reads from top to bottom to produce the image. The documentation defines it this way: Docker builds images by reading the instructions from a Dockerfile, "a text file containing instructions for building your source code" (Docker, "Dockerfile overview"); and its reference describes it as a text document with all the commands a user could run on the command line to assemble an image (Dockerfile reference). What matters for you is that the recipe is repeatable: it describes the same process on your machine, on your teammate's and on the server, and that is why an image is fixed by changing its recipe and rebuilding, not by getting into the container. (For two builds to give an identical image you also have to pin the base by its fingerprint, as you will in Lesson 5; with a tag, the base can change underneath you, as you will see in 3.3.1.) What you called in Lesson 0 "you do not fix an image by getting into the container" has its other half here: an image is fixed by changing its recipe and rebuilding.
And every line of that recipe is a decision. The scene at the beginning was not caused by a programming error: it was caused by writing CMD revisor instead of CMD ["revisor"]. They look almost the same. This is the difference you will measure in section 3.7, with the same image and the same program:
## docker stop con forma exec
tardó 562 ms
## docker stop con forma shell
tardó 10365 ms
ExitCode=137
A few tenths of a second against ten seconds and a corpse. Those are this lesson's run numbers. The milliseconds change (that machine was heavily loaded); the ten seconds and the 137 do not. The lesson has seven parts:
- What building is (3.1): the recipe, the context and who does the work.
- The
revisor's Dockerfile, line by line (3.2). - The name and the tag (3.3): why
curso-docker-revisor:1.0.0. USER: not running asroot(3.4).- The context and
.dockerignore(3.5), with a fake secret that sneaks in. - Building, failing and running (3.6), and the exec form versus the shell form (3.7), with the stopwatch.
- Checking before building (3.8) and the old things you will find in tutorials (3.9).
The concepts
3.1 What building an image is
3.1.1 The recipe, the ingredients and the kitchen
Building an image has three components, and it is worth separating them from the start because each one fails in a different way:
- The recipe: the Dockerfile.
- The ingredients: the files the recipe can use. They are called the build context, and almost always they are the contents of a folder. In the command
docker build ., the final dot is that folder: "the context is this folder I am in". - The kitchen: the builder, which in Docker is called BuildKit and is part of the daemon. Remember Lesson 0: the
dockerclient does not do the work; it asks the daemon. When building, the client sends it the recipe and the ingredients the recipe asks for, and the daemon cooks.
The build documentation says it with these same components: the context is the set of files the build has access to, and the Dockerfile instructions can only take files from there (Docker, "Build context"). This has a practical consequence you will use in 3.5: what is not in the context cannot get into the image, and what is in the context can get in even if you do not want it to.
3.1.2 Two moments: building and running
There is an idea that organizes everything else in this lesson: a Dockerfile describes two different moments.
- When building (
docker build): Docker starts from a base image, copies files and runs commands. What results is stored in the image. This happens once, on your machine or on a build server. - When running (
docker run): Docker creates a container from that image and starts one process. This happens every time someone runs the image, on any machine.
Some instructions act in the first moment and others only leave a note of what to do in the second. If you confuse the two moments, you write things like a RUN that tries to start the server (it would run at build time, would sit waiting, and the build would never finish) or you expect an EXPOSE to open a port (it only notes it). The table in 3.2.2 separates each instruction by moment.
3.2 The revisor's Dockerfile, line by line
3.2.1 The complete recipe
This is the Dockerfile the lesson ends with. It goes in the root of the revisor's project, next to go.mod, and it is called exactly Dockerfile, with no extension, which is the name docker build looks for by default:
# syntax=docker/dockerfile:1
# fig03_01.Dockerfile — Lección 3: el primer Dockerfile del revisor: una sola etapa.
FROM golang:1.27.3-trixie
WORKDIR /src
COPY . .
RUN go build -o /usr/local/bin/revisor ./cmd/revisor
# Un usuario sin privilegios, por número: la imagen no necesita tenerlo en /etc/passwd.
USER 10001:10001
EXPOSE 8080
CMD ["revisor"]
Twelve lines, of which seven are instructions. Let us take them one by one.
# syntax=docker/dockerfile:1. It looks like a comment, and for almost all of the file it is, but on the first line it is a directive: it tells BuildKit which version of the Dockerfile syntax to use. The reference explains it like this: it is used to "declare the Dockerfile syntax version to use for the build", and it lets you use new features without upgrading Docker (Dockerfile reference, syntax). The 1 means "the latest version 1". That is why, in the build outputs, you will see Docker first resolve docker.io/docker/dockerfile:1: it goes for the recipe's interpreter before reading it. All the course's Dockerfiles start with this line.
FROM golang:1.27.3-trixie. Every image starts from another (the only exception is the empty image scratch, which you will not need in this course). FROM says which is the base image: here, the official Go one in its version 1.27.3, built on Debian 13, whose codename is trixie. The revisor is written in Go and compiling it requires the Go compiler; this image brings it, along with a whole Debian. Docker Hub lists that tag among the current ones of the Go image, and clarifies that names like bookworm or trixie are the codenames of the Debian versions it is built on (golang on Docker Hub).
WORKDIR /src. It sets the working folder inside the image: everything that comes afterwards (COPY, RUN, CMD) happens there. If the folder does not exist, Docker creates it (reference, WORKDIR). It is better than writing RUN cd /src, which does not work: each RUN starts afresh and the cd is lost when that line ends.
COPY . .. It copies from the context (the first dot) to the image's working folder (the second dot, which here is /src). It is the only door through which your files enter the image. Copying everything is the simplest and the most dangerous: everything in the context goes in. That is why the .dockerignore of 3.5 exists. (Copying everything at once also makes rebuilding slow; the course plan deals with that in Lesson 4.)
RUN go build -o /usr/local/bin/revisor ./cmd/revisor. It runs a command at build time and stores the result in the image. Here it compiles the revisor and leaves the executable at /usr/local/bin/revisor, a folder that is already in the image's PATH variable. That is why further down it is enough to write revisor, without a path.
USER 10001:10001. From here on, and above all at run time, the process uses user 10001 and group 10001 instead of root. It has its own section, 3.4.
EXPOSE 8080. It notes that the program listens on port 8080. It only notes it: the reference is clear that EXPOSE "does not actually publish the port"; to publish it you need -p when running (reference, EXPOSE). It serves as documentation for whoever reads the image and for tools that query it. The port is published, as you learned in Lesson 2, with -p 127.0.0.1:8080:8080.
CMD ["revisor"]. It notes which process to start when running the container. Only one can be effective: if you write several CMDs, the last one wins (reference, CMD). And it is a default value: if you pass docker run a command after the image name, that command replaces the CMD (Docker, "Running containers"). The brackets and the quotes are not decoration: they are the exec form, the subject of 3.7.
3.2.2 Each instruction in its moment
| Instruction | When it acts | What it leaves in the image |
|---|---|---|
FROM |
at build time | all the contents of the base image |
WORKDIR |
at build time, and notes the folder for running | the folder, and the value of where to start |
COPY |
at build time | the copied files |
RUN |
at build time | what the command wrote to disk |
USER |
notes for running (and for the RUNs that come after) |
the user value |
EXPOSE |
only notes | the port value; it opens nothing |
CMD |
only notes | the command each container will start |
Read the third column carefully: the last three instructions do not add files, they add data to the image's configuration. You will be able to read them with docker inspect and, later on, in the image's history. Keep in mind that order matters: a USER that goes before a RUN makes that RUN execute as that user. That is why here USER goes after compiling: the compilation needs to write to root's folders, the server does not.
3.3 The name and the tag
3.3.1 The parts of a name
When you build, you name the image with -t. An image's full name has this form, according to the docker image tag documentation: [HOST[:PORT]/]NAMESPACE/REPOSITORY[:TAG] (docker image tag). In practice almost everything can be omitted: golang:1.27.3-trixie writes neither the registry nor the namespace, because Docker fills in docker.io/library/ (the library namespace is that of the official images; you already saw that full name in Lesson 2, in naming to docker.io/library/…). For an image that only lives on your machine, two parts are enough:
- the repository, which says what it is:
curso-docker-revisor; - the tag, after the colon, which says which of its versions:
1.0.0.
The same documentation says something worth underlining: "if no tag is given, Docker uses latest". That is, latest does not mean "the newest". It means "they did not give it a tag". An image built two years ago without a tag is called latest just like one from today. In the scene at the beginning, had the images been called revisor:latest, you could not even have known which version was running when the problem started.
Tags, besides, are nicknames that move. Look at the base image's line on Docker Hub: the same content carries at the same time the tags 1.27.3-trixie, 1.27-trixie, 1-trixie and trixie (golang on Docker Hub). When Go 1.27.4 comes out, 1.27-trixie will point to something else and 1.27.3-trixie will stay where it is. That is why this lesson's FROM uses the most specific tag there is. Not even that is an absolute guarantee, because whoever publishes can push a tag again: the best practices guide warns that "image tags are mutable" and that to really pin a version you have to use its fingerprint (digest) (best practices). The course plan leaves that step for Lesson 5; here it is enough not to use latest.
3.3.2 Why curso-docker-revisor and not revisor
The course plan, in its lesson table, already says that here you build curso-docker-revisor:1.x.0 and not revisor. It is worth stopping at that prefix, because it is a lesson in itself: the course prefix.
Since Lesson 0, everything the course creates on your machine starts with curso-docker-: the containers, the networks, the volumes and, from now on, the images. That prefix does three things for you:
- It avoids clashes.
revisoris a name anyone would use. If your machine already has arevisor:1.0.0image from another project (from another Hábil course, say), yourdocker build -t revisor:1.0.0takes the name away from it without asking: the old image is left without a tag and the new one takes its place. It is not hypothetical: on the machine where this lesson was run there was already an image calledrevisor:1.0.0, from another project, which has nothing to do with this Dockerfile. With the prefix, the two coexist. - It lets you clean up only what is yours. Docker's filters work by name.
docker ps -a --filter name=curso-docker-lists only the course's containers, anddocker image ls --filter reference='curso-docker-*'only its images (the form that thedocker image lsreference describes for wildcards). Deleting "everything from the course" becomes a safe command, which does not touch the databases or the images of other projects you may have on the same machine. - It says where it came from. Six months from now, when you clean up and find
curso-docker-revisor:1.0.0, you will know what it is without investigating.
The revisor's versioning rule in this course is simple: every time a lesson changes the image, the version goes up. This one is 1.0.0: the first that works completely. When the Dockerfile changes, the tag is not reused; another is created.
3.4 USER: the first Dockerfile no longer runs as root
3.4.1 What happens if you say nothing
In Lesson 0 you saw that root inside a container is user 0 of the same kernel, with fourteen capabilities enabled. Now the concrete question: if your Dockerfile says nothing about the user, who does the revisor run as? It depends on the base image. fig03_02.sh asks the Go image:
#!/usr/bin/env bash
# fig03_02.sh — Sin USER, la imagen base corre como root.
# Pregunta con qué usuario arranca la imagen de Go que usa el revisor, y compara
# con un usuario sin privilegios elegido por número.
set -euo pipefail
PREFIJO="${PREFIJO:-curso-docker-}"
IMAGEN=golang:1.27.3-trixie
docker pull -q "$IMAGEN" >/dev/null
echo "== 1. Lo que la imagen base declara como usuario (vacío = root)"
docker image inspect --format 'User="{{.Config.User}}"' "$IMAGEN"
echo
echo "== 2. Quién soy dentro, sin decir nada"
docker run --rm --name "${PREFIJO}quien" "$IMAGEN" id
echo
echo "== 3. Quién soy dentro, con el usuario 10001:10001"
docker run --rm --name "${PREFIJO}quien" --user 10001:10001 "$IMAGEN" id
echo
echo "== 4. ¿Existe ese usuario en /etc/passwd de la imagen?"
docker run --rm --name "${PREFIJO}quien" "$IMAGEN" \
sh -c 'grep -q ":10001:" /etc/passwd && echo "sí está" || echo "no está"'
echo
echo "== 5. ¿Puede ese usuario abrir un puerto bajo? (primer puerto sin privilegios)"
docker run --rm --name "${PREFIJO}quien" --user 10001:10001 "$IMAGEN" \
cat /proc/sys/net/ipv4/ip_unprivileged_port_start
echo
echo "== 6. Lo mismo con la red del anfitrión (--network host)"
docker run --rm --name "${PREFIJO}quien" --network host --user 10001:10001 "$IMAGEN" \
cat /proc/sys/net/ipv4/ip_unprivileged_port_start
echo
echo "== 7. Y el valor del propio anfitrión (sin contenedor)"
cat /proc/sys/net/ipv4/ip_unprivileged_port_start
$ ./fig03_02.sh
== 1. Lo que la imagen base declara como usuario (vacío = root)
User=""
== 2. Quién soy dentro, sin decir nada
uid=0(root) gid=0(root) groups=0(root)
== 3. Quién soy dentro, con el usuario 10001:10001
uid=10001 gid=10001 groups=10001
== 4. ¿Existe ese usuario en /etc/passwd de la imagen?
no está
== 5. ¿Puede ese usuario abrir un puerto bajo? (primer puerto sin privilegios)
0
== 6. Lo mismo con la red del anfitrión (--network host)
0
== 7. Y el valor del propio anfitrión (sin contenedor)
0
This output is stable. The first time, docker pull downloads the Go image, which weighs hundreds of megabytes; be patient. --user does at run time the same thing USER does in the Dockerfile, and here it is used to test the user without building anything.
Read it from top to bottom. The base image declares an empty user, and empty means root: block 2 confirms it, with uid=0(root). A Dockerfile that starts from golang and says nothing inherits that, and the revisor would serve network requests as your kernel's user 0. With user 10001, block 3 says uid=10001 gid=10001, with no name in parentheses. And block 4 explains why: that user does not exist in the image's /etc/passwd.
3.4.2 A number is enough
That is correct, and it is on purpose. For the kernel, a user is a number; the name is a convenience that lives in /etc/passwd. The USER reference accepts a name or a number, in the form USER <UID>[:<GID>] (reference, USER), and writing both numbers has two advantages. The first: there is no need to create the user in the image. That is enough for the revisor, which does not ask for its name or its home folder; a program that does will find that its home folder is /, where it cannot write (it is what you will see in Solution 1). For those cases the user is created in the image, or a base that already has one is used, like Lesson 5's nonroot. The second: the group is fixed; if you give a user without a group and that user has no primary group, the reference warns that the process runs with root's group. With 10001:10001 there are no surprises.
Why 10001? Because it is far from the numbers distributions use for their own users and for the team's people (on Mint, your user is usually 1000). A high, dedicated number reduces the chance that the container's process coincides with a real user of the host.
The best practices guide asks for it bluntly: "if a service can run without privileges, use USER to change to a non-root user" (best practices, USER). The revisor needs nothing from root: it listens on 8080, a high port, and writes to no system folders. (Ports below 1024 are the ones that, on Linux, require root or a special capability; the first one that does not is in net.ipv4.ip_unprivileged_port_start, which by default is 1024 and is a value per network namespace (kernel documentation). Inside a Docker container with its own network, that value comes as 0: block 5 of fig03_02.sh measured it and it gave 0, which means any port can be opened without privileges. But that is not a law: with --network host the container shares the host's network and sees the host's value. On this lesson's machine that value is also 0 (the virtual machine sets it that way), so blocks 5, 6 and 7 give 0 and do not tell the cases apart; on a freshly installed Mint you should see 0 in 5 and 1024 in 6 and 7. That difference was not measured here.) So from the first Dockerfile it runs as 10001. The course plan puts it like this: security is not a lesson, it is a habit.
3.5 The context and .dockerignore
3.5.1 What travels ends up inside
Now the dangerous ingredient: COPY . .. It copies the whole context. In a real project, the folder holds more than code: a .git with the history, a .env with passwords for testing on your machine, a .tar backup you left there last week. fig03_04.sh builds the revisor with this lesson's Dockerfile and a fake .env file in the folder, first without a .dockerignore and then with one, and looks at what ended up in /src inside the image:
#!/usr/bin/env bash
# fig03_04.sh — Lo que viaja en el contexto acaba dentro de la imagen.
# Construye el revisor con un archivo .env de mentira en la carpeta: primero SIN
# .dockerignore y después CON el de fig03_03. Mira qué quedó en /src cada vez.
set -euo pipefail
PREFIJO="${PREFIJO:-curso-docker-}"
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
SIN="${PREFIJO}revisor:1.0.0-sin-ignore"
CON="${PREFIJO}revisor:1.0.0-con-ignore"
trap 'docker rmi -f "$SIN" "$CON" >/dev/null 2>&1 || true; rm -rf "$TRABAJO"' EXIT
# El proyecto: el código que entrega el curso + el Dockerfile de la lección.
cp -R "$AQUI/../revisor/." "$TRABAJO/"
cp "$AQUI/fig03_01.Dockerfile" "$TRABAJO/Dockerfile"
cd "$TRABAJO"
# Un secreto DE MENTIRA, como el que cualquiera deja en la carpeta del proyecto.
echo 'REVISOR_TOKEN=curso-docker-env-de-mentira-3c1d' > .env
echo "== 1. SIN .dockerignore: lo que hay en /src dentro de la imagen"
docker build -q -t "$SIN" . >/dev/null
docker run --rm --name "${PREFIJO}mira" "$SIN" ls -A /src
echo "-- y el .env, leído desde la imagen:"
docker run --rm --name "${PREFIJO}mira" "$SIN" cat /src/.env
echo
echo "== 2. CON .dockerignore: lo que hay en /src dentro de la imagen"
cp "$AQUI/fig03_03.dockerignore" .dockerignore
docker build -q -t "$CON" . >/dev/null
docker run --rm --name "${PREFIJO}mira" "$CON" ls -A /src
$ ./fig03_04.sh
== 1. SIN .dockerignore: lo que hay en /src dentro de la imagen
.env
Dockerfile
cmd
go.mod
internal
-- y el .env, leído desde la imagen:
REVISOR_TOKEN=curso-docker-env-de-mentira-3c1d
== 2. CON .dockerignore: lo que hay en /src dentro de la imagen
cmd
go.mod
internal
This output is stable. The script works in a temporary folder (mktemp -d) so as not to dirty programas/revisor/, which is the code the course delivers and which is not edited, and it deletes its two images on exit. docker build -q builds quietly (quiet); ls -A also lists files that start with a dot. The two builds take the same as a normal one, because each compiles again: in this lesson's run, the whole script took a little over a minute, with the base image already downloaded.
Look at the first block. The .env got into the image and can be read with a cat. Anyone who has the image —your teammate, the server, a public registry if you ever push it— also has that secret. And you do not need to run the image to extract it: it is enough to save it to a file and open it. The Dockerfile itself also got in, which is of no use inside. In the second block, with the .dockerignore, /src has only what the program needs in order to compile: cmd, go.mod and internal.
3.5.2 The lesson's .dockerignore
This is the file. It is called exactly .dockerignore, with the dot, and goes at the root of the context, next to the Dockerfile:
# fig03_03.dockerignore
# Lo que no entra a la imagen. (El Dockerfile y este archivo sí viajan al
# constructor porque los necesita, pero COPY no los mete en la imagen.)
.git
.env
*.tar
Dockerfile*
.dockerignore
Each line is a pattern, similar to shell wildcards; lines with # are comments (Docker, "Build context", .dockerignore files). The documentation says where it is looked for: the client looks for a file called .dockerignore "in the root directory of the context". Each line has its reason:
.git: the project's complete history. It can weigh much more than the code and can hold secrets that someone deleted from the code but not from the history..env: the place where people keep passwords for testing on their machine. It is exactly the file in the figure.*.tar: backups and exported files that stay in the folder without anyone remembering. Mind the scope: this pattern only reaches the.tarfiles at the root of the context; one inside a subfolder keeps traveling. It was checked withfig03_13.sh(below). To exclude them at any level, the pattern is**/*.tar.Dockerfile*and.dockerignore: the recipe does not have to be inside the dish. And then how does Docker build, if you exclude the Dockerfile? The documentation answers: those two files "are sent to the builder anyway, because it needs them to build"; what the pattern prevents isCOPYfrom putting them into the image. The asterisk inDockerfile*also reaches theDockerfile.forma-shellof 3.7.
fig03_13.sh measures it with a .tar at the root and another in respaldos/, and lists those that reached the image:
#!/usr/bin/env bash
# fig03_13.sh — Dónde alcanza un patrón de .dockerignore: «*.tar» contra «**/*.tar».
# Construye una imagen mínima con un .tar en la raíz y otro en una subcarpeta, y
# lista los .tar que llegaron adentro. Borra lo que crea.
set -euo pipefail
PREFIJO="${PREFIJO:-curso-docker-}"
TRABAJO="$(mktemp -d)"
IMAGEN="${PREFIJO}ctx:1.0.0-tar"
trap 'docker rmi -f "$IMAGEN" >/dev/null 2>&1 || true; rm -rf "$TRABAJO"' EXIT
cd "$TRABAJO"
mkdir respaldos
touch raiz.tar respaldos/enero.tar
printf 'FROM golang:1.27.3-trixie\nCOPY . /ctx\n' > Dockerfile
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
echo '*.tar' > .dockerignore
echo "## .dockerignore con: *.tar"
docker build -q -t "$IMAGEN" . >/dev/null
muestra "docker run --rm --name ${PREFIJO}mira $IMAGEN find /ctx -name '*.tar'"
echo
echo '**/*.tar' > .dockerignore
echo "## .dockerignore con: **/*.tar"
docker build -q -t "$IMAGEN" . >/dev/null
muestra "docker run --rm --name ${PREFIJO}mira $IMAGEN find /ctx -name '*.tar'"
$ ./fig03_13.sh
## .dockerignore con: *.tar
$ docker run --rm --name curso-docker-mira curso-docker-ctx:1.0.0-tar find /ctx -name '*.tar'
/ctx/respaldos/enero.tar
## .dockerignore con: **/*.tar
$ docker run --rm --name curso-docker-mira curso-docker-ctx:1.0.0-tar find /ctx -name '*.tar'
With *.tar, the one at the root stayed out and respaldos/enero.tar got into the image; with **/*.tar, neither did. This output is stable.
A reading detail: a line that starts with ! makes an exception ("this one yes, even if an earlier pattern excludes it"), and when several patterns match a file, the last one wins. You do not need it today, but you will see it in other people's projects.
3.6 Building, failing and running
3.6.1 Building
With the Dockerfile and the .dockerignore in the revisor's folder, the build is a single command. fig03_05.sh assembles the project in a temporary folder, builds, triggers the first error, runs the service properly, queries it and stops it:
#!/usr/bin/env bash
# fig03_05.sh — Construir el revisor, verlo fallar sin configuración, correrlo
# bien y detenerlo. CMD en forma exec. Borra lo que crea al salir.
set -euo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-8080}"
LIMITES="--memory 64m --cpus 0.5 --pids-limit 50" # los tres límites de la Lección 2
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
# El proyecto: el código que entrega el curso + los archivos de esta lección.
cp -R "$AQUI/../revisor" "$TRABAJO/revisor"
cp "$AQUI/estado/Dockerfile" "$AQUI/estado/.dockerignore" "$TRABAJO/revisor/"
cp "$AQUI/fig03_06.Dockerfile" "$TRABAJO/revisor/Dockerfile.forma-shell"
cd "$TRABAJO/revisor"
titulo() { printf '\n## %s\n' "$*"; }
# muestra: imprime el comando con «$ » y lo ejecuta (salida y error juntos)
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
# t: milisegundos desde la época, para medir duraciones
t() {
local x; x=$(date +%s%3N)
case "$x" in *N) python3 -c 'import time; print(int(time.time() * 1000))' ;; *) echo "$x" ;; esac
}
esperar_http() {
for _ in $(seq 40); do curl -fsS -o /dev/null "$1" 2>/dev/null && return 0; sleep 0.5; done
echo "(no respondió $1)"; return 1
}
trap 'docker rm -f ${PREFIJO}revisor >/dev/null 2>&1; docker rmi -f ${PREFIJO}revisor:1.0.0 >/dev/null 2>&1; rm -rf "$TRABAJO"' EXIT
titulo "Construir y medir"
muestra "docker build -q -t ${PREFIJO}revisor:1.0.0 . >/dev/null && docker image ls ${PREFIJO}revisor:1.0.0 --format '{{.Repository}}:{{.Tag}} {{.Size}}'"
titulo "El error: arrancar sin configuración"
muestra "docker run --name ${PREFIJO}revisor ${LIMITES} ${PREFIJO}revisor:1.0.0"
muestra "docker ps -a --filter name=${PREFIJO}revisor --format '{{.Names}} {{.Status}}'"
docker rm ${PREFIJO}revisor >/dev/null
titulo "Correrlo bien"
muestra "docker run -d --name ${PREFIJO}revisor -p 127.0.0.1:${PUERTO}:8080 ${LIMITES} -e REVISOR_SERVICIOS='propio http://127.0.0.1:8080/salud' ${PREFIJO}revisor:1.0.0 >/dev/null"
esperar_http "http://127.0.0.1:${PUERTO}/salud"
muestra "curl -s http://127.0.0.1:${PUERTO}/salud"
muestra "curl -s 'http://127.0.0.1:${PUERTO}/api/estados?formato=tabla' | sed -E 's/[0-9.]+(ms|µs)/<tiempo>/'"
muestra "docker exec ${PREFIJO}revisor id"
muestra "docker top ${PREFIJO}revisor -o pid,user,args"
titulo "docker stop con forma exec"
inicio=$(t); docker stop ${PREFIJO}revisor >/dev/null; fin=$(t)
echo "tardó $(( fin - inicio )) ms"
muestra "docker logs ${PREFIJO}revisor"
The script is long because of the helpers at the beginning, which you will recognize in the following ones: muestra prints each command with a $ in front before running it, so that in the output you know what produced what, and if the command fails, it records its exit code instead of stopping the script; t gives the time in milliseconds for timing; esperar_http asks for the service's health until it answers, because a freshly started container takes a moment to listen. The first part of the output:
## Construir y medir
$ docker build -q -t curso-docker-revisor:1.0.0 . >/dev/null && docker image ls curso-docker-revisor:1.0.0 --format '{{.Repository}}:{{.Tag}} {{.Size}}'
curso-docker-revisor:1.0.0 1.45GB
1.45 GB. The compiled revisor is a single file of a few megabytes; everything else is what the base image brings: a whole Debian, the Go compiler and its tools, plus what the compilation left in the image. None of that is needed to run the revisor, only to build it. Note the number in your logbook: it is the starting point, and the course plan devotes Lesson 5 to bringing it down. On your machine the number may vary a little depending on the architecture.
In your folder: the image that stays
The course scripts work in temporary folders and delete what they create on exit: they are for seeing and measuring, not for leaving you anything. Your project, on the other hand, has to stay, because Lesson 4 starts from it. From the root of the course repository, assemble your folder and build the image you are going to keep:
mkdir -p ~/curso-docker/revisor
cp -R programas/revisor/. ~/curso-docker/revisor/
cp programas/03-primer-dockerfile/estado/Dockerfile \
programas/03-primer-dockerfile/estado/.dockerignore ~/curso-docker/revisor/
cd ~/curso-docker/revisor
ls -A
docker build -q -t curso-docker-revisor:1.0.0 .
docker image ls curso-docker-revisor --format '{{.Repository}}:{{.Tag}} {{.Size}}'
$ ls -A
.dockerignore
Dockerfile
cmd
go.mod
internal
$ docker build -q -t curso-docker-revisor:1.0.0 .
sha256:1eb7de835928…
$ docker image ls curso-docker-revisor --format '{{.Repository}}:{{.Tag}} {{.Size}}'
curso-docker-revisor:1.0.0 1.45GB
The sha256:… that docker build -q prints is the image's identifier on your machine and changes with every build; it was trimmed here. (The fingerprint used to pin a base, the one from 3.3.1, is the one the registry publishes; Lesson 5 uses it.) This output is from fig03_09.sh, which does these same commands in a temporary folder and with the tag curso-docker-revisor:1.0.0-carpeta (and the run prefix from "About the runs"), so as not to touch yours; here it is shown with the tag you are going to use, and it is saved as is in fig03_09.salida.txt. The order in which ls -A lists the files may vary with your system's language. The cp -R programas/revisor/. … copies the contents of the folder, so repeating it does not nest a revisor/revisor. This is fig03_09.sh, in case you want to see how the run was assembled:
#!/usr/bin/env bash
# fig03_09.sh — «En tu carpeta»: armar la carpeta del revisor y construir la imagen
# que se queda. Aquí se hace en una carpeta temporal y con la etiqueta 1.0.0-carpeta,
# para no tocar tu ~/curso-docker ni tu curso-docker-revisor:1.0.0. Borra lo que crea.
set -euo pipefail
# Nombres por omisión: los del curso. Se pueden cambiar para no chocar con otra
# corrida en el mismo equipo (PREFIJO=... ./fig03_09.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
IMAGEN="${PREFIJO}revisor:1.0.0-carpeta"
trap 'docker rmi -f "$IMAGEN" >/dev/null 2>&1 || true; rm -rf "$TRABAJO"' EXIT
# muestra: imprime el comando con «$ » y lo ejecuta (salida y error juntos)
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
# Los comandos del bloque de la lección, desde la raíz del repositorio del curso;
# la carpeta ~/curso-docker es aquí una carpeta temporal.
cd "$AQUI/../.."
mkdir -p "$TRABAJO/curso-docker/revisor"
cp -R programas/revisor/. "$TRABAJO/curso-docker/revisor/"
cp programas/03-primer-dockerfile/estado/Dockerfile \
programas/03-primer-dockerfile/estado/.dockerignore "$TRABAJO/curso-docker/revisor/"
cd "$TRABAJO/curso-docker/revisor"
muestra "ls -A"
muestra "docker build -q -t $IMAGEN ."
muestra "docker image ls ${PREFIJO}revisor --format '{{.Repository}}:{{.Tag}} {{.Size}}'"
From now on, when the lesson says "in the revisor folder", it means ~/curso-docker/revisor, and this curso-docker-revisor:1.0.0 is the one that stays on your machine when you finish. You can type the commands in 3.6.2 and 3.6.3 exactly as written there, with a caution stated in each one: only one container at a time can have the name curso-docker-revisor.
3.6.2 The first error: starting without configuration
## El error: arrancar sin configuración
$ docker run --name curso-docker-revisor --memory 64m --cpus 0.5 --pids-limit 50 curso-docker-revisor:1.0.0
revisor: falta REVISOR_SERVICIOS (ejemplo: REVISOR_SERVICIOS="prueba http://curso-docker-prueba:9000/ 2s")
[código de salida: 1]
$ docker ps -a --filter name=curso-docker-revisor --format '{{.Names}} {{.Status}}'
curso-docker-revisor Exited (1) Less than a second ago
The image is fine; the container started and the revisor refused to work. The revisor is configured through environment variables, and the only mandatory one is REVISOR_SERVICIOS, the list of services it must check. Without it, it prints what is missing, with an example, and exits with code 1. docker ps -a shows it as Exited (1): the process ended on its own, with an error of its own. Compare it with the 137 from Lesson 0: there, someone killed the process; here, the process decided to leave. Knowing how to read that difference saves you from looking for the problem in the wrong place. Notice also that the container was left created even though it ended: the script deletes it with docker rm before going on, because the name is taken. If you type the commands yourself, do the same before moving on to 3.6.3: docker rm curso-docker-revisor.
3.6.3 Running it properly
## Correrlo bien
$ docker run -d --name curso-docker-revisor -p 127.0.0.1:8080:8080 --memory 64m --cpus 0.5 --pids-limit 50 -e REVISOR_SERVICIOS='propio http://127.0.0.1:8080/salud' curso-docker-revisor:1.0.0 >/dev/null
$ curl -s http://127.0.0.1:8080/salud
{"estado":"vivo","version":"desarrollo"}
$ curl -s 'http://127.0.0.1:8080/api/estados?formato=tabla' | sed -E 's/[0-9.]+(ms|µs)/<tiempo>/'
SERVICIO ESTADO TIEMPO DETALLE
propio OK <tiempo> 200
$ docker exec curso-docker-revisor id
uid=10001 gid=10001 groups=10001
$ docker top curso-docker-revisor -o pid,user,args
PID USER COMMAND
2246176 10001 revisor
The process number changes on every run; everything else is stable, except for the time in the table: the command's sed is there to mask it when it comes out in milliseconds, and in this run it came out that way, which is why you see <tiempo> (in another run it may come out 0s, and then there is nothing to mask).
-p 127.0.0.1:8080:8080 and the three limits (--memory 64m --cpus 0.5 --pids-limit 50) are Lesson 2's habits, put in from the revisor's first container: the port only for your machine and a cap on memory, CPU and processes. Those numbers are Lesson 2's, not a measurement of the revisor; if more is needed later on, it is measured and raised. -e passes an environment variable to the container. Here the revisor checks itself: the list has a single service, propio, which points at its own health. It is a way of checking that everything works without another container, because connecting containers to each other is a subject for networks and the course plan leaves it for Lesson 8. The table says OK and 200.
/salud answers "version":"desarrollo". It is not an error: the image was built without stamping a version into the program, and the revisor is made to give that away. Mind the difference: the image has the tag 1.0.0, but the program inside does not know which version it is. They are two different things, and joining them is the job of a later lesson.
Now the last two lines, which are the important ones of this section. docker exec ... id says uid=10001: the Dockerfile's USER worked. And docker top, which lists a container's processes as seen from the host, shows a single process: revisor, owned by user 10001. That single line is just the exec form working. Keep it in mind, because in the next section you will see one with two lines. When you finish testing: docker stop curso-docker-revisor && docker rm curso-docker-revisor, so that the name is free and docker ps -a is clean.
3.7 Exec form versus shell form
3.7.1 Two ways of writing the same thing
CMD (and also RUN and ENTRYPOINT, CMD's sister instruction that sets the image's main program and which you will use from Lesson 5) can be written in two ways (Dockerfile reference, shell and exec forms):
- Exec form: a list in JSON format, with brackets and double quotes:
CMD ["revisor"]. Docker starts that program directly, with nothing in between. - Shell form: plain text:
CMD revisor. Docker wraps it in a shell; on Linux, in/bin/sh -c "revisor". The reference puts it like this: unlike the exec form, instructions in shell form "always use a shell".
The shell form exists for a reason: with a shell in the middle you can use variables ($HOME), pipes (|) and chain commands (&&). That is why it is used a lot in RUNs and it is fine to use it there. The problem appears when you use it for the container's main process, the one CMD starts.
To see it, the lesson brings a copy of the Dockerfile in which only the last line changes. It exists only for comparison:
# syntax=docker/dockerfile:1
# fig03_06.Dockerfile — Lección 3: la MISMA imagen, pero con CMD en forma shell. Existe solo para
# comparar: así NO se escribe.
FROM golang:1.27.3-trixie
WORKDIR /src
COPY . .
RUN go build -o /usr/local/bin/revisor ./cmd/revisor
USER 10001:10001
EXPOSE 8080
CMD revisor
In the project it is saved as Dockerfile.forma-shell, and built with docker build -f Dockerfile.forma-shell: the -f option tells Docker which recipe to use when it is not the one called Dockerfile.
3.7.2 The measurement
First, the half you have already seen: the end of fig03_05.sh, which stops the container with the exec form and shows its logs:
## docker stop con forma exec
tardó 562 ms
$ docker logs curso-docker-revisor
revisor desarrollo escuchando en :8080 (1 servicio(s))
revisor: GET /salud 200 0s
revisor: GET /salud 200 0s
revisor: GET /salud 200 0s
revisor: GET /api/estados 200 1ms
revisor: recibí terminated, apagando
revisor: apagado limpio
The times (562 ms, and the 0s and 1ms of each request) change on every run; the sequence of lines is stable, although the number of GET /salud depends on how many times esperar_http asked before the service answered. The two final lines are the ones that matter: the revisor received the signal (terminated is the name Go gives SIGTERM), finished what it was doing and said goodbye. All in 562 milliseconds in this run, with the machine heavily loaded.
Now the other half. fig03_07.sh builds the image with the shell form, runs it with the same configuration, looks at its processes and stops it:
#!/usr/bin/env bash
# fig03_07.sh — La MISMA imagen con CMD en forma shell: quién es el PID 1,
# cuánto tarda docker stop y con qué código muere. Borra lo que crea al salir.
set -euo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-8080}"
LIMITES="--memory 64m --cpus 0.5 --pids-limit 50" # los tres límites de la Lección 2
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
# El proyecto: el código que entrega el curso + los archivos de esta lección.
cp -R "$AQUI/../revisor" "$TRABAJO/revisor"
cp "$AQUI/estado/Dockerfile" "$AQUI/estado/.dockerignore" "$TRABAJO/revisor/"
cp "$AQUI/fig03_06.Dockerfile" "$TRABAJO/revisor/Dockerfile.forma-shell"
cd "$TRABAJO/revisor"
titulo() { printf '\n## %s\n' "$*"; }
# muestra: imprime el comando con «$ » y lo ejecuta (salida y error juntos)
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
# t: milisegundos desde la época, para medir duraciones
t() {
local x; x=$(date +%s%3N)
case "$x" in *N) python3 -c 'import time; print(int(time.time() * 1000))' ;; *) echo "$x" ;; esac
}
esperar_http() {
for _ in $(seq 40); do curl -fsS -o /dev/null "$1" 2>/dev/null && return 0; sleep 0.5; done
echo "(no respondió $1)"; return 1
}
trap 'docker rm -f ${PREFIJO}revisor-shell >/dev/null 2>&1; docker rmi -f ${PREFIJO}revisor:1.0.0-shell >/dev/null 2>&1; rm -rf "$TRABAJO"' EXIT
titulo "docker stop con forma shell"
docker build -q -t ${PREFIJO}revisor:1.0.0-shell -f Dockerfile.forma-shell . >/dev/null
docker run -d --name ${PREFIJO}revisor-shell ${LIMITES} -e REVISOR_SERVICIOS='propio http://127.0.0.1:8080/salud' ${PREFIJO}revisor:1.0.0-shell >/dev/null
sleep 1
muestra "docker top ${PREFIJO}revisor-shell -o pid,user,args"
inicio=$(t); docker stop ${PREFIJO}revisor-shell >/dev/null; fin=$(t)
echo "tardó $(( fin - inicio )) ms"
muestra "docker inspect --format 'ExitCode={{.State.ExitCode}}' ${PREFIJO}revisor-shell"
muestra "docker logs ${PREFIJO}revisor-shell"
The helpers at the beginning are the same as in fig03_05.sh. The tag 1.0.0-shell and the name curso-docker-revisor-shell are support items: they live only as long as the script lasts.
## docker stop con forma shell
$ docker top curso-docker-revisor-shell -o pid,user,args
PID USER COMMAND
2254874 10001 /bin/sh -c revisor
2254924 10001 revisor
tardó 10365 ms
$ docker inspect --format 'ExitCode={{.State.ExitCode}}' curso-docker-revisor-shell
ExitCode=137
$ docker logs curso-docker-revisor-shell
revisor desarrollo escuchando en :8080 (1 servicio(s))
The process numbers change; the time changes by a few hundred milliseconds, but it always ends up a little above 10 seconds; ExitCode=137 and the one-line log are stable. It is the scene at the beginning, in full.
3.7.3 What happened: the signal that did not arrive
Put the two docker top outputs side by side. With the exec form there was one process, revisor. With the shell form there are two: /bin/sh -c revisor and, as its child, revisor. The shell started first, so the shell is the container's process number 1 (remember Lesson 0: inside the container, the main program is number 1). The revisor was left as its child.
Now, the docker stop chain, according to its documentation: it sends the container's main process the SIGTERM signal, waits for a deadline that by default is 10 seconds on Linux, and if the process is still alive, it sends SIGKILL (docker container stop). The "main process" is number 1. With the exec form, number 1 is the revisor, which is written to handle SIGTERM: it receives it, shuts down in an orderly way and exits. With the shell form, number 1 is /bin/sh.
Here two facts come together. The first: the shell does not forward the signal to its child. The explanation of the JSONArgsRecommended rule says it verbatim: with the shell form, the program "runs as a child process of a shell, which does not pass signals along" (Docker, JSONArgsRecommended). The Dockerfile reference says it of ENTRYPOINT's shell form in so many words: the executable "will not be the container's PID 1 and will not receive Unix signals" (reference, ENTRYPOINT). The second fact is the kernel's and is subtler: process number 1 of a process namespace is special. docker stop sends the signal from outside the container, from the host's namespace, which is the container's "ancestor". For that case, the Linux manual says that process number 1 only receives the signals for which it has established a handler, and that SIGKILL and SIGSTOP are the exception: those two "are forcibly delivered" when they come from an ancestor namespace (pid_namespaces(7)). The shell did not install a handler for SIGTERM, so the signal does not even kill it: it is lost.
With that, the output reads itself:
docker stopsendsSIGTERMto number 1, the shell. It is lost. Therevisor, underneath, never finds out.- Ten seconds pass. That is almost all the time you measured: 10,365 ms is the 10,000 of the deadline plus what Docker and the kernel take (with the machine loaded).
docker stopsendsSIGKILL, which, according to the signals manual, "cannot be caught, blocked, or ignored" (signal(7)). The shell and therevisordie at once.- Docker records
ExitCode=137. You already know how to read it since Lesson 0: 128 plus 9, and 9 isSIGKILL's number. - The log has a single line. It does not say
recibí terminatedbecause therevisorreceived nothing, and it does not sayapagado limpiobecause it did not shut down: it was killed in the middle of what it was doing. If at that instant it was writing a report, the report was left half-written.
Notice that the revisor's code is exactly the same in both images. It knows how to shut down: it demonstrated it in a few tenths of a second. What changed is that, with a shell in the middle, the signal never reached it. And it is not worth finding out how each base image's shell behaves: with the exec form there is no shell in the middle and the question disappears.
Lessons 0 and 2 left you this problem noted for later. Here you saw the half that depends on the Dockerfile: the exec form puts your program as process number 1. Lesson 7 brings the other half: what obligations that process number 1 has toward the processes it creates (--init), how to change the signal with STOPSIGNAL and how to measure shutdown together with health.
3.7.4 When you do need a shell
Sometimes the startup needs something only a shell gives, such as reading a variable. The same rule page gives two ways out that remain honest: writing a small startup script and calling it in exec form, or declaring the shell on purpose with the SHELL instruction. In both cases the program is once again a child of a shell and the signal is once again your responsibility, for example with exec at the end of the script, which replaces the shell with your program. The revisor needs none of that: it reads its variables itself. The practical rule of this course ends up like this: CMD and ENTRYPOINT, always in exec form.
3.8 Checking before building: docker build --check
What you have just measured with a stopwatch, Docker can detect by reading the recipe, without building anything. docker build --check runs the build checks: rules that look for common errors in the Dockerfile. The documentation says that, instead of executing the build steps, the command "only runs the checks and reports what it finds", and that they exist since version 1.8 of the Dockerfile syntax and Buildx 0.15.0 (Docker, "Build checks"). Your Docker ships a much newer version. fig03_08.sh checks the lesson's two Dockerfiles:
#!/usr/bin/env bash
# fig03_08.sh — Revisar los dos Dockerfile ANTES de construir, con
# docker build --check. No construye nada ni crea contenedores.
set -euo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-8080}"
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
# El proyecto: el código que entrega el curso + los archivos de esta lección.
cp -R "$AQUI/../revisor" "$TRABAJO/revisor"
cp "$AQUI/estado/Dockerfile" "$AQUI/estado/.dockerignore" "$TRABAJO/revisor/"
cp "$AQUI/fig03_06.Dockerfile" "$TRABAJO/revisor/Dockerfile.forma-shell"
cd "$TRABAJO/revisor"
titulo() { printf '\n## %s\n' "$*"; }
# muestra: imprime el comando con «$ » y lo ejecuta (salida y error juntos)
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
# t: milisegundos desde la época, para medir duraciones
t() {
local x; x=$(date +%s%3N)
case "$x" in *N) python3 -c 'import time; print(int(time.time() * 1000))' ;; *) echo "$x" ;; esac
}
esperar_http() {
for _ in $(seq 40); do curl -fsS -o /dev/null "$1" 2>/dev/null && return 0; sleep 0.5; done
echo "(no respondió $1)"; return 1
}
trap 'rm -rf "$TRABAJO"' EXIT
titulo "Revisar el Dockerfile antes de construir"
muestra "docker build --check ."
muestra "docker build --check -f Dockerfile.forma-shell ."
The first, the good one:
## Revisar el Dockerfile antes de construir
$ docker build --check .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 372B 0.0s done
#1 DONE 0.0s
#2 resolve image config for docker-image://docker.io/docker/dockerfile:1
#2 DONE 0.2s
#3 docker-image://docker.io/docker/dockerfile:1@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e
#3 resolve docker.io/docker/dockerfile:1@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e 0.0s done
#3 CACHED
#4 [internal] load metadata for docker.io/library/golang:1.27.3-trixie
#4 DONE 0.2s
#5 [internal] load .dockerignore
#5 transferring context: 231B done
#5 DONE 0.0s
Check complete, no warnings found.
The times and the long digest after dockerfile:1@sha256: change (that digest is the exact version of the syntax interpreter that Docker used that day); the size in bytes of transferring dockerfile and of transferring context also change, since they depend on the files' comments. What is stable is the sequence of steps and the last line. The numbered steps are what BuildKit did: load the recipe (#1), resolve the interpreter the # syntax line asks for (#2 and #3), ask for the base image's metadata without downloading it entirely (#4) and read the .dockerignore (#5). There was no COPY or RUN: nothing was built. And at the end, Check complete, no warnings found.
The second, the shell-form one:
$ docker build --check -f Dockerfile.forma-shell .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile.forma-shell
#1 transferring dockerfile: 355B done
#1 DONE 0.0s
#2 resolve image config for docker-image://docker.io/docker/dockerfile:1
#2 DONE 0.1s
#3 docker-image://docker.io/docker/dockerfile:1@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e
#3 resolve docker.io/docker/dockerfile:1@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e done
#3 CACHED
#4 [internal] load metadata for docker.io/library/golang:1.27.3-trixie
#4 DONE 0.1s
#5 [internal] load .dockerignore
#5 transferring context: 231B done
#5 DONE 0.0s
Check complete, 1 warning has been found!
WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for CMD to prevent unintended behavior related to OS signals
Dockerfile.forma-shell:10
--------------------
8 | USER 10001:10001
9 | EXPOSE 8080
10 | >>> CMD revisor
11 |
--------------------
[código de salida: 1]
There is the scene at the beginning, found before building. The warning has four parts. The rule's name, JSONArgsRecommended, with the address of its explanation. The sentence: "JSON arguments recommended for CMD to prevent unintended behavior related to OS signals". The exact place: Dockerfile.forma-shell:10. And the line pointed to with >>>. Besides, the command exits with code 1: the documentation clarifies that, unlike a normal build, with --check any violation makes the command end with a non-zero code. That is what makes it useful on a build server: a warning stops the process instead of getting lost on the screen.
A normal build (docker build without --check) also runs these checks and shows the same warning, but it ends successfully and builds the image anyway. It is easy not to see it among hundreds of lines. That is why the habit is to run --check beforehand, on purpose. And if you want no build to pass with warnings, the Dockerfile can declare it on its second line, right after # syntax, with # check=error=true: that way the warning also stops a normal docker build (Docker, "Build checks").
3.9 What you will find in old tutorials
You will read other people's Dockerfiles, and some carry things this course does not use. It is worth recognizing them:
MAINTAINER name. It declared the image's author. It is deprecated: the reference indicates thatLABELis "a much more flexible version" and that it should be used instead, for exampleLABEL org.opencontainers.image.authors="..."(reference,MAINTAINER).ADDto copy local files.ADDdoes more thanCOPY: it also downloads from the internet and decompresses. That magic is surprising. The best practices guide usesCOPYfor copying and reservesADDfor when you really need to bring in something remote (best practices,ADDorCOPY). In this courseADDis not needed.FROM something:latestorFROM somethingwithout a tag. You already know why not (3.3).CMD commandandENTRYPOINT commandin shell form. You already know how to measure why not (3.7).- Installing
sudoin the image. The guide advises against it because of its unpredictable behavior with terminals and signal forwarding. If something needsrootat build time, it goes before theUSER; at run time, it should not need it.
The error you will see
This lesson has three errors, from least to most serious. You have already triggered all three.
1. The docker build --check warning.
WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for CMD to prevent unintended behavior related to OS signals
Dockerfile.forma-shell:10
It reads like this: there is a rule that is not met (JSONArgsRecommended); its explanation is at the address; and line 10 of Dockerfile.forma-shell is the culprit. It is fixed by changing CMD revisor to CMD ["revisor"]. If you really need a shell at startup, see 3.7.4. What is not done is to ignore the warning because "the image built fine": it builds fine and shuts down badly.
2. The container that exits with 1 at startup.
revisor: falta REVISOR_SERVICIOS (ejemplo: REVISOR_SERVICIOS="prueba http://curso-docker-prueba:9000/ 2s")
curso-docker-revisor Exited (1) Less than a second ago
It reads like this: the container started, the program read its configuration, did not find something mandatory and exited on its own with 1. It is not a problem of the image or of Docker. The first question when faced with a just-started Exited (1) is always docker logs <container>: the cause is almost always there, written by the program itself. It is fixed by passing the variable with -e.
3. The 10-second docker stop and code 137.
tardó 10365 ms
ExitCode=137
It reads like this: the main process did not handle SIGTERM, the deadline ran out and Docker killed it with SIGKILL (137 = 128 + 9). Before blaming the program, look at docker top <container>: if the first line is /bin/sh -c ..., the signal stayed in the shell. It is fixed with CMD in exec form. There are other possible causes of a 137 —the lack of memory from Lesson 0 is the other big one, and it is told apart because docker inspect says OOMKilled=true—, but when docker stop's time is exactly the deadline, the main suspect is this one. What you do not do is raise docker stop's deadline so that it "is enough": a longer deadline only lengthens the wait, because the signal still does not arrive.
The security habit of the lesson
The first Dockerfile already carries USER, .dockerignore and a tag with a version. All three are cheap on day one and expensive to add afterwards, when there are already containers running and people who depend on them.
USER 10001:10001keeps a bug in therevisor—or in any library it uses— from handing whoever exploits it your kernel's user 0. You checked it withdocker exec ... id..dockerignoreprevents the accident of 3.5: a.env, a key or the history of.gitstuffed into an image that then travels to other machines. You checked it by looking at/srcinside the image.- The tag with a version and the prefix keep you from running "whatever happens to have that name": you know which image is running and you delete it without touching other people's.
Common mistakes
Writing CMD in shell form "because it is easier to read". It reads easier and shuts down worse: 10 seconds of waiting and a 137 on every docker stop. The exec form is two brackets and some quotes.
Using latest, or giving no tag, which is the same thing. latest does not mean "the newest"; it means "no tag". On the day of the problem you will not know which version was running.
Naming the image with a generic name like revisor or app. It clashes with images of other projects on the same machine and takes their name away without warning.
Leaving the Dockerfile without USER because "it is only for testing". What is for testing tends to end up in production with the same Dockerfile. It costs one line.
COPY . . without .dockerignore. Everything in the folder travels and ends up in the image, including files nobody remembers having left there.
Using RUN to start the service. RUN executes at build time: the build would wait for the server forever. What is executed at run time goes in CMD.
Believing that EXPOSE publishes the port. It only notes it. Without -p at run time, nobody from outside gets in. And with -p, the port is published on 127.0.0.1, as you learned in Lesson 2.
Ignoring the build warnings. A normal build shows the JSONArgsRecommended warning and ends successfully. Running docker build --check beforehand turns the warning into an error that cannot be overlooked.
Exercises
Do them in order. Before running each one, write in your logbook what you think is going to happen; comparing it afterwards is half of the learning. When you finish, delete what you created: everything carries the curso-docker- prefix.
Exercise 1 — At build time or at run time?
Without looking at the table in 3.2.2, classify each line: does it act at build time, does it only note something for running, or both?
FROM golang:1.27.3-trixieCOPY . .RUN go build -o /usr/local/bin/revisor ./cmd/revisorUSER 10001:10001EXPOSE 8080CMD ["revisor"]
And a trick question: if you change the order and put USER 10001:10001 before the RUN go build, which user does the compilation run as?
Exercise 2 — Your own .dockerignore (project)
Work in your folder ~/curso-docker/revisor (the one from the "In your folder" block in 3.6.1). Set the lesson's .dockerignore aside for a moment with mv .dockerignore ../dockerignore-leccion3 and create three things there: a .env file with a fake secret, a notas.md file and a secretos/ folder with a file inside. Build curso-docker-revisor:1.0.0-ej2 and list /src inside the image with docker run --rm curso-docker-revisor:1.0.0-ej2 ls -A /src. Then write a new .dockerignore that leaves out the three things, in addition to everything the lesson's excluded (you have it in ../dockerignore-leccion3), rebuild with the same tag and list again. The exercise is self-checking: you are done when /src shows exactly cmd, go.mod and internal. At the end, docker rmi curso-docker-revisor:1.0.0-ej2. Your extended .dockerignore can stay: it excludes everything the lesson's excluded and something more, so your curso-docker-revisor:1.0.0 does not change.
Exercise 3 — The stopwatch on your machine
Run fig03_05.sh and fig03_07.sh on your Mint (after Lesson 1 and with the Go image downloaded). Note in your logbook, on the line that Lesson 3 provides for it, the two docker stop times.
A warning before running them: fig03_05.sh builds and, on exit, deletes an image called curso-docker-revisor:1.0.0, the same name as your own from 3.6.1. So that it does not take it away, run it with a different prefix, which is what the variable exists for: PREFIJO=curso-docker-ej3- ./fig03_05.sh. In your output you will read curso-docker-ej3-revisor where the lesson says curso-docker-revisor; it still starts with curso-docker-, so the course cleanup reaches it. If you already ran it without the prefix and your image disappeared, go back to ~/curso-docker/revisor and rebuild it with docker build -q -t curso-docker-revisor:1.0.0 .: it takes seconds, because everything comes from the cache.
Before running them, predict:
- Which of the two times will change the most from one run to another, and which will stay almost the same?
- What number will
ExitCodeshow in the shell form? And what number would you expect in the exec form, knowing that therevisoris written to end with 0 after a clean shutdown? Check it by adding, after thedocker stopinfig03_05.sh, adocker inspect --format 'ExitCode={{.State.ExitCode}}' "${PREFIJO}revisor"(with the prefix above, the container is calledcurso-docker-ej3-revisor).
Exercise 4 — Fix someone else's Dockerfile
A teammate hands you this Dockerfile for the revisor. Find six problems, five in the file and one that is missing next to it; say what accident each one can cause and write the corrected version. Then check yours with docker build --check: it has to say Check complete, no warnings found.
# ASÍ NO — seis problemas a propósito (Ejercicio 4 de la Lección 3).
FROM golang
MAINTAINER alguien@ejemplo.invalid
COPY . /src
RUN cd /src && go build -o /usr/local/bin/revisor ./cmd/revisor
EXPOSE 8080
CMD revisor
Hint: docker build --check finds two of the six; the other four you have to find yourself.
Solutions
Solution 1
- At build time. It brings in all the contents of the base image.
- At build time. It copies the context's files into the image.
- At build time. It compiles and leaves the executable in the image. It is not executed again at run time.
- Both. It notes the user the container will run as, and it also affects the
RUNs that come after it in the Dockerfile. - It only notes. It publishes nothing (3.2).
- It only notes the process each container will start.
The trick question: as user 10001, because USER changes the user of everything that comes after, at build time too. And the compilation fails. Go tries to create its cache in the user's home folder and, since 10001 does not exist in /etc/passwd, that folder is /, where it cannot write. Measured on this lesson's machine with fig03_12.sh, which builds with docker build --progress=plain the fig03_11.Dockerfile (the lesson's, with USER before the RUN); the #10 and the 0.204 are the step number and the seconds, and they change, and between the two lines below the rest of the output is in fig03_12.salida.txt:
#10 0.204 failed to initialize build cache at /.cache/go-build: mkdir /.cache: permission denied
ERROR: failed to build: failed to solve: process "/bin/sh -c go build -o /usr/local/bin/revisor ./cmd/revisor" did not complete successfully: exit code: 1
Even if the cache worked, it could not write to /usr/local/bin either. That is why USER goes after compiling.
This is the Dockerfile with the USER moved (fig03_11.Dockerfile), which exists only to see the failure:
# syntax=docker/dockerfile:1
# fig03_11.Dockerfile — Lección 3: el Dockerfile de la lección con USER ANTES del RUN.
# Existe solo para ver fallar la compilación (Solución 1): así NO se escribe.
FROM golang:1.27.3-trixie
WORKDIR /src
COPY . .
USER 10001:10001
RUN go build -o /usr/local/bin/revisor ./cmd/revisor
EXPOSE 8080
CMD ["revisor"]
And this is fig03_12.sh, which builds it and, while at it, checks with docker build --check the other person's Dockerfile from Exercise 4 (fig03_10.Dockerfile, the same one as above):
#!/usr/bin/env bash
# fig03_12.sh — Los dos Dockerfile con defectos de la lección: el del Ejercicio 4,
# revisado con docker build --check, y el de la Solución 1 (USER antes del RUN),
# que falla al compilar. No deja imágenes ni contenedores.
set -euo pipefail
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
trap 'rm -rf "$TRABAJO"' EXIT
# El proyecto: el código que entrega el curso + los dos Dockerfile con defectos.
cp -R "$AQUI/../revisor" "$TRABAJO/revisor"
cp "$AQUI/estado/.dockerignore" "$TRABAJO/revisor/"
cp "$AQUI/fig03_10.Dockerfile" "$TRABAJO/revisor/Dockerfile.ajeno"
cp "$AQUI/fig03_11.Dockerfile" "$TRABAJO/revisor/Dockerfile.user-antes"
cd "$TRABAJO/revisor"
titulo() { printf '\n## %s\n' "$*"; }
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
titulo "Ejercicio 4: el Dockerfile ajeno, revisado antes de construir"
muestra "docker build --check -f Dockerfile.ajeno ."
titulo "Solución 1: USER antes del RUN, la compilación falla"
muestra "docker build --progress=plain -f Dockerfile.user-antes ."
Solution 2
A .dockerignore that meets the goal:
.git
.env
*.tar
*.md
secretos/
Dockerfile*
.dockerignore
With it, ls -A /src inside the image shows only cmd, go.mod and internal, just like the second block of fig03_04.sh. If your list still shows notas.md or secretos, check that the pattern is written correctly and that the .dockerignore file is at the root of the context, next to the Dockerfile, and not in a subfolder. If .dockerignore or Dockerfile appears, you missed the last two lines. A variant that is also valid is *.md changed to notas.md: it excludes only that file. The one above is more far-sighted, because the revisor does not need any .md to compile.
Solution 3
- The exec form's time changes the most from one run to another (tens or hundreds of milliseconds, depending on how busy your machine is); the shell form's stays almost the same, always a little above 10,000 ms, because almost all of it is
docker stop's fixed deadline. - In the shell form,
ExitCode=137, as infig03_07.sh. In the exec form,ExitCode=0: therevisorreceivesSIGTERM, writesapagado limpioand its code ends with 0. If your run gives another number, look atdocker logs: ifapagado limpiodoes not appear, the signal did not arrive, and the first suspicion is that your Dockerfile has the shell form.
Solution 4
The five problems in the file:
FROM golangwithout a tag, which isgolang:latest: each build can use a different version of Go. Fix:FROM golang:1.27.3-trixie.MAINTAINER, deprecated. Fix: remove it, or useLABEL org.opencontainers.image.authors="...".RUN cd /src && ...instead ofWORKDIR. It works on that line, but thecdis lost afterwards, and the container starts in/. Fix:WORKDIR /srcandCOPY . ..- No
USER: therevisorwould run asroot. Fix:USER 10001:10001after compiling. CMD revisorin shell form: 10 seconds and 137 on everydocker stop. Fix:CMD ["revisor"]. It is exactly whatdocker build --checkpoints to.
And the sixth, the one that is not in the file: the .dockerignore is missing, and with COPY . /src the whole context goes into the image. The corrected version is this lesson's fig03_01.Dockerfile plus the fig03_03.dockerignore.
docker build --check on the original Dockerfile (fig03_10.Dockerfile, which fig03_12.sh checks) finds two of the six. Measured on this lesson's machine (only the warning lines are shown; the full output is in fig03_12.salida.txt):
Check complete, 2 warnings have been found!
WARNING: MaintainerDeprecated - https://docs.docker.com/go/dockerfile/rule/maintainer-deprecated/
Maintainer instruction is deprecated in favor of using label
WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for CMD to prevent unintended behavior related to OS signals
MaintainerDeprecated is problem 2: the rule says MAINTAINER is deprecated and that a label (LABEL) should be used in its place (Docker, MaintainerDeprecated). JSONArgsRecommended is problem 5. The other four (latest, the cd, the missing USER and the missing .dockerignore) are not detected by any of the rules that ran here: that is why --check helps but does not replace reading the recipe. Your version has to say Check complete, no warnings found.
How I know I got it
Check each point only if you verified it on your machine:
-
docker build --check .in~/curso-docker/revisorends withCheck complete, no warnings found.and code 0. -
docker image ls 'curso-docker-revisor'showscurso-docker-revisorwith the tag1.0.0, and you noted its size in the logbook (in this lesson's run, 1.45 GB). - Without
REVISOR_SERVICIOS, the container is leftExited (1)anddocker logssays what is missing. - With the variable,
curl -s http://127.0.0.1:8080/saludanswers{"estado":"vivo","version":"desarrollo"}. -
docker exec curso-docker-revisor idsaysuid=10001 gid=10001 groups=10001. -
docker top curso-docker-revisor -o pid,user,argsshows a single process,revisor. -
docker stop curso-docker-revisortakes less than a second anddocker logsends withrevisor: apagado limpio. - With the shell-form image you measured the 10 seconds and the 137, and you can explain with
docker topwhy. - Inside the image,
ls -A /srcshows no.env,.gitorDockerfile. - When you finish,
docker ps -a --filter name=curso-docker-revisorshows nothing, anddocker image ls 'curso-docker-revisor'shows onlycurso-docker-revisor:1.0.0, the one you built in your folder and which Lesson 4 uses.
Summary
Answer without looking at the lesson:
- What is a Dockerfile, what is the context and who does the work of building?
- Which instructions act at build time and which only note something for running?
- What does the
latesttag really mean? Why is the course's image calledcurso-docker-revisor:1.0.0and notrevisor:1.0.0? - Which user does an image run as if nobody wrote
USER? Why is a number enough? - What does
.dockerignoredo and what happens to a.envif you do not have one? - What is the difference between
CMD ["revisor"]andCMD revisor? What doesdocker topshow in each case? - Why does
docker stoptake 10 seconds and leave 137 with the shell form? What is the 137? - What does
docker build --checkdo and which code does it exit with if it finds something?
Sources
All consulted on October 21, 2026.
- Docker, "Dockerfile overview": what a Dockerfile is.
- Dockerfile reference: the instructions
FROM,WORKDIR,COPY,RUN,USER,EXPOSE,CMD,ENTRYPOINTandMAINTAINER; the shell and exec forms; thesyntaxdirective. - Docker, "Build context": the build context and the
.dockerignorefiles. - Docker, "Build checks":
docker build --check, its exit code and the minimum versions. - Docker,
MaintainerDeprecatedrule: the warning aboutMAINTAINERin exercise 4. - Docker,
JSONArgsRecommendedrule: why the shell form does not pass signals along, and the two alternatives when a shell is needed. - Docker, best practices for building images:
USER,sudo,ADDversusCOPY, mutable tags and.dockerignore. docker container stop:SIGTERM, the 10-second deadline andSIGKILL.docker image tag: the parts of an image name and the defaultlatesttag.- Docker, "Running containers": replacing the
CMDfromdocker run. - golang on Docker Hub: the
1.27.3-trixietag and its nicknames, and the meaning of trixie. - Docker Engine 29, release notes: the versions in "About the runs".
- Linux kernel, "IP Sysctl":
ip_unprivileged_port_start, per namespace and with a default value of 1024. docker image ls: thereferencefilter with wildcards.- pid_namespaces(7), Linux manual: the signals that process number 1 of a namespace can receive.
- signal(7), Linux manual: the numbers of
SIGTERM(15) andSIGKILL(9), and thatSIGKILLcannot be caught. - Colima: the tool used to run the programs on macOS.
Terms in this lesson
| BuildKit | the image builder Docker uses; it is part of the daemon |
| build context | the files the build can use; almost always, a folder |
| Dockerfile | the text recipe used to build an image |
.dockerignore |
the list of what does not travel in the context and cannot get into the image |
ENTRYPOINT |
an instruction that sets the container's main program; CMD gives it default arguments; you use it from Lesson 5 |
| tag | the part of an image's name after the colon; it says which version it is |
| exec form | an instruction written as a JSON list: CMD ["revisor"]; it starts the program without a shell |
| shell form | an instruction written as text: CMD revisor; it is wrapped by /bin/sh -c |
| base image | the image another one starts from, the one in FROM |
latest |
the tag Docker adds when none is given; it does not mean "the newest" |
| PID 1 | a container's first process; the one that receives docker stop's signal |
| build check | a rule that looks for common errors in a Dockerfile without building it |
SIGTERM |
signal 15: "please finish"; the one docker stop sends first |
SIGKILL |
signal 9: immediate death; it cannot be caught; it leaves code 137 |
Prefer email? Write to us at hola@habil.mx