Docker🔗
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:
- Alpine Linux: versión 3.13
- Debian: versión 10
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
sh
también está disponible en Debian pero realmente se trata dedash
(Debian Almquist Shell).
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:
- Learning the Shell.
- Ryans Tutorials, Linux Tutorial: solo las secciones 1, 2, 3, 5, 7, 8, 9, 10, 11 y 14.
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:
- adoptopenjdk: https://hub.docker.com/_/adoptopenjdk
- openjdk: https://hub.docker.com/_/openjdk
- azul: https://hub.docker.com/u/azul
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:
- Otros sistemas operativos:
- CentOS: https://hub.docker.com/_/centos
- fedora: https://hub.docker.com/_/fedora
- Windows: https://hub.docker.com/_/microsoft-windows
- Lenguajes de programación:
- C/C++ con GCC: https://hub.docker.com/_/gcc
- C# y ASP (.Net): https://hub.docker.com/_/microsoft-dotnet
- F#: https://hub.docker.com/_/fsharp
- PHP: https://hub.docker.com/_/php
- Python: https://hub.docker.com/_/python
- Ruby: https://hub.docker.com/_/ruby
- Node.js: https://hub.docker.com/_/node
- Sistemas Gestores de Bases de Datos:
- MariaDB: https://hub.docker.com/_/mariadb
- MySQL: https://hub.docker.com/_/mysql
- MongoDB:https://hub.docker.com/_/mongo
- PostgreSQL: https://hub.docker.com/_/postgres
- Redis: https://hub.docker.com/_/redis
- Cassandra: https://hub.docker.com/_/cassandra
- CouchDB: https://hub.docker.com/_/couchdb
- Aplicaciones o servidores:
- nginx: https://hub.docker.com/_/nginx
- httpd: https://hub.docker.com/_/httpd
- Drupal: https://hub.docker.com/_/drupal
- Joomla: https://hub.docker.com/_/joomla
- Sonarqube: https://hub.docker.com/_/sonarqube
- Solr: https://hub.docker.com/_/solr
- Odoo: https://hub.docker.com/_/odoo
- Jenkins: https://registry.hub.docker.com/_/jenkins/
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 archivosEchoClient.java
yEchoServer.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 socket127.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 archivosecho_server.rb
yecho_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 socket127.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:
- Alibaba Cloud Container Registry
- Amazon Elastic Container Registry
- Azure Container Registry
- CentOS Container Registry
- Container Registry
- Dist Container Registry
- Fedora Container Registry
- GitHub Container Registry
- GitLab Container Registry
- Google Cloud Container Registry
- IBM Cloud Container Registry
- JFrog Container Registry
- Mirantis Secure Registry
- Nexus Repository
- Oracle Cloud Infrastructure Container Registry
- openSUSE Container Registry
- Red Hat Quay.io
- Scaleway Container Registry
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