Until now, containers have only used pre-built images directly.
The new objective is to build our own image.
This process generally does not start from scratch, but from an existing
image to which we apply modifications in order to produce a new image
tailored to a specific requirement.
Each modification constitutes a
layer containing the differences
introduced relative to the previous image layer
When an image is downloaded, you can indeed observe that it does not
arrive as a single block but as multiple layers, with the download
progress displayed across several lines.
.
In your working directory on the host system, create a directory named
sh_db_demo and move your
shell into it.
Create a file named
sh_db_demo.sh with the following initial
content:
#!/bin/sh
set -eu # exit script on any failure, undefined variables are errors
echo "variable DB_HOST is ${DB_HOST}"
echo "variable DB_PORT is ${DB_PORT}"
echo "variable DB_NAME is ${DB_NAME}"
echo "variable DB_USER is ${DB_USER}"
echo "variable DB_PASS is ${DB_PASS}"
exec /bin/sh
The goal is now to start from an
Alpine system image and
ensure that it implicitly executes this script.
To do so, create a file named
Dockerfile in the same
directory with the following content:
# start from an existing image
FROM alpine
# run this command in the guest system (package installation)
RUN apk add --no-cache tini postgresql-client
# create a working directory and execute every remaining command from it
WORKDIR /demo
# copy the specified file(s) from the host to this directory
COPY *.sh .
# help the shell process to terminate when requested
ENTRYPOINT ["/sbin/tini", "-g", "--"]
# run this command by default (from the working directory)
CMD ["/bin/sh", "sh_db_demo.sh"]
This is essentially a
“recipe” describing the steps required to build
the image
The package installation and file copy steps take place when the image
is built, not when it is run inside a container (they will already
have been executed).
.
The image is built with the command
podman build -t sh_db_demo . ;
it follows the instructions in the
Dockerfile found in the current
directory and builds the image whose name is specified by the
-t option.
Verify with
podman image ls that this image is now present
among those that were previously downloaded.
Try to use it interactively with
podman run --name A --rm --network my_network -it sh_db_demo.
Observe that the initial script executed by this container reports an error
about an undefined environment variable, causing the container to
terminate immediately.
Then start it again, this time specifying the expected environment
variables:
podman run --name A --rm \
--network my_network \
-e DB_HOST=DB \
-e DB_PORT=5432 \
-e DB_NAME=demo_db \
-e DB_USER=demo_user \
-e DB_PASS=demo_secret \
-it sh_db_demo
Observe that this time the variables are correctly received by the script
and that it launches the expected interactive
shell; simply type
exit to terminate this container.
Replace the contents of the
sh_db_demo.sh file with the following:
#!/bin/sh
set -eu # exit script on any failure, undefined variables are errors
export PGPASSWORD="${DB_PASS}"
export DB_CMD="psql -h ${DB_HOST} -p ${DB_PORT} -d ${DB_NAME} -U ${DB_USER}"
echo "waiting for ${DB_NAME} database at ${DB_HOST}:${DB_PORT}..."
while ! ${DB_CMD} -c 'SELECT 1' >/dev/null 2>&1; do
echo 'database not ready, retrying...'
sleep 2
done
echo 'database is ready'
echo "initialising table reminder in ${DB_NAME} if needed"
${DB_CMD} <<'EOF'
CREATE TABLE IF NOT EXISTS reminder ( id SERIAL PRIMARY KEY, item TEXT );
EOF
echo 'contents of table reminder:'
${DB_CMD} -c 'SELECT * from reminder'
exec /bin/sh
This time, the variables are used to connect to the database.
After checking that the database is available, the table that we created
in the previous step is used and the
shell provides an interactive
session.
Rebuild your
sh_db_demo image; examine the messages carefully and
observe that the layers preceding the script copy step do not need to
be rebuilt (
Using cache...), since the change only affects the
script.
List the images with
podman image ls and observe that this new image
has the chosen name, while the previous one has lost its name (as it
was the same).
The command
podman image prune -f can be used to remove images and
layers that are no longer needed.
Run container
A once again as before and observe the script's new
behaviour.
It interacts with the database in container
DB and displays
the few entries that we inserted during the previous step.
The overall situation can be described as follows:

Once again, terminate container
A.
Replace the last three lines of
sh_db_demo.sh with the following:
PORT=9988
while true ; do
echo "waiting for connection on ${PORT}"
nc -lp ${PORT} -e /bin/sh sh_db_commands.sh
done
The interactive
shell has been replaced by a loop that waits for a
connection on port
9988.
For each connection, the script
sh_db_commands.sh is executed to
communicate with the client (
nc redirects the standard input/output
of this script through the connection).
The
sh_db_commands.sh script is a new file in the current directory
with the following content:
#!/bin/sh
exec 2>&1 # redirect errors to standard output
echo 'available commands:'
echo ' list'
echo ' add item'
echo ' del id'
echo ' clear'
echo ' quit'
while true ; do
echo -n 'reminder> '
read command arg || break
case "${command}" in
list)
echo '... listing ...'
${DB_CMD} -c 'SELECT * FROM reminder;'
;;
add)
echo "... adding ${arg} ..."
${DB_CMD} -c "INSERT INTO reminder (item) VALUES ('${arg}');"
;;
del)
echo "... deleting ${arg} ..."
${DB_CMD} -c "DELETE FROM reminder WHERE id=${arg};"
;;
clear)
echo '... clearing ...'
${DB_CMD} -c 'DELETE FROM reminder;'
;;
quit)
echo '... bye ...'
break
;;
*)
echo "!!! unknown command ${command} !!!"
;;
esac
done
Depending on the lines sent by the client, actions are performed on the
database.
Naturally, this new script will also be included in the built image, thanks
to the
COPY command in the
Dockerfile.
Rebuild the
sh_db_demo image and remove the old one with
podman image prune -f.
Since this new version waits for connections on a chosen port, that port
must be made accessible.
Container
A must be started again as follows:
podman run --name A --rm \
--network my_network -p 9988:9988 \
-e DB_HOST=DB \
-e DB_PORT=5432 \
-e DB_NAME=demo_db \
-e DB_USER=demo_user \
-e DB_PASS=demo_secret \
-d sh_db_demo
The
-d option starts it in the background because the new script
does not require direct interaction with the terminal or keyboard.
Check its startup messages with
podman container logs A.
The overall situation can be described as follows:

From the host system, it now becomes possible to interact with this
application through the exposed
9988 port.
Using the command
ncat localhost 9988, verify that you receive
the messages from the
sh_db_commands.sh script executed
inside the container and that it responds to your commands
The simplistic nc loop in sh_db_demo.sh only allows
communication with one client at a time.
One client must disconnect before another can interact with the
server.
This major limitation is not an issue in the case of this
simplified demonstration.
.
Terminate container
A with
podman stop A
The command podman stop ... is a shorthand for the command
podman container stop ....
and then do the same for container
DB.
Verify with the command
podman container ls -a that they have indeed
been removed.
Also remove the network with
podman network rm my_network
and the volume with
podman volume rm my_volume.
If you wish to remove the contents of the directory associated with your
volume, it is likely that the
Podman permissions model will
prevent you from doing so directly.
The
Podman-specific command
podman unshare ...
allows the directory to be accessed while taking this permissions
model into account.
You can, for example, use
podman unshare chown -R 0:0 /tmp/podman_${USER}/STORAGE
to regain ownership of this content so that you can copy or
delete it.
If you wish to remove the
/tmp/podman_${USER} directory
yourself in order to reset everything, simply run
/tmp/podman_${USER}/remove_all.