Docker🔗

docker.png

El proyecto Docker te permitirá la creación de entornos de construcción y/o ejecución de código fuente o procesos a partir del uso de imágenes y contenedores.

Una imagen suele ser un sistema operativo (basado en GNU/Linux) y el contenedor puede ser desde un proceso (un interprete de comandos, un servidor de HTTP o un sistema gestor de bases de datos) hasta una aplicación web o una aplicación de escritorio.

Mientras que una imagen es obtenida y/o publicada en Docker Hub, un contenedor es creado de manera local.

¿Qué es Docker?

Docker es una plataforma de software que le permite crear, probar e implementar aplicaciones rápidamente. Docker empaqueta software en unidades estandarizadas llamadas contenedores que incluyen todo lo necesario para que el software se ejecute, incluidas bibliotecas, herramientas de sistema, código y tiempo de ejecución. - AWS Amazon

Docker (software)

Docker es un proyecto de código abierto que automatiza el despliegue de aplicaciones dentro de contenedores de software, proporcionando una capa adicional de abstracción y automatización de virtualización de aplicaciones en múltiples sistemas operativos. - Wikipedia

Una excelente introducción a Docker por parte de Peter McKee hablando de imágenes, contenedores, red, e inclusive Docker Hub:

Instalación🔗

  • En el caso de usar Microsoft Windows o macOS te será necesario la instalación de Docker Desktop así como la instalación de otros programas (virtualizadores) según sea el caso.

  • En GNU/Linux te será necesario hacer uso del gestor de paquetes de tu distribución para la instalación del paquete docker.

La instalación de Docker incluye a Docker Compose.

Imagen🔗

Se hará uso de dos imágenes ofrecidas en Docker Hub, ambas imágenes corresponden a distribuciones del sistema operativo GNU/Linux:

Para obtener (pull) dichas imágenes se ejecuta lo siguiente en la línea de comandos:

[nihilipster@localhost:~]$ docker image pull alpine:3.13.2
[nihilipster@localhost:~]$ docker image pull debian:10.8

Importante

  • Es recomendable obtener las últimas versiones disponibles así como siempre ser explicitos en la versión usada.
  • Es posible obtener tantas imágenes como sean necesarias desde Docker Hub tomando en cuenta su espacio.
  • Es posible borrar una imagen mientras ningún contenedor se encuentre haciendo uso de ella.

Contenedor🔗

Mientras que una imagen es obtenida de Docker Hub, un contenedor es creado (create) de manera local a partir de una imagen previamente obtenida. Es posible crear tantos contenedores como sean necesarios a partir de una misma imagen tomando en cuenta el espacio usado en disco duro por cada contenedor. Por otro lado, es posible crear-iniciar-detener-borrar un contenedor sin afectar la imagen a partir del cual fue creado.

El interprete de comandos sh (Bourne Shell)🔗

Para la creación de un contenedor a partir de la imagen de Alpine Linux:

[nihilipster@localhost:~]$ docker container create --name alpine-sh --tty --interactive alpine:3.13.2

Para obtener una lista de contenedores y su estado (STATUS):

[nihilipster@localhost:~]$ docker container ls -a
CONTAINER ID   IMAGE           COMMAND   STATUS    PORTS   NAMES
xxxxxxxxxxxx   alpine:3.13.2   "sh"      Created           alpine-sh

Para iniciar un contenedor:

[nihilipster@localhost:~]$ docker container start --attach --interactive alpine-sh

Por el ejemplo abordado al iniciar el contenedor se obtendrá un interprete de comandos de Alpine Linux llamado sh (Bourne Shell):

/ # env
HOSTNAME=yyyyyyyyyyyyyy
SHLVL=1
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
/ # whoami
root
/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.13.2
PRETTY_NAME="Alpine Linux v3.13"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
/ # exit

Al salir de sh se puede observar que el contenedor se ha detenido:

[nihilipster@localhost:~]$ docker container ls -a
CONTAINER ID   IMAGE           COMMAND     STATUS                     PORTS   NAMES
xxxxxxxxxxxx   alpine:3.13.2   "/bin/sh"   Exited (0) 2 minutes ago           alpine-sh

El interprete de comandos Bash (GNU Bash)🔗

Se procede de manera similar al ejemplo dado con Alpine Linux haciendo modificaciones donde sea necesario tomando en cuenta que el interprete de comandos en Debian es bash (GNU Bash).

[nihilipster@localhost:~]$ docker container create --name debian-bash --tty --interactive debian:10.8
[nihilipster@localhost:~]$ docker container start --attach --interactive debian-bash
root@zzzzzzzzzzzzzzz:/# env
HOSTNAME=zzzzzzzzzzzzzzz
PWD=/
HOME=/root
TERM=xterm
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env
root@zzzzzzzzzzzzzzz:/# whoami
root
root@zzzzzzzzzzzzzzz:/# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

Información

El sistema operativo GNU/Linux🔗

En los anteriores interpretes de comandos fueron usados los comandos env, whoami y cat. Para aprender otros comandos disponibles pero sobre todo para familiarizarse con el sistema operativo y su interprete de comandos tienes los siguientes recursos:

Algunos comandos abordados en los anteriores recursos no están disponibles en las imágenes de Alpine Linux y Debian; tal es el caso de tree, vim, ps, file, entre otros tantos. Es posible instalarlos en caso de ser necesario.

Advertencia

Existen otros tantos proyectos-terminos asociados a Docker, tal es el caso de Docker Compose, Docker Swarm, Kubernetes, DevOps, Continuous Integration, Continuous Delivery, Continuous Deployment, etc. por lo que será importante concentrarse solo en aquello que por el momento sea necesario.

Las siguientes presentaciones por parte de Wendy Fabela puede ser de tu interes para contextualizar a Docker.

Paquetes en Alpine Linux y Debian🔗

Las imágenes de Alpine Linux y Debian disponibles en Docker Hub no cuentan con varios paquetes por lo que es necesario hacer uso de sus gestores de paquetes dentro de los distintos contenedores que podamos crear.

Para lo siguiente se ejemplificará con la instalación del editor de texto Nano, el cual no forma parte de las imágenes de ambos sistemas operativos.

Información

Las imágenes de los sistemas operativos solo integran lo más básico e indispensable para su ejecución, esto se hace principalmente para reducir su tamaño-peso por lo que aprender a su administración es clave para brindar mayor seguridad tanto al contenedor como aquello que le integremos.

Alpine Linux🔗

Se ofrece https://pkgs.alpinelinux.org/packages para la búsqueda de paquetes. Por ejemplo ingresando nano, seleccionando v13.3, x86_64 y dando click al botón de Search se obtiene un resultado con el paquete de Nano disponible.

Por otro lado, en la línea de comandos, dentro de algún contenedor basado en la imagen de Alpine Linux podemos hacer uso del gestor de paquetes apk:

