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