[nihilipster@localhost:~]$ docker container start --attach --interactive alpine-sh
/ # which nano
/ #
/ # apk update
fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
v3.13.2-105-g4e9e4fc875 [https://dl-cdn.alpinelinux.org/alpine/v3.13/main]
v3.13.2-106-g54cefe59f4 [https://dl-cdn.alpinelinux.org/alpine/v3.13/community]
OK: 13878 distinct packages available
/ # apk search nano
nano-doc-5.4-r1
plasma-nano-5.20.5-r0
nano-5.4-r1
nano-syntax-5.4-r1
/ # apk add nano
(1/4) Installing libmagic (5.39-r0)
(2/4) Installing ncurses-terminfo-base (6.2_p20210109-r0)
(3/4) Installing ncurses-libs (6.2_p20210109-r0)
(4/4) Installing nano (5.4-r1)
Executing busybox-1.32.1-r3.trigger
OK: 13 MiB in 18 packages
/ # which nano
/usr/bin/nano

Por un lado se ha actualizado la lista de paquetes disponibles con apk update, posteriormente se ha buscado a Nano con apk search y finalmente se ha instalado con apk add. Por otro lado como puedes observar which no encuentra a nano en el PATH sin embargo una vez instalado indica su ubicación en /usr/bin/nano.

Finalmente, podrás hacer uso del editor de texto Nano solo en este contenedor haciendo uso del comando nano.

Debian🔗

Se ofrece https://www.debian.org/distrib/packages para la búsqueda de paquetes. Por ejemplo, ingresando nano, seleccionando Package names only, stable y dando click al botón de Search se obtiene un resultado con el paquete de Nano disponible.

Por otro lado, en la línea de comandos, dentro de algún contenedor basado en la imagen de Debian podemos hacer uso del gestor de paquetes apt:

[nihilipster@localhost:~]$ docker container start --attach --interactive debian-bash
root@yyyyyyyyyyyy:/# which nano
root@yyyyyyyyyyyy:/#
root@yyyyyyyyyyyy:/# apt-get update
Get:1 http://security.debian.org/debian-security buster/updates InRelease [65.4 kB]
Hit:2 http://deb.debian.org/debian buster InRelease
Get:3 http://deb.debian.org/debian buster-updates InRelease [51.9 kB]
Fetched 117 kB in 1s (136 kB/s)
Reading package lists... Done
root@yyyyyyyyyyyy:/# apt-cache search '^nano$'
nano - small, friendly text editor inspired by Pico
root@yyyyyyyyyyyy:/# apt-get install nano
Reading package lists... Done
Building dependency tree
Reading state information... Done
nano is already the newest version (3.2-3).
0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
root@yyyyyyyyyyyy:/# which nano
/bin/nano

Por un lado se ha actualizado la lista de paquetes disponibles con apt-get update, posteriormente se ha buscado a Nano con apt-cache search y finalmente se ha instalado con apt-get install. Por otro lado, como puedes observar which no encuentra a nano en el PATH sin embargo una vez instalado indica su ubicación en /bin/nano.

Finalmente, podrás hacer uso del editor de texto Nano solo en este contenedor haciendo uso del comando nano.

Importante

Aún cuando el comando usualmente usado en Debian es apt por cuestiones del uso de Docker y la programación/automatización de paquetes se recomienda hacer uso de apt-get y apt-cache para la administración de paquetes en Debian

Creación de imágenes de Docker.🔗

Uno de los aspectos interesantes de Docker es la creación de imágenes a partir de una imagen pre existente. Por ejemplo, podrías crear una imagen en base a GNU/Linux en la cual automatizar la instalación de programas/bibliotecas así como su configuración para poder crear contenedores de alguna aplicación de usuario.

Lo anteriormente mencionado se ejemplificará creando una imagen para una aplicación de usuario en la línea de comandos usando Java 11.

Aplicación de usuario🔗

El código fuente para la aplicación de usuario haciendo uso de Java 11 es el siguiente, Main.java:

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
  public static void main(String[] args) {
    var ubicacionArchivo = "/etc/os-release";
    var contenidoArchivo = "";
    try {
      contenidoArchivo = Files.readString(Paths.get(ubicacionArchivo), StandardCharsets.UTF_8);
    } catch (IOException e) {
      System.out.println("[Clase Main] Error en la lectura de '" + ubicacionArchivo + "'");
      System.err.println("[Clase Main] Excepción atrapada '" + e + "'");
      System.exit(1);
    }
    System.out.println("|---------------------------------");
    System.out.println("| Contenido de " + ubicacionArchivo);
    System.out.println("|---------------------------------");
    System.out.println(contenidoArchivo);
    System.out.println("|---------------------------------");
  }
}

Definición de imagen🔗

Para definir una imagen harás uso de un archivo Dockerfile en el cual (usando un conjunto de directivas o nemónicos) definirás la imagen pre existente desde la cual se construirá (build) una nueva imagen así como las instrucciones necesarias para copiar y compilar el código fuente de la aplicación de usuario en la imagen.

Importante

El archivo Dockerfile debe estar en la misma carpeta que Main.java.

  • Dockerfile para Alpine Linux:

    FROM     alpine:3.13.2
    LABEL    description="Aplicación en la línea de comandos con Java 11"
    
    ENV      APP_DIR="/app"
    ENV      JAVA_HOME="/usr/lib/jvm/default-jvm"
    ENV      PATH="$JAVA_HOME/bin:$PATH"
    
    WORKDIR  "$APP_DIR"
    
    RUN      apk update --quiet --no-cache
    RUN      apk add --quiet --no-cache openjdk11-jdk
    
    COPY     Main.java "$APP_DIR"/
    
    RUN      javac Main.java
    
    CMD      ["java", "Main"]
    
  • Dockerfile para Debian:

    FROM     debian:10.8
    LABEL    description="Aplicación en la línea de comandos con Java 11"
    
    ENV      APP_DIR="/app"
    ENV      JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"
    ENV      PATH="$JAVA_HOME/bin:$PATH"
    
    WORKDIR  "$APP_DIR"
    
    RUN      apt-get update --quiet --assume-yes
    RUN      apt-get install --quiet --assume-yes openjdk-11-jdk
    
    COPY     Main.java "$APP_DIR"/
    
    RUN      javac Main.java
    
    CMD      ["java", "Main"]
    

Construcción de nueva imagen🔗

Como siguiente paso es necesario que te encuentres en la misma carpeta donde se encuentran los archivos Dockerfile y Main.java para que ejecutes la opción build de Docker, dando un nombre (--tag) a la nueva imagen:

  • Construcción de nueva imagen a partir de Alpine Linux:

    [nihilipster@localhost:~]$ docker build --tag java11-app:0.1.0 .
    Sending build context to Docker daemon  ...
    Step 1/11 : FROM     alpine:3.13.2
    ...
    Step 2/11 : LABEL    description="Aplicación en la línea de comandos con Java 11"
    ...
    ...
    ...
    Successfully tagged java11-app:0.1.0
    
  • Construcción de nueva imagen a partir de Debian:

    [nihilipster@localhost:~]$ docker build --tag java11-app:0.1.0 .
    Sending build context to Docker daemon  7.168kB
    Step 1/11 : FROM     debian:10.8
    ...
    Step 2/11 : LABEL    description="Aplicación en la línea de comandos con Java 11"
    ...
    ...
    ...
    Successfully tagged java11-app:0.1.0
    

Al terminar la ejecución de alguno de ellos uno puedes observa la nueva imagen creada, prestando atención en la diferencia de sus tamaños (SIZE):

[nihilipster@localhost:~]$ docker image ls -a
REPOSITORY   TAG     IMAGE ID     SIZE
java11-app   0.1.0   xxxxxxxxxx   ???MB

Advertencia

Se debe de tomar en cuenta que solo es posible tener una imagen con un nombre en especifico (--tag) dado que en los anteriores casos se está usando el mismo nombre (java11-app:0.1.0) para la creación de la nueva imagen.

Creación de nuevo contenedor🔗

Para la creación de un nuevo contenedor haciendo uso de la imagen recientemente creada ejecuta lo siguiente, según el caso:

[nihilipster@localhost:~]$ docker container create --name app01 --tty --interactive java11-app:0.1.0

Podrás observar la creación del nuevo contenedor mediante:

[nihilipster@localhost:~]$ docker container ls -a
CONTAINER ID  IMAGE             COMMAND      CREATED        STATUS   PORTS  NAMES
xxxxxxxxxxxx  java11-app:0.1.0  "java Main"  3 minutes ago  Created         app01

Ejecución o inicio del contenedor🔗

Para que inicialices el nuevo contenedor:

  • Inicialización de nuevo contenedor en base a Alpine Linux:

    [nihilipster@localhost:~]$ docker container start --attach --interactive app01
    |--------------------------------
    | Contenido de /etc/os-release
    |--------------------------------
    NAME="Alpine Linux"
    ...
    ...
    ...
    BUG_REPORT_URL="https://bugs.alpinelinux.org/"
    
    |--------------------------------
    
  • Inicialización de nuevo contenedor en base a Debian:

    [nihilipster@localhost:~]$ docker container start --attach --interactive app01
    |--------------------------------
    | Contenido de /etc/os-release
    |--------------------------------
    PRETTY_NAME="Debian GNU/Linux 10 (buster)"
    ...
    ...
    ...
    BUG_REPORT_URL="https://bugs.debian.org/"
    
    |--------------------------------
    

Una vez que obtengas lo programado en Main.java observarás que el contenedor se encuentra detenido:

[nihilipster@localhost:~]$ docker container ls -a
CONTAINER ID  IMAGE             COMMAND      CREATED        STATUS                     PORTS  NAMES
xxxxxxxxxxxx  java11-app:0.1.0  "java Main"  7 minutes ago  Exited (0) 24 seconds ago         app01

Reutilización de imágenes🔗

Algo muy importante de observar de los contenedores creados es su tamaño (SIZE), en el caso de Alpine Linux se obtuvo un contenedor de ~200MB mientras que para Debian uno de ~900MB.

Ante la anterior observación es muy recomendable que busques una imagen base que ya contenga lo necesario para que tu imagen-contenedor tenga un tamaño más pequeño.

En el caso particular que se ha planteado ya existen imágenes especificas para aplicaciones basadas en Java por lo que no es necesario hacer uso de Alpine Linux o Debian y después instalar (apk/apt-get) el JDK en ellos:

Analiza las secciones How to use this Image en los anteriores enlaces para comprender este último punto.

Por otro lado también es posible encontrar imágenes base para:

Te recomiendo la excelente presentación, con consejos prácticos para la creación de un Dockerfile, por parte de Tibor Vass y Sebastiaan van Stijn hablando sobre environments, minimal imágenes, tags, cache, así como multi-stage images:

Red en Docker.🔗

Un contenedor en Docker puede tener comunicación de red mediante drivers de red (subsistemas de comunicación de Docker) ya integrados en Docker o bien ofrecidos mediante plugins por terceros.

El driver de red bridge permite la comunicación entre contenedores dentro de la misma computadora o instancia de Docker. Docker conecta un contenedor a un bridge ya activado por default .

Para ejemplificar la comunicación con un servicio o proceso en un contenedor se hará uso del protocolo de red Echo.

Java: EchoServer & EchoClient🔗

  • EchoServer.java

    /*
     * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions
     * are met:
     *
     *   - Redistributions of source code must retain the above copyright
     *     notice, this list of conditions and the following disclaimer.
     *
     *   - Redistributions in binary form must reproduce the above copyright
     *     notice, this list of conditions and the following disclaimer in the
     *     documentation and/or other materials provided with the distribution.
     *
     *   - Neither the name of Oracle or the names of its
     *     contributors may be used to endorse or promote products derived
     *     from this software without specific prior written permission.
     *
     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
     * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     */
    
    import java.net.*;
    import java.io.*;
    
    public class EchoServer {
        public static void main(String[] args) throws IOException {
    
            if (args.length != 1) {
                System.err.println("Usage: java EchoServer <port number>");
                System.exit(1);
            }
    
            int portNumber = Integer.parseInt(args[0]);
    
            try (
                ServerSocket serverSocket =
                    new ServerSocket(Integer.parseInt(args[0]));
                Socket clientSocket = serverSocket.accept();     
                PrintWriter out =
                    new PrintWriter(clientSocket.getOutputStream(), true);                   
                BufferedReader in = new BufferedReader(
                    new InputStreamReader(clientSocket.getInputStream()));
            ) {
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    out.println(inputLine);
                }
            } catch (IOException e) {
                System.out.println("Exception caught when trying to listen on port "
                    + portNumber + " or listening for a connection");
                System.out.println(e.getMessage());
            }
        }
    }
    
  • EchoClient.java

    /*
     * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions
     * are met:
     *
     *   - Redistributions of source code must retain the above copyright
     *     notice, this list of conditions and the following disclaimer.
     *
     *   - Redistributions in binary form must reproduce the above copyright
     *     notice, this list of conditions and the following disclaimer in the
     *     documentation and/or other materials provided with the distribution.
     *
     *   - Neither the name of Oracle or the names of its
     *     contributors may be used to endorse or promote products derived
     *     from this software without specific prior written permission.
     *
     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
     * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     */
    
    import java.net.*;
    import java.io.*;
    
    public class EchoServer {
        public static void main(String[] args) throws IOException {
    
            if (args.length != 1) {
                System.err.println("Usage: java EchoServer <port number>");
                System.exit(1);
            }
    
            int portNumber = Integer.parseInt(args[0]);
    
            try (
                ServerSocket serverSocket =
                    new ServerSocket(Integer.parseInt(args[0]));
                Socket clientSocket = serverSocket.accept();     
                PrintWriter out =
                    new PrintWriter(clientSocket.getOutputStream(), true);                   
                BufferedReader in = new BufferedReader(
                    new InputStreamReader(clientSocket.getInputStream()));
            ) {
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    out.println(inputLine);
                }
            } catch (IOException e) {
                System.out.println("Exception caught when trying to listen on port "
                    + portNumber + " or listening for a connection");
                System.out.println(e.getMessage());
            }
        }
    }
    

Información

La implementación en Java ha sido obtenida de Reading from and Writing to a Socket.

Dockerfile🔗

FROM     adoptopenjdk:11.0.10_9-jre-hotspot
LABEL    description="EchoServer"
LABEL    source="https://docs.oracle.com/javase/tutorial/networking/sockets/readingWriting.html"

ENV      APP_DIR="/app"
ENV      ECHO_PORT="7"

EXPOSE   "$ECHO_PORT"

WORKDIR  "$APP_DIR"

COPY     EchoServer.class "$APP_DIR"/

CMD      ["bash", "-c", "java EchoServer $ECHO_PORT"]

Importante

  • El archivo Dockerfile se encontrará en la misma carpeta que los archivos EchoClient.java y EchoServer.java.
  • La imagen usada es el JRE, mas no es JDK, del proyecto AdoptOpenJDK por lo que solo se puede ejecutar código de Java en dicho contenedor.
  • La compilación del cliente y del servidor del protocolo de red Echo se hace desde el sistema anfitrión (el sistema donde se haya instalado y se ejecute a Docker):
    • javac EchoCliente.java
    • javac EchoServer.java
  • Solo el servidor EchoServer.class es copiado (COPY) en el contenedor.
  • La ejecución del cliente EchoCliente es llevado a cabo dentro del sistema anfitrión.

Imagen🔗

Puedes obtener primero la imagen de Docker:

[nihilipster@localhost:~]$ docker image pull adoptopenjdk:11.0.10_9-jre-hotspot
11.0.10_9-jre-hotspot: Pulling from library/adoptopenjdk
...
...
...
Status: Downloaded newer image for adoptopenjdk:11.0.10_9-jre-hotspot
docker.io/library/adoptopenjdk:11.0.10_9-jre-hotspot

Para después crear una imagen a partir de esta:

[nihilipster@localhost:~]$ docker image build --tag echo-server:0.1.0 .
Sending build context to Docker daemon  15.36kB
...
...
...
Successfully built xxxxxxxxxxxx
Successfully tagged echo-server:0.1.0

Contenedor🔗

Finalmente se crea un contenedor en base a la nueva imagen echo-server:0.1.0:

[nihilipster@localhost:~]$ docker container create --name echo-server --publish 127.0.0.1:1234:7 \
  --tty --interactive echo-server:0.1.0

Importante

  • En el archivo Dockerfile se ha expuesto (EXPOSE) el puerto TCP #7 para la comunicación externa hacía el servidor del protocolo de red Echo.
  • Mediante --publish se ha establecido un redireccionamiento (o binding) del socket 127.0.0.1:1234 al puerto TCP #7. Lo anterior con la finalidad de que al establecer una conexión con el anfitrión (127.0.0.1) en el puerto TCP #1234 dicha conexión realmente sea al puerto TCP #7 del contenedor.

Ejecución🔗

Inicializa el contenedor recientemente creado:

[nihilipster@localhost:~]$ docker container start --attach --interactive echo-server

Importante

La terminal quedará bloqueada ya que el servidor del protocolo de red Echo está en ejecución dentro del contenedor.

Acceso🔗

En otra terminal ejecuta el cliente del protocolo de red Echo indicando el servidor al cual ha de conectarse:

[nihilipster@localhost:~]$ java EchoClient 127.0.0.1 1234

Importante

La terminal quedará bloqueada ya que espera que escribas algo y aprietes la tecla de Enter.

Como resultado notarás que todo aquello que escribas en el cliente será reenviado por el servidor precedido por la palabra echo:, por ejemplo:

[nihilipster@localhost:~]$ java EchoClient 127.0.0.1 1234
¡Hola, mundo!
echo: ¡Hola, mundo!

Una vez que detengas el cliente (Ctrl + C o Ctrl + D en el emulador de terminal) también se detendrá el servidor-contenedor.

Ruby: echo_server & echo_client🔗

  • echo_server.rb

    # frozen_string_literal: true
    
    # Copyright (c) 2021 Antonio Hernández Blas <https://nihilipster.dev>
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    # 
    # The above copyright notice and this permission notice shall be included in all
    # copies or substantial portions of the Software.
    # 
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    # SOFTWARE.
    
    require 'socket'
    
    # Server
    class EchoServer
      def self.main(port)
        echo_socket = TCPServer.new(port)
        connection = echo_socket.accept
        begin
          while (line = connection.gets)
            message = line.chomp
            connection.puts message
            break if message.empty?
          end
        rescue SystemExit, Interrupt
          exit 1
        end
        connection.close
      end
    end
    
    EchoServer.main(ARGV[0])
    
  • echo_client.rb

    # frozen_string_literal: true
    
    # Copyright (c) 2021 Antonio Hernández Blas <https://nihilipster.dev>
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    # 
    # The above copyright notice and this permission notice shall be included in all
    # copies or substantial portions of the Software.
    # 
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    # SOFTWARE.
    
    require 'socket'
    
    # ECHO client
    class EchoClient
      def self.main(host, port)
        echo_socket = TCPSocket.open(host, port)
        begin
          while (line = $stdin.gets)
            message = line.chomp
            echo_socket.puts message
            $stdout.puts "echo: #{echo_socket.gets.chomp}"
            break if message.empty?
          end
        rescue SystemExit, Interrupt
          exit 1
        end
        echo_socket.close
      end
    end
    
    EchoClient.main(ARGV[0], ARGV[1])
    

Dockerfile🔗

FROM     ruby:2.7.4-bullseye
LABEL    description="echo_server"
LABEL    source="https://rosettacode.org/wiki/Echo_server#Ruby"

ENV      APP_DIR="/app"
ENV      ECHO_PORT="7"

EXPOSE   "$ECHO_PORT"

WORKDIR  "$APP_DIR"

COPY     echo_server.rb "$APP_DIR"/

CMD      ["bash", "-c", "ruby echo_server.rb $ECHO_PORT"]

Importante

  • El archivo Dockerfile se encontrará en la misma carpeta que los archivos echo_server.rb y echo_client.rb.
  • La imagen usada es de Ruby YARV, la imagen oficial de Ruby en Docker Hub, por lo que es posible llevar a cabo la instalación de otras gemas de ser necesario mediante gem.
  • Solo el servidor echo_server.rb es copiado (COPY) en el contenedor.
  • La ejecución del cliente echo_client.rb es llevado a cabo dentro del sistema anfitrión.

Imagen🔗

Puedes obtener primero la imagen de Docker:

[nihilipster@localhost:~]$ docker image pull ruby:2.7.4-bullseye
ruby:2.7.4-bullseye: Pulling from library/ruby
...
...
...
Status: Downloaded newer image for ruby:2.7.4-bullseye
docker.io/library/ruby:2.7.4-bullseye

Para después crear una imagen a partir de esta:

[nihilipster@localhost:~]$ docker image build --tag echo-server:0.1.0 .
Sending build context to Docker daemon  15.36kB
...
...
...
Successfully built xxxxxxxxxxxx
Successfully tagged echo-server:0.1.0

Contenedor🔗

Finalmente se crea un contenedor en base a la nueva imagen echo-server:0.1.0:

[nihilipster@localhost:~]$ docker container create --name echo-server --publish 127.0.0.1:1234:7 \
  --tty --interactive echo-server:0.1.0

Importante

  • En el archivo Dockerfile se ha expuesto (EXPOSE) el puerto TCP #7 para la comunicación externa hacía el servidor del protocolo de red Echo.
  • Mediante --publish se ha establecido un redireccionamiento (o binding) del socket 127.0.0.1:1234 al puerto TCP #7. Lo anterior con la finalidad de que al establecer una conexión con el anfitrión (127.0.0.1) en el puerto TCP #1234 dicha conexión realmente sea al puerto TCP #7 del contenedor.

Ejecución🔗

Inicializa el contenedor recientemente creado:

[nihilipster@localhost:~]$ docker container start --attach --interactive echo-server

Importante

La terminal quedará bloqueada ya que el servidor del protocolo de red Echo está en ejecución dentro del contenedor.

Acceso🔗

En otra terminal ejecuta el cliente del protocolo de red Echo indicando el servidor al cual ha de conectarse:

[nihilipster@localhost:~]$ ruby echo_client.rb 127.0.0.1 1234

Importante

La terminal quedará bloqueada ya que espera que escribas algo y aprietes la tecla de Enter.

Como resultado notarás que todo aquello que escribas en el cliente será reenviado por el servidor precedido por la palabra echo:, por ejemplo:

[nihilipster@localhost:~]$ ruby echo_client.rb 127.0.0.1 1234
¡Hola, mundo!
echo: ¡Hola, mundo!

Una vez que detengas el cliente (Ctrl + C o Ctrl + D en el emulador de terminal) también se detendrá el servidor-contenedor.

Docker Hub🔗

Repositorio🔗

Es posible compartir una imagen construida, haciendo uso de un registro de imágenes, siendo Docker Hub el más conocido.

Para lo siguiente es necesario crear una cuenta de usuario (<Docker ID>) en Docker Hub y acceder a https://hub.docker.com/repository/create para crear un nuevo repositorio (imagen remota).

El formulario obtenido es rellenado teniendo en cuenta lo siguiente:

  • Name: nombre del repositorio (imagen remota).
  • Description: descripción del repositorio (imagen remota).
  • Visibility: visibilidad pública o privada.

En este caso podría ser:

  • Name: echo-server
  • Description: Echo server in Ruby
  • Visibility: public

Al finalizar estarás en el dashboard del repositorio: https://hub.docker.com/repository/docker/<Docker ID>/echo-server.

Información

El perfil del repositorio (https://hub.docker.com/r/<Docker ID>/echo-server) cuenta con un Readme vacio por default pero es posible modificarlo accediendo al dashboard del repositorio.

Publicación🔗

Para publicar la imagen local al repositorio (imagen remota) es necesario cambiar la etiqueta de la imagen local:

[nihilipster@localhost:~]$ docker tag echo-server:0.1.0 <Docker ID>/echo-server:0.1.0

Autentificarse ante el registro de imágenes (por default es Docker Hub) haciendo uso de la cuenta de usuario (<Docker ID>):

[nihilipster@localhost:~]$ docker login --username <Docker ID>
Password: 
Login Succeeded

Y finalmente publicar la imagen local al repositorio:

docker push <Docker ID>/echo-server:tagname
The push refers to repository [docker.io/<USUARIO>/echo-server]
34381271ebba: Pushed 
a214018272ff: Pushed 
0bc5e3a10491: Mounted from library/ruby 
7b095a612410: Mounted from library/ruby 
65be50d09676: Mounted from library/ruby 
21abb8089732: Mounted from library/ruby 
9889ce9dc2b0: Mounted from library/ruby 
21b17a30443e: Mounted from library/ruby 
05103deb4558: Mounted from library/ruby 
a881cfa23a78: Mounted from library/ruby 
0.1.0: digest: sha256:4d6dfe4edca0970abf10d3af575ac3ad4c56551703563c440dd736b9cad16123 size: 2418

Alternativas🔗

Algunas alternativas, publicas y/o privadas, a Docker Hub son:

Mientras que algunas aplicaciones para crear un registro son Harbor y Portus.

Almacenamiento en Docker🔗

Un contenedor en Docker puede tener un espacio para el almacenamiento de datos mediante drivers de almacenamiento (subsistemas de almacenamiento de Docker) ya integrados en Docker o bien ofrecidos mediante plugins por terceros.

Por default un contenedor puede almacenar datos dentro de si mismo (tal es el caso cuando se instalan paquetes mediante un gestor de paquetes) pero en otras ocasiones se busca compartir datos entre el anfitrión (la computadora donde está en ejecución el contenedor o la instancia de Docker) y el contenedor o bien entre varios contenedores.

Volúmenes🔗

Un volumen es un almacenamiento (archivo o carpeta) controlado por Docker.

Para listar los volúmenes creados y disponibles en Docker:

[nihilipster@localhost:~]$ docker volume ls
DRIVER    VOLUME NAME

Para crear un nuevo volumen indicando su nombre, shared01 como ejemplo:

[nihilipster@localhost:~]$ docker volume create shared01
shared01
[nihilipster@localhost:~]$ docker volume ls
DRIVER    VOLUME NAME
local     shared01

Para determinar la carpeta que retiene los datos del volumen se inspecciona el volumen:

[nihilipster@localhost:~]$ docker volume inspect shared01
[
    {
        ...
        "Driver": "local",
        ...
        "Mountpoint": "/var/lib/docker/volumes/shared01/_data",
        "Name": "shared01",
        ...
        ...
    }
]

Puede observarse que aquellos datos almacenados por un contenedor en el volumen shared01 serán realmente almacenados en la carpeta /var/lib/docker/volumes/shared01/_data (Mountpoint) del sistema operativo anfitrión.

Importante

Puesto que el sistema anfitrión puede ser GNU/Linux, Microsoft Windows o macOS es importante considerar la ruta o ubicación de archivos y carpetas en ellos. En este ejemplo se está haciendo uso del sistema operativo GNU/Linux por lo que se obtiene ese tipo de ruta en Mountpoint.

Para poner a disposición (montar) un volumen en un contenedor se indica esto al momento de crear al contenedor:

[nihilipster@localhost:~]$ docker container create --name alpine-shared01 --tty --interactive \
  --mount source=shared01,target=/shared01 alpine:3.13.2

La opción usada es --mount con la cual se indica que el volumen shared01 será montado o puesto a disposición del contenedor en la carpeta (target) /shared01 dentro del contenedor.

Importante

Puesto que el contenedor es creado a partir de la imagen de Alpine Linux es importante conocer el sistema operativo GNU/Linux en especifico el uso de su interprete de comandos así como de los comandos necesarios para manipular archivos y carpetas en él (pwd, ls, mkdir, cd, cat, etc).

Finalmente se podrá observar como los archivos creados en el contenedor son accesibles por el anfitrión una vez que el contenedor es iniciado (docker container start --attach --interactive alpine-shared01):

Importante

Observar que antes de llevar a cabo lo siguiente el volumen estará vacío.

  • En el contenedor:

    / # ls -l /shared01
    total 0
    / # echo "¡Hola, mundo!" > /shared01/hola.txt
    / # ls -l /shared01
    total 4
    -rw-r--r--    1 root     root            31 Feb  3 01:18 hola.txt
    / #
    
  • En el anfitrión:

    [nihilipster@localhost:~]$ ls -l /var/lib/docker/volumes/shared01/_data
    total 4
    -rw-r--r-- 1 root root 31 Feb  2 19:18 hola.txt
    [nihilipster@localhost:~]$ cat /var/lib/docker/volumes/shared01/_data/hola.txt
    ¡Hola, mundo!
    

Importante

Puesto que el sistema anfitrión puede ser GNU/Linux, Microsoft Windows o macOS es necesario que ajustes esta sección según sea tu caso.

Bind mounts🔗

Un bind mount es un almacenamiento (archivo o carpeta) disponible al contenedor desde el mismo sistema operativo anfitrión sin la intervención de Docker por lo que su ruta o ubicación puede ser decidida por uno y no por Docker.

Suponiendo que se tenga en el anfitrión la carpeta /tmp/shared01 o bien C:\shared01, según sea el caso, se puede crear el contenedor de la siguiente forma:

[nihilipster@localhost:~]$ docker container create --name alpine-shared01 --tty --interactive \
  --mount type=bind,source=/tmp/shared01,target=/shared01 alpine:3.13.2

La opción usada es --mount con la cual se indica que el bind mount /tmp/shared01 será montado o puesto a disposición del contenedor en la carpeta (target) /shared01 dentro del contenedor.

Advertencia

Es necesario que el bind mount ya exista previa creación del contenedor, de no ser así se obtendrá el error bind source path does not exist.

Finalmente se podrá observar como los archivos creados en el contenedor son accesibles por el anfitrión una vez que el contenedor es iniciado (docker container start --attach --interactive alpine-shared01):

Importante

Observar que antes de llevar a cabo lo siguiente el bind mount estará vacío.

  • En el contenedor:

    / # ls -l /shared01/
    total 0
    / # echo "¡Hola, mundo!" > /shared01/hola.txt
    / # ls -l /shared01
    total 4
    -rw-r--r--    1 root     root            31 Feb  3 01:18 hola.txt
    / #
    
  • En el anfitrión:

    [nihilipster@localhost:~]$ ls -l /tmp/shared01/
    total 4
    -rw-r--r-- 1 root root 31 Feb  3 08:00 hola.txt
    [nihilipster@localhost:~]$ cat /tmp/shared01/hola.txt
    ¡Hola, mundo!
    

Importante

Puesto que el sistema anfitrión puede ser GNU/Linux, Microsoft Windows o macOS es necesario que ajustes esta sección según sea tu caso.

Servidor HTTP en Docker (contenido estático)🔗

Un Servidor de HTTP o Servidor Web puede exponer dos tipos de contenidos hacía un Cliente de HTTP o Cliente Web: estático y/o dinámico.

El contenido estático es todo aquel recurso accedido por el Cliente HTTP mediante un URL y que previamente ha sido creado y depositado en alguna carpeta a disposición del Servidor de HTTP, por lo que el Servidor de HTTP solo se encarga de entregar dicho recurso al Cliente de HTTP. Ejemplos de contenido estático pueden ser archivos de imágenes (JPEG, PNG, GIF, etc), archivos de audio (MP3, OGG, FLAC, etc), documentos de ofimática (DOCX, XLSX, PDF, etc), archivos de vídeo (AVI, WAV, MP4, etc) así como archivos de texto plano (TXT, HTML, CSS, CSV, JSON, XML, etc).

darkhttpd🔗

Dockerfile🔗

A partir de la imagen de Alpine Linux se definen dos variables de entorno (ENV): una para indicar el puerto TCP en el cual estará expuesto el servidor web y otra para indicar la carpeta que se expondrá al exterior (conocida como carpeta raíz) por el servidor web.

FROM     alpine:3.13.2
LABEL    description="Sitio web estatico con darkhttpd"

ENV      HTTP_PORT 80
ENV      HTTP_DIR  /srv/www

EXPOSE   "$HTTP_PORT"
WORKDIR  "$HTTP_DIR"

RUN      apk add darkhttpd
RUN      mkdir -p "$HTTP_DIR"

CMD      ["sh", "-c", "darkhttpd $HTTP_DIR --port $HTTP_PORT"]

El servidor web que usarás, como ejemplo, será darkhttpd. darkhttpd es un servidor web muy básico en comparación con otros servidores web. El primero argumento indica la carpeta raíz mientras que --port indica el puerto TCP a usar en espera de conexiones.

Imagen🔗

Construye la imagen con el nombre darkhttpd y la etiqueta 0.1.0:

[nihilipster@localhost:~]$ docker build --tag darkhttpd:0.1.0 .

Contenedor🔗

Crea un nuevo contenedor en base a la imagen recientemente creada:

[nihilipster@localhost:~]$ docker container create --name darkhttpd01 --tty --interactive \
  --publish 127.0.0.1:8080:80 --mount type=bind,source=/tmp/www,target=/srv/www darkhttpd:0.1.0

El contenedor creado hace un redireccionamiento del puerto TCP 8080 del anfitrión al puerto TCP 80 del contenedor. Por otro lado se está poniendo a disposición del contenedor la carpeta /tmp/www como bind mount en la carpeta /srv/www la cual fue establecida como carpeta raíz en el archivo Dockerfile.

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Ejecución🔗

Inicia el contenedor:

[nihilipster@localhost:~]$ docker container start --attach --interactive darkhttpd01

Acceso🔗

Podrás acceder al servidor web con un cliente web, como por ejemplo un navegador web, haciendo uso del UR: http://127.0.0.1:8080.

Contenido🔗

Para corroborar que todo está funcionando puedes crear contenido estático dentro del bind mount: /tmp/www

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

mini_httpd🔗

Dockerfile🔗

A partir de la imagen de Alpine Linux se definen dos variables de entorno (ENV): una para indicar el puerto TCP en el cual estará expuesto el servidor web y otra para indicar la carpeta que se expondrá al exterior (conocida como carpeta raíz) por el servidor web.

FROM     alpine:3.13.2
LABEL    description="Sitio web estatico con mini_httpd"

ENV      HTTP_PORT 80
ENV      HTTP_DIR  /srv/www

EXPOSE   "$HTTP_PORT"
WORKDIR  "$HTTP_DIR"

RUN      apk add mini_httpd
RUN      mkdir -p "$HTTP_DIR"

CMD      ["sh", "-c", "mini_httpd -p $HTTP_PORT -d $HTTP_DIR -T UTF-8 -D -M 0"]

El servidor web que usarás, como ejemplo, será mini_httpd. mini_httpd es un servidor web muy básico en comparación con otros servidores web. Los argumentos -p y -d sirven para establecer el puerto TCP y la carpeta raíz respectivamente.

Imagen🔗

Construye la imagen con el nombre mini_httpd y la etiqueta 0.1.0:

[nihilipster@localhost:~]$ docker build --tag mini_httpd:0.1.0 .

Contenedor🔗

Crea un nuevo contenedor en base a la imagen recientemente creada:

[nihilipster@localhost:~]$ docker container create --name mini_httpd01 --tty --interactive \
  --publish 127.0.0.1:8080:80 --mount type=bind,source=/tmp/www,target=/srv/www mini_httpd:0.1.0

El contenedor creado hace un redireccionamiento del puerto TCP 8080 del anfitrión al puerto TCP 80 del contenedor. Por otro lado se está poniendo a disposición del contenedor la carpeta /tmp/www como bind mount en la carpeta /srv/www la cual fue establecida como carpeta raíz en el archivo Dockerfile.

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Ejecución🔗

Inicia el contenedor:

[nihilipster@localhost:~]$ docker container start --attach --interactive mini_httpd01

Acceso🔗

Podrás acceder al servidor web con un cliente web, como por ejemplo un navegador web, haciendo uso del UR: http://127.0.0.1:8080.

Contenido🔗

Para corroborar que todo está funcionando puedes crear contenido estático dentro del bind mount: /tmp/www

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

lighttpd🔗

Dockerfile🔗

A partir de la imagen de Alpine Linux se definen dos variables de entorno (ENV): una para indicar el puerto TCP en el cual estará expuesto el servidor web y otra para indicar la carpeta que se expondrá al exterior (conocida como carpeta raíz) por el servidor web.

FROM     alpine:3.13.2
LABEL    description="Sitio web estatico con lighttpd"

ENV      HTTP_PORT 80
ENV      HTTP_DIR  /srv/www

EXPOSE   "$HTTP_PORT"
WORKDIR  "$HTTP_DIR"

RUN      apk add lighttpd
RUN      mkdir -p "$HTTP_DIR"

# Se configura la carpeta raiz:
RUN      sed -i "s|^var.basedir.*|var.basedir = \"$HTTP_DIR\"|" /etc/lighttpd/lighttpd.conf
RUN      sed -i "s|^server.document-root.*|server.document-root = var.basedir|" /etc/lighttpd/lighttpd.conf

# Se activa el listado del contenido de carpetas:
RUN      sed -i "s|^#   dir-listing.activate.*|dir-listing.activate = \"enable\"|" /etc/lighttpd/lighttpd.conf
RUN      sed -i "s|^#   dir-listing.hide-dotfiles.*|dir-listing.hide-dotfiles = \"enable\"|" /etc/lighttpd/lighttpd.conf

# Se configura el puerto TCP:
RUN      sed -i "s|^# server.port.*|server.port = \"$HTTP_PORT\"|" /etc/lighttpd/lighttpd.conf

CMD      ["sh", "-c", "lighttpd -f /etc/lighttpd/lighttpd.conf -D"]

El servidor web que usarás, como ejemplo, será lighttpd.

Imagen🔗

Construye la imagen con el nombre lighttpd y la etiqueta 0.1.0:

[nihilipster@localhost:~]$ docker build --tag lighttpd:0.1.0 .

Contenedor🔗

Crea un nuevo contenedor en base a la imagen recientemente creada:

[nihilipster@localhost:~]$ docker container create --name lighttpd01 --tty --interactive \
  --publish 127.0.0.1:8080:80 --mount type=bind,source=/tmp/www,target=/srv/www lighttpd:0.1.0

El contenedor creado hace un redireccionamiento del puerto TCP 8080 del anfitrión al puerto TCP 80 del contenedor. Por otro lado se está poniendo a disposición del contenedor la carpeta /tmp/www como bind mount en la carpeta /srv/www la cual fue establecida como carpeta raíz en el archivo Dockerfile.

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Ejecución🔗

Inicia el contenedor:

[nihilipster@localhost:~]$ docker container start --attach --interactive lighttpd01

Acceso🔗

Podrás acceder al servidor web con un cliente web, como por ejemplo un navegador web, haciendo uso del UR: http://127.0.0.1:8080.

Contenido🔗

Para corroborar que todo está funcionando puedes crear contenido estático dentro del bind mount: /tmp/www

IMPORTANTE: ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

thttpd🔗

Advertencia

El servidor thttpd utiliza como scripts de CGI los archivos ejecutables encontrados en la carpeta ráiz por lo que su uso en Windows (como anfitrión) no es el esperado como servidor de contenido estático. Lo siguiente queda como referencia.

Dockerfile🔗

A partir de la imagen de Alpine Linux se definen dos variables de entorno (ENV): una para indicar el puerto TCP en el cual estará expuesto el servidor web y otra para indicar la carpeta que se expondrá al exterior (conocida como carpeta raíz) por el servidor web.

FROM     alpine:3.13.2
LABEL    description="Sitio web estatico con thttpd"

ENV      HTTP_PORT 80
ENV      HTTP_DIR  /srv/www

EXPOSE   "$HTTP_PORT"
WORKDIR  "$HTTP_DIR"

RUN      apk add thttpd
RUN      mkdir -p "$HTTP_DIR"

CMD      ["sh", "-c", "thttpd -p $HTTP_PORT -d $HTTP_DIR -T UTF-8 -D -M 0 -l -"]

El servidor web que usarás, como ejemplo, será thttpd. thttpd es un servidor web muy básico en comparación con otros servidores web. Los argumentos -p y -d sirven para establecer el puerto TCP y la carpeta raíz respectivamente.

Imagen🔗

Construye la imagen con el nombre thttpd y la etiqueta 0.1.0:

[nihilipster@localhost:~]$ docker build --tag thttpd:0.1.0 .

Contenedor🔗

Crea un nuevo contenedor en base a la imagen recientemente creada:

[nihilipster@localhost:~]$ docker container create --name thttpd01 --tty --interactive \
  --publish 127.0.0.1:8080:80 --mount type=bind,source=/tmp/www,target=/srv/www thttpd:0.1.0

El contenedor creado hace un redireccionamiento del puerto TCP 8080 del anfitrión al puerto TCP 80 del contenedor. Por otro lado se está poniendo a disposición del contenedor la carpeta /tmp/www como bind mount en la carpeta /srv/www la cual fue establecida como carpeta raíz en el archivo Dockerfile.

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Ejecución🔗

Inicia el contenedor:

[nihilipster@localhost:~]$ docker container start --attach --interactive thttpd01

Acceso🔗

Podrás acceder al servidor web con un cliente web, como por ejemplo un navegador web, haciendo uso del UR: http://127.0.0.1:8080.

Contenido🔗

Para corroborar que todo está funcionando puedes crear contenido estático dentro del bind mount: /tmp/www

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Servidor HTTP en Docker (contenido dinámico)🔗

Un Servidor de HTTP o Servidor Web puede exponer dos tipos de contenidos hacía un Cliente de HTTP o Cliente Web: estático y/o dinámico.

El contenido dinámico es todo aquel recurso accedido por el Cliente HTTP mediante un URL y que es generado por parte del Servidor de HTTP haciendo uso de algún lenguaje de programación por lo que el Servidor de HTTP se encarga de atender la conexión con el cliente mientras que el lenguaje de programación procesa la solicitud y genera una respuesta al cliente. Ejemplos de contenido dinámico pueden ser archivos de imágenes (JPEG, PNG, GIF, etc), archivos de audio (MP3, OGG, FLAC, etc), documentos de ofimática (DOCX, XLSX, PDF, etc), archivos de vídeo (AVI, WAV, MP4, etc) así como archivos de texto plano (TXT, HTML, CSS, CSV, JSON, XML, etc) generados de manera dinámica por el lenguaje de programación usado.

Java (JavaServer Pages & Apache Tomcat)🔗

Dockerfile🔗

A partir de la imagen de Apache Tomcat se definen dos variables de entorno (ENV): una para indicar el puerto TCP en el cual estará expuesto el servidor web y otra para indicar la carpeta en la cual residirá la aplicación web.

FROM     tomcat:9.0.20-jre8-alpine
LABEL    description="Página web dinámica generada con JavaServer Pages (JSP) en Apache Tomcat"

ENV      HTTP_PORT 80
ENV      APP_DIR   "/usr/local/tomcat/webapps/ROOT"

EXPOSE   "$HTTP_PORT"
WORKDIR  "$APP_DIR"

# Se configura el puerto TCP para el servidor web Coyote integrado en Apache Tomcat:
RUN      sed -i "s|port=\"8080\"|port=\"80\"|" /usr/local/tomcat/conf/server.xml

CMD      ["sh", "-c", "catalina.sh run"]

Imagen🔗

Construye la imagen con el nombre tomcat y la etiqueta 0.1.0:

[nihilipster@localhost:~]$ docker build --tag tomcat:0.1.0 .

Contenedor🔗

Crea un nuevo contenedor en base a la imagen recientemente creada:

[nihilipster@localhost:~]$ docker container create --name tomcat01 \
  --tty --interactive --publish 127.0.0.1:8080:80 \
  --mount type=bind,source=/tmp/ROOT,target=/usr/local/tomcat/webapps/ROOT tomcat:0.1.0

El contenedor creado hace un redireccionamiento del puerto TCP 8080 del anfitrión al puerto TCP 80 del contenedor. Por otro lado se está poniendo a disposición del contenedor la carpeta /tmp/ROOT como bind mount en la carpeta /usr/local/tomcat/webapps/ROOT la cual fue establecida como la carpeta de la aplicación web en el archivo Dockerfile.

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Aplicación Web🔗

Dentro de la carpeta /tmp/ROOT crea el archivo index.jsp con el siguiente contenido:

<%--
  -- JavaServer Pages (JSP)
  -- https://es.wikipedia.org/wiki/JavaServer_Pages
  -- 
  -- La Directiva page, establece lo siguiente:
  -- 
  -- * language: lenguaje de programación a usar embebido en este archivo JSP: java
  -- * contenType: tipo de contenido que generará este archivo JSP: text/html
  -- * pageEncoding: codificación de carácteres a usar para este archivo JSP: UTF-8
  -- 
  -- JSP forza la creación e inicio de una sesión (JSESSIONID) con el cliente (navegador web).
  -- Si se quiere desactivar esta carácteristica se establece el atributo session a false en la
  -- directiva page.
  -- 
  --     <%@
  --       page session="false"
  --            language="java"
  --     %>
  -- 
  -- Un JSP tiene acceso a varios objetos globales u objetos implicitos:
  -- 
  -- *  HttpServletRequest     request
  -- *  HttpServletResponse    response
  -- *  HttpSession            session
  -- *  ServletContext         application
  -- *  ServletConfig          config
  --%>
<%@
  page language="java"
       contentType="text/html;charset=UTF-8"
       pageEncoding="UTF-8"
%>
<%--
  -- Importación de clases de Java
  --%>
<%@
  page import="java.util.List"
       import="java.util.ArrayList"
       import="java.util.Arrays"
       import="java.util.Enumeration"
       import="java.util.Date"
       import="java.util.Locale"
       import="java.util.Properties"
       import="java.text.SimpleDateFormat"
       import="java.io.File"
%>
<%--
  -- Scriptlet: sentencias de código Java embebido/incrustado en los elementos estáticos del
  -- documento (por ejemplo HTML/XML).
  -- https://es.wikipedia.org/wiki/JavaServer_Pages#Scriptlets
  --%>
<%!
  /*
   * Definición de un método privado en este archivo JSP
   */
  private List<Double> generarNumerosPseudoAleatorios(int cantidad) {
    List<Double> numeros = new ArrayList<>();
    if (cantidad <= 0) {
      return numeros;
    }
    for (int i = 0; i < cantidad; i++) {
      numeros.add(Math.random());
    }
    return numeros;
  }
%>
<%--
  -- Declaración e inicialización de variables globales:
  --%>
<%
  String titulo = "¡Bienvenid@!";
  SimpleDateFormat formatoFecha = new SimpleDateFormat("EEEE d 'de' MMMM 'del' yyyy", new Locale("es", "MX"));
  String fecha = formatoFecha.format(new Date());
  SimpleDateFormat formatoHora = new SimpleDateFormat("H:mm:ss a", new Locale("es", "MX"));
  String hora = formatoHora.format(new Date());
  List<Double> numerosPseudoAleatorios = generarNumerosPseudoAleatorios(5);
%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><%= titulo %></title>
  <link rel="stylesheet" href="https://www.w3.org/StyleSheets/Core/Modernist" type="text/css" />
</head>
<body>
  <main class="container">
    <h1><%= titulo %></h1>
    <p>Fecha y hora actual del servidor: <%= fecha + ", " + hora %></p>
    <h2>Números pseudo-aleatorios</h2>
    <ol>
<%
  for (Double numero : numerosPseudoAleatorios) {
%>
      <li><code><%= numero %></code></li>
<%
  }
%>
    </ol>
    <h2>Entorno de ejecución</h2>
    <dl>
<%
  /*
   * Información encontrada en el entorno de ejecución
   * (Contenedor de Servlets o Servidor de Aplicaciones).
   */
  Properties propiedades = System.getProperties();
  Object[] llaves = propiedades.keySet().toArray();
  Arrays.sort(llaves);
  for (int i = 0; i < llaves.length; i++) {
    String llave = (String)llaves[i];
    String valor = (String)propiedades.get(llave);
%>
      <dt><em><%= llave %></em></dt>
      <dd><code><%= valor %></code></dd>
<%
  }
%>
    </dl>
    <h2>HTTP - Solicitud</h2>
    <dl>
      <dt><em>request.getCharacterEncoding()</em></dt>
      <dd><code><%=request.getCharacterEncoding()%></code></dd>
      <dt><em>request.getContentType()</em></dt>
      <dd><code><%=request.getContentType()%></code></dd>
      <dt><em>request.getLocale()</em></dt>
      <dd><code><%=request.getLocale()%></code></dd>
      <dt><em>request.getProtocol()</em></dt>
      <dd><code><%=request.getProtocol()%></code></dd>
      <dt><em>request.getRemoteAddr()</em></dt>
      <dd><code><%=request.getRemoteAddr()%></code></dd>
      <dt><em>request.getRemoteHost()</em></dt>
      <dd><code><%=request.getRemoteHost()%></code></dd>
      <dt><em>request.getScheme()</em></dt>
      <dd><code><%=request.getScheme()%></code></dd>
      <dt><em>request.getServerName()</em></dt>
      <dd><code><%=request.getServerName()%></code></dd>
      <dt><em>request.getServerPort</em></dt>
      <dd><code><%=request.getServerPort()%></code></dd>
      <dt><em>request.isSecure</em></dt>
      <dd><code><%=request.isSecure()%></code></dd>
      <dt><em>request.getAuthType()</em></dt>
      <dd><code><%=request.getAuthType()%></code></dd>
      <dt><em>request.getContextPath()</em></dt>
      <dd><code><%=request.getContextPath()%></code></dd>
      <dt><em>request.getMethod()</em></dt>
      <dd><code><%=request.getMethod()%></code></dd>
      <dt><em>request.getPathInfo()</em></dt>
      <dd><code><%=request.getPathInfo()%></code></dd>
      <dt><em>request.getPathTranslated()</em></dt>
      <dd><code><%=request.getPathTranslated()%></code></dd>
      <dt><em>request.getQueryString()</em></dt>
      <dd><code><%=request.getQueryString()%></code></dd>
      <dt><em>request.getRemoteUser()</em></dt>
      <dd><code><%=request.getRemoteUser()%></code></dd>
      <dt><em>request.getRequestedSessionId()</em></dt>
      <dd><code><%=request.getRequestedSessionId()%></code></dd>
      <dt><em>request.getRequestURI()</em></dt>
      <dd><code><%=request.getRequestURI()%></code></dd>
      <dt><em>request.getRequestURL.toString()</em></dt>
      <dd><code><%= request.getRequestURL().toString() %></code></dd>
      <dt><em>request.getServletPath()</em></dt>
      <dd><code><%= request.getServletPath() %></code></dd>
    </dl>
    <h2>HTTP - Cabeceras de la solicitud</h2>
    <dl>
<%
  /*
   * Información enviada por el cliente y encontrada dentro de la solicitud.
   */
  Enumeration<String> cabeceras = request.getHeaderNames();
  while (cabeceras.hasMoreElements()) {
    String llave = (String)cabeceras.nextElement();
    String valor = request.getHeader(llave);
%>
      <dt><em><%= llave %></em></dt>
      <dd><code><%= valor %></code></dd>
<%
  }
%>
    </dl>
  </main>
</body>
</html>

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Ejecución🔗

Inicia el contenedor:

[nihilipster@localhost:~]$ docker container start --attach --interactive tomcat01

Acceso🔗

Podrás acceder al servidor web con un cliente web, como por ejemplo un navegador web, haciendo uso del UR: http://127.0.0.1:8080.

Contenido🔗

Para corroborar que todo está funcionando puedes hacer pequeñas modificaciones al archivo index.jsp y ver los efectos de dichas ediciones en el navegador web.

Importante

Ya que el anfitrión puede ser Windows, macOS o GNU/Linux es importante que consideres que carpeta estás usando para bind mount.

Cliente HTTP en Docker🔗

Existen distintos clientes HTTP disponibles para distintas plataformas o sistemas operativos, siendo un navegadores web el más conocido y de más amplio uso

Lynx y Curl🔗

Para ejemplificar otros clientes HTTP se hará uso de lynx y cURL, ambas aplicaciones en la línea de comandos.

El nodo cliente será un contenedor de Alpine Linux con lynx y curl como clientes HTTP y dirección IP 10.2.3.4.

El nodo servidor será un contenedor de Apline Linux con thttpd como servidor HTTP y dirección IP 10.5.6.7.

Red virtual en Docker🔗

Para tener un mayor control sobre la configuración y comunicación entre los nodos cliente-servidor en Docker se creará una red virtual usando el controlador bridge.

[nihilipster@localhost:~]$ docker network create --driver bridge --subnet 10.0.0.0/8 --gateway 10.0.0.1 \
  cliente-servidor-http

Puedes obtener la lista de redes en Docker mediante docker network ls:

[nihilipster@localhost:~]$ docker network ls
NETWORK ID     NAME                    DRIVER    SCOPE
xxxxxxxxxxxx   bridge                  bridge    local
xxxxxxxxxxxx   cliente-servidor-http   bridge    local
xxxxxxxxxxxx   host                    host      local
xxxxxxxxxxxx   none                    null      local

Puedes obtener información sobre la subred y la puerta de salida de una red en particular mediante docker network inspect cliente-servidor-http por ejemplo:

[
    {
        "Name": "cliente-servidor-http",
        ...
        "Driver": "bridge",
        ...
                    "Subnet": "10.0.0.0/8",
                    "Gateway": "10.0.0.1"
        ...
    }
]

La red se llama cliente-servidor-http y su subred es 10.0.0.0/8 con puerta de salida 10.0.0.1. Para determinar el rango de direcciones IP disponibles en dicha subred puedes hacer uso de http://jodies.de/ipcalc, introduciendo 10.0.0.0 en Address y 8 en Netmask para finalmente prestar atención a los valores de HostMin y HostMax cuando des click al botón Calculate.

Nodo Cliente🔗

Dockerfile🔗

FROM     alpine:3.13.2
LABEL    description="Cliente de HTTP"

ENV      HOME="/root"
ENV      DESCARGAS="$HOME/Descargas"

WORKDIR  "$DESCARGAS"

RUN      apk add lynx
RUN      apk add curl

CMD      ["sh"]

Creación de la imagen🔗

[nihilipster@localhost:~]$ docker build --tag cliente-http:0.1.0 .

Creación del contenedor🔗

[nihilipster@localhost:~]$ docker container create --name cliente-http01 --tty --interactive \
    --mount type=bind,source=/tmp/Descargas,target=/root/Descargas --network cliente-servidor-http \
--ip 10.2.3.4 cliente-http:0.1.0

Importante

Observa que se hace uso de un bind mount, /tmp/Descargas, por lo que es importante que consideres su ubicación en base al anfitrión que uses.

Ejecución del contenedor🔗

[nihilipster@localhost:~]$ docker container start --attach --interactive cliente-http01

Nodo servidor🔗

Dockerfile🔗

FROM     alpine:3.13.2
LABEL    description="Servidor de HTTP"

ENV      HTTP_PORT 80
ENV      HTTP_DIR  /srv/www

EXPOSE   "$HTTP_PORT"
WORKDIR  "$HTTP_DIR"

RUN      apk add thttpd
RUN      mkdir -p "$HTTP_DIR"

COPY     www/* ./
RUN      find ./ -type f -exec chmod 644 {} \;

CMD      ["sh", "-c", "thttpd -p $HTTP_PORT -d $HTTP_DIR -T UTF-8 -D -M 0 -l -"]

Contenido estático🔗

En la misma carpeta donde se encuentra el archivo Dockerfile (para el nodo servidor) crea la carpeta www y en ella el archivo index.html con el siguiente contenido:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8"/>
  <title>Inicio</title>
</head>
<body>
  <h1>¡Bienvenid@!</h1>
</body>
</html>

Creación de la imagen🔗

[nihilipster@localhost:~]$ docker build --tag servidor-http:0.1.0 .

Creación del contenedor🔗

[nihilipster@localhost:~]$ docker container create --name servidor-http01 --tty --interactive \
  --network cliente-servidor-http --ip 10.5.6.7 servidor-http:0.1.0

Ejecución del contenedor🔗

[nihilipster@localhost:~]$ docker container start --attach --interactive servidor-http01

Comunicación de nodos🔗

Estando en el nodo cliente puedes acceder al nodo servidor mediante lynx con el comando lynx http://10.5.6.7, de igual manera puedes descargar al cliente el archivo index.html encontrado en el nodo servidor mediante el comando curl http://10.5.6.7/index.html --output hola.txt observando que en la carpeta /root/Descargas se creará el archivo hola.txt.

También te será posible ejecutar:

  • lynx https://nihilipster.dev
  • curl http://nihilipster.dev --output nihilipster.dev.txt

Referencias de comandos🔗

Podrás encontrar las siguientes referencias para trabajar con Docker:

Seguridad🔗

OWASP provee una referencia de errores y sugerencias para mejorar la seguridad en el uso de Docker, imágenes y contenedores en Docker Security Cheat Sheet