El software de código abierto Docker se ha es­ta­ble­ci­do como estándar para la vi­sua­li­za­ción de co­n­te­ne­do­res. La vi­r­tua­li­za­ción de co­n­te­ne­do­res supone una co­n­ti­nua­ción para el de­sa­rro­llo de las máquinas virtuales, pero con una di­fe­re­n­cia im­po­r­ta­n­te: en vez de simular un sistema operativo completo, se vi­r­tua­li­za una apli­ca­ción en un co­n­te­ne­dor. Hoy día, los co­n­te­ne­do­res Docker se utilizan en todas las fases del ciclo vital del software, como el de­sa­rro­llo, el testeo y la ejecución.

Dentro del eco­si­s­te­ma Docker existen distintos conceptos. Reconocer y entender estos co­m­po­ne­n­tes es esencial para trabajar de manera fluida con Docker. Además del Do­c­ke­r­fi­le, otros co­m­po­ne­n­tes im­po­r­ta­n­tes son la Docker Image y el Docker Container. En este artículo, te ex­pli­ca­mos el Do­c­ke­r­fi­le en pro­fu­n­di­dad y te damos consejos prácticos para uti­li­zar­lo.

¿Qué es un Do­c­ke­r­fi­le?

El Do­c­ke­r­fi­le es la unidad fu­n­da­me­n­tal del eco­si­s­te­ma Docker. Describe los pasos para crear una imagen de Docker. El flujo de in­fo­r­ma­ción sigue este esquema central: Do­c­ke­r­fi­le > Docker Image > Docker Container.

Un co­n­te­ne­dor Docker tiene una vida limitada e in­ter­ac­túa con su entorno. Imagina que co­n­te­ne­dor es como un organismo vivo. Piensa en un organismo uni­ce­lu­lar, como una célula de levadura. Siguiendo esta analogía, una imagen Docker equivale, digamos, a la in­fo­r­ma­ción genética: todos los co­n­te­ne­do­res creados a partir de una imagen son iguales, como todos los or­ga­ni­s­mos uni­ce­lu­la­res clonados a partir de una unidad de in­fo­r­ma­ción genética. Entonces, ¿cómo entra el Do­c­ke­r­fi­le en este esquema?

El Do­c­ke­r­fi­le define los pasos a seguir para crear una nueva imagen. Hay que entender que se empieza siempre con una imagen base existente. La nueva imagen nace de la imagen base. Además, hay ciertos cambios puntuales. En nuestro ejemplo de la célula de levadura, los cambios serían mu­ta­cio­nes. Un Do­c­ke­r­fi­le es de­te­r­mi­na­n­te en dos aspectos de la nueva imagen de Docker:

  1. La imagen base de la que procede la nueva imagen. Esto ancla la nueva imagen al árbol ge­nea­ló­gi­co del eco­si­s­te­ma Docker.
  2. Un conjunto de cambios es­pe­cí­fi­cos que di­fe­re­n­cian la nueva imagen de la imagen base.

¿Cómo funciona un Do­c­ke­r­fi­le y cómo se crea una imagen a partir de él?

En el fondo, el Do­c­ke­r­fi­le es un archivo de texto to­ta­l­me­n­te normal. El Do­c­ke­r­fi­le contiene un conjunto de in­s­tru­c­cio­nes, cada una en una línea distinta. Para crear una Docker Image, las in­s­tru­c­cio­nes se ejecutan una tras otra. Quizás te suene este esquema de la ejecución de un script por lotes. Durante la ejecución, se añaden paso por paso más capas a la imagen. Te ex­pli­ca­mos cómo funciona exac­ta­me­n­te en nuestro artículo temático sobre Docker Image.

Una imagen Docker se crea eje­cu­ta­n­do las in­s­tru­c­cio­nes de un Do­c­ke­r­fi­le. Este paso se conoce como el proceso build y empieza con la ejecución del comando “docker build”. El contexto de co­n­s­tru­c­ción es un concepto crucial: define a qué archivos y di­re­c­to­rios tiene acceso el proceso de co­n­s­tru­c­ción, donde un di­re­c­to­rio local hace las veces de fuente. El contenido del di­re­c­to­rio fuente se tra­n­s­fie­re al Docker Daemon al accionar “docker build”. Las in­s­tru­c­cio­nes co­n­te­ni­das en el Do­c­ke­r­fi­le reciben acceso a los archivos y di­re­c­to­rios co­n­te­ni­dos en el contexto de co­n­s­tru­c­ción.

A veces no queremos iniciar todos los archivos del di­re­c­to­rio fuente local en el contexto build. Para estos casos existe el archivo .do­c­ke­ri­g­no­re, que sirve para excluir archivos y di­re­c­to­rios del contexto de co­n­s­tru­c­ción y cuyo nombre se basa en el archivo .gitignore de Git. El punto antes del nombre del archivo indica que se trata de un archivo oculto.

¿Cómo se construye un Do­c­ke­r­fi­le?

Un Do­c­ke­r­fi­le es un archivo de texto simple que lleva el nombre de archivo “Do­c­ke­r­fi­le”. Ten en cuenta que es obli­ga­to­rio que la primera letra sea mayúscula. El archivo contiene una entrada por fila. Te mostramos a co­n­ti­nua­ción cómo se construye ge­ne­ra­l­me­n­te un Do­c­ke­r­fi­le:

# Comentario
INSTRUCCIONES Argumentos

Además de los co­me­n­ta­rios, el Do­c­ke­r­fi­le contiene in­s­tru­c­cio­nes y ar­gu­me­n­tos, que describen la co­n­s­tru­c­ción de las imágenes.

Co­me­n­ta­rios e in­s­tru­c­cio­nes del ana­li­za­dor

Los co­me­n­ta­rios contienen in­fo­r­ma­ción pensada pri­n­ci­pa­l­me­n­te para las personas. Conocido por los lenguajes de pro­gra­ma­ción Python o Perl, los co­me­n­ta­rios en un Do­c­ke­r­fi­le empiezan con una al­moha­di­lla (#). Durante el proceso de co­n­s­tru­c­ción, las filas de co­me­n­ta­rios se eliminan antes del pro­ce­sa­mie­n­to, por lo que es im­po­r­ta­n­te tener en cuenta que solo se co­n­si­de­ran como filas de co­me­n­ta­rios las que empiezan con la al­moha­di­lla.

Este sería un co­me­n­ta­rio válido:

# nuestra Base Image
FROM busybox

En cambio, aquí se generaría un fallo, ya que la al­moha­di­lla no está al principio de la línea:

FROM busybox # nuestra Base Image

Como variante de los co­me­n­ta­rios, están las in­s­tru­c­cio­nes del ana­li­za­dor. Van dentro de las líneas de co­me­n­ta­rio y deben aparecer siempre al principio del Do­c­ke­r­fi­le. Si no, se tratarán como co­me­n­ta­rios y se eli­mi­na­rán durante el build. También hay que tener en cuenta que Do­c­ke­r­fi­le solo puede utilizar una vez una de­te­r­mi­na­da in­s­tru­c­ción del ana­li­za­dor.

En el momento de escribir este artículo, solo existen dos tipos de in­s­tru­c­cio­nes de análisis: “syntax” y “escape”. La in­s­tru­c­ción de análisis “escape” define el símbolo de escape que debe uti­li­zar­se. Se utiliza para escribir in­s­tru­c­cio­nes en varias filas y para expresar ca­ra­c­te­res es­pe­cia­les. La in­s­tru­c­ción de análisis “syntax” define las reglas según las cuales el ana­li­za­dor tiene que procesar las in­s­tru­c­cio­nes del Do­c­ke­r­fi­le. He aquí un ejemplo:

# syntax=docker/dockerfile:1
# escape=\

In­s­tru­c­cio­nes, ar­gu­me­n­tos y variables

Las in­s­tru­c­cio­nes conforman la parte principal del contenido del Do­c­ke­r­fi­le. Eje­cu­ta­das una tras otra, las in­s­tru­c­cio­nes describen la es­tru­c­tu­ra es­pe­cí­fi­ca de una imagen Docker. Al igual que los comandos de la línea de comandos, las in­s­tru­c­cio­nes tienen ar­gu­me­n­tos. Algunas son di­re­c­ta­me­n­te co­m­pa­ra­bles a comandos es­pe­cí­fi­cos de la línea de comandos. De esta manera, existe una in­s­tru­c­ción COPY, que copia archivos y di­re­c­to­rios y co­rre­s­po­n­de apro­xi­ma­da­me­n­te al comando cp de la fila de comandos. A di­fe­re­n­cia de la línea de comandos, para algunas in­s­tru­c­cio­nes del Do­c­ke­r­fi­le hay reglas es­pe­cí­fi­cas para su secuencia. Además, ciertas in­s­tru­c­cio­nes solo pueden ocurrir una vez por Do­c­ke­r­fi­le.

Nota

No es obli­ga­to­rio escribir en ma­yú­s­cu­las las in­s­tru­c­cio­nes, pero has de seguir la co­n­ve­n­ción para crear un Do­c­ke­r­fi­le.

En el caso de los ar­gu­me­n­tos, hay que di­s­ti­n­guir entre las partes de código fijo y variable. Según la me­to­do­lo­gía de la app de 12 factores, Docker utiliza variables de entorno para la co­n­fi­gu­ra­ción de los co­n­te­ne­do­res. Dentro de un Do­c­ke­r­fi­le, las variables de entorno se definen con la in­s­tru­c­ción ENV. Así se asigna un valor a la variable de entorno:

Los valores al­ma­ce­na­dos en las variables de entorno pueden leerse y uti­li­zar­se como partes variables de los ar­gu­me­n­tos. Además, se utiliza una sintaxis especial que recuerda al script de Shell. El nombre de la variable de entorno se indica con un signo de dólar: $env_var. También existe una escritura al­te­r­na­ti­va para delimitar ex­plí­ci­ta­me­n­te el nombre de la variable. En este caso, el nombre de la variable va entre corchetes: ${env_var}. Veamos un ejemplo concreto:

# Establecer variable 'user' como valor 'admin' 
ENV user="admin"
# Establecer nombre de usuario como 'admin_user'
USER ${user}_user

Las in­s­tru­c­cio­nes Do­c­ke­r­fi­le más im­po­r­ta­n­tes

A co­n­ti­nua­ción, te pre­se­n­ta­mos las in­s­tru­c­cio­nes de Do­c­ke­r­fi­le más im­po­r­ta­n­tes. Tra­di­cio­na­l­me­n­te, algunas in­s­tru­c­cio­nes, sobre todo FROM, solo podían uti­li­zar­se una vez por Do­c­ke­r­fi­le. Con el paso del tiempo, ha surgido la co­n­s­tru­c­ción en múltiples etapas, o multi-stage builds, que describe múltiples imágenes en un Do­c­ke­r­fi­le, por lo que la li­mi­ta­ción se refiere a cada etapa in­di­vi­dual.

In­s­tru­c­ción De­s­cri­p­ción Co­me­n­ta­rio
FROM Es­ta­ble­cer imagen base Debe pre­se­n­tar­se como primera in­s­tru­c­ción; solo una entrada por etapa
ENV Variable de entorno para el proceso de co­n­s­tru­c­ción y es­ta­ble­cer vida del co­n­te­ne­dor —
ARG Declarar pa­rá­me­tros de la línea de comandos para el proceso de co­n­s­tru­c­ción Debe aparecer antes que la in­s­tru­c­ción FROM
WORKDIR Cambiar de di­re­c­to­rio actual —
USER Cambiar usuario y pe­r­te­ne­n­cia al grupo —
COPY Copiar los archivos y di­re­c­to­rios de una Image Crea una nueva capa
ADD Copiar los archivos y di­re­c­to­rios de una Image Crea una nueva capa; uso no re­co­me­n­da­ble
RUN Ejecutar el comando de Image durante el proceso de co­n­s­tru­c­ción Crea una nueva capa
CMD Es­ta­ble­cer ar­gu­me­n­tos estándar para el inicio del co­n­te­ne­dor Solo una entrada por build stage
EN­TR­Y­POI­NT Es­ta­ble­cer comando estándar para el inicio del co­n­te­ne­dor Solo una entrada por etapa de co­n­s­tru­c­ción
EXPOSE Definir asi­g­na­ción de puerto para co­n­te­ne­do­res en ejecución Los puertos deben estar activos al iniciar el co­n­te­ne­dor
VOLUME Integrar como volumen di­re­c­to­rio de Image al iniciar el co­n­te­ne­dor en el sistema anfitrión —

In­s­tru­c­ción FROM

La in­s­tru­c­ción FROM establece la imagen base sobre la que operan las in­s­tru­c­cio­nes po­s­te­rio­res. Esta directiva solo puede incluirse una vez por etapa de co­n­s­tru­c­ción y debe ser la primera in­s­tru­c­ción. Con una re­s­tri­c­ción: la in­s­tru­c­ción ARG puede pre­se­n­tar­se antes que FROM. Esto permite es­pe­ci­fi­car exac­ta­me­n­te qué imagen se utiliza como imagen base a través de un argumento de línea de comandos al iniciar el proceso build.

Cada imagen Docker debe basarse en una imagen base. En otras palabras, todas las imágenes Docker tienen exac­ta­me­n­te una imagen pre­de­ce­so­ra. Esto da lugar al clásico dilema “del huevo y la gallina”: la cadena debe empezar en algún sitio. En el universo Docker, el linaje comienza con la imagen “scratch”. Esta imagen mínima sirve como origen de toda imagen Docker.

Nota

En inglés, “from scratch” significa que algo está hecho con in­gre­die­n­tes básicos. El término se utiliza en re­po­s­te­ría y cocina. Si un Do­c­ke­r­fi­le comienza con la línea “FROM scratch”, alude a que la imagen se compila desde cero.

In­s­tru­c­cio­nes ENV y ARV

Estas dos in­s­tru­c­cio­nes asignan un valor a una variable. La di­s­ti­n­ción entre ambas afi­r­ma­cio­nes radica pri­n­ci­pa­l­me­n­te en el origen de los valores y en el contexto en el que están di­s­po­ni­bles las variables. Veamos primero la in­s­tru­c­ción ARG.

Con la in­s­tru­c­ción ARG, se declara dentro del Do­c­ke­r­fi­le una variable que solo está di­s­po­ni­ble mientras dure el proceso de co­n­s­tru­c­ción. El valor de una variable declarada con ARG se pasa como argumento de la línea de comandos cuando se inicia el proceso build. Veamos un ejemplo de­cla­ra­n­do la variable de build “user”:

ARG user

Al iniciar el proceso build pasamos el valor real de la variable:

docker build --build-arg user=admin

Al declarar la variable, existe la opción de es­pe­ci­fi­car un valor por defecto. Si no se pasa ningún argumento adecuado al iniciar el proceso build, la variable recibe el valor por defecto:

ARG user=tester

Si no se utiliza “--build-arg”, la variable “user” contiene por defecto el valor “tester”:

docker build

Mediante la in­s­tru­c­ción ENV, definimos una variable de entorno. A di­fe­re­n­cia de la in­s­tru­c­ción ARG, una variable definida con ENV existe tanto durante el proceso de co­n­s­tru­c­ción como durante la ejecución del co­n­te­ne­dor. Hay dos es­cri­tu­ras posibles para la in­s­tru­c­ción ENV.

  1. Escritura re­co­me­n­da­da:
ENV version="1.0"

2. Escritura al­te­r­na­ti­va para co­m­pa­ti­bi­li­dad con el pasado:

ENV version="1.0"
Consejo

La fu­n­cio­na­li­dad de la in­s­tru­c­ción ENV se co­rre­s­po­n­de más o menos a la del comando “export” de la línea de comandos.

In­s­tru­c­cio­nes WORKDIR y USER

La in­s­tru­c­ción WORKDIR sirve para cambiar los di­re­c­to­rios durante el proceso de co­n­s­tru­c­ción, así como al iniciar el co­n­te­ne­dor. Al activar WORKDIR, esta se aplica a todas las in­s­tru­c­cio­nes po­s­te­rio­res. Durante el proceso de co­n­s­tru­c­ción, se ven afectadas las in­s­tru­c­cio­nes RUN, COPY y ADD; durante la ejecución del co­n­te­ne­dor, las in­s­tru­c­cio­nes CMD y EN­TR­Y­POI­NT.

Consejo

La in­s­tru­c­ción WORKDIR se co­rre­s­po­n­de más o menos con el comando cd de la línea de comandos.

Si­mi­la­r­me­n­te al cambio de di­re­c­to­rio, la in­s­tru­c­ción USER permite cambiar el usuario actual (de Linux). Existe la opción de es­pe­ci­fi­car el grupo al que pertenece el usuario. La llamada a USER se aplica a todas las in­s­tru­c­cio­nes po­s­te­rio­res. Durante el proceso de co­n­s­tru­c­ción, las in­s­tru­c­cio­nes RUN se ven in­flue­n­cia­das por su pe­r­te­ne­n­cia al usuario y al grupo; durante el tiempo de ejecución del co­n­te­ne­dor, esto se aplica a las in­s­tru­c­cio­nes CMD y EN­TR­Y­POI­NT.

Consejo

La in­s­tru­c­ción USER co­rre­s­po­n­de apro­xi­ma­da­me­n­te al comando su de la línea de comandos.

In­s­tru­c­cio­nes COPY y ADD

Las in­s­tru­c­cio­nes COPY y ADD sirven ambas para añadir archivos y di­re­c­to­rios a la Docker Image. Ambas in­s­tru­c­cio­nes crean una nueva capa que se apila a la imagen existente. En la in­s­tru­c­ción COPY, la fuente siempre es el contexto de co­n­s­tru­c­ción. En el siguiente ejemplo, copiamos un archivo readme del su­b­di­re­c­to­rio “doc” del contexto build en el di­re­c­to­rio de nivel superior “app” de la imagen:

COPY ./doc/readme.md /app/
Consejo

La in­s­tru­c­ción COPY co­rre­s­po­n­de apro­xi­ma­da­me­n­te al comando cp de la línea de comandos.

La in­s­tru­c­ción ADD se comporta de forma casi idéntica, pero puede recuperar recursos URL fuera del contexto de co­n­s­tru­c­ción y de­s­co­m­pri­mir archivos co­m­pri­mi­dos. En la práctica, esto puede conllevar efectos se­cu­n­da­rios ine­s­pe­ra­dos, por lo tanto, se des­aco­n­se­ja to­ta­l­me­n­te el uso de la in­s­tru­c­ción ADD. En la mayoría de los casos debe uti­li­zar­se ex­clu­si­va­me­n­te la in­s­tru­c­ción COPY.

In­s­tru­c­ción RUN

La in­s­tru­c­ción RUN es una de las más comunes de Do­c­ke­r­fi­le. Con ella, indicamos a Docker que ejecute un comando de la línea de comandos durante el proceso de co­n­s­tru­c­ción. Los cambios re­su­l­ta­n­tes se apilan como una nueva capa sobre la imagen existente. Hay dos es­cri­tu­ras para la in­s­tru­c­ción RUN:

  1. Escritura Shell: los ar­gu­me­n­tos pasados a RUN se ejecutan en el Shell estándar de la Image. Los símbolos es­pe­cia­les y las variables de entorno se su­s­ti­tu­yen según las reglas de Shell. He aquí un ejemplo de una llamada que saluda al usuario actual y utilizar una subshell “$()”:
RUN echo "Hello $(whoami)"

2. Escritura “Exec”: en vez de pasar un comando a la Shell, se activa di­re­c­ta­me­n­te un archivo eje­cu­ta­ble. En el proceso, pueden pasarse ar­gu­me­n­tos adi­cio­na­les. Este es un ejemplo de una llamada a la he­rra­mie­n­ta de de­sa­rro­llo “npm” in­di­cá­n­do­le que ejecute el script “build”:

CMD ["npm", "run", " build"]
Nota

En principio, la in­s­tru­c­ción RUN puede sustituir a algunas de las otras in­s­tru­c­cio­nes de Docker. Por ejemplo, la llamada “RUN cd src” es más o menos equi­va­le­n­te a “WORKDIR src”. No obstante, este enfoque crea Do­c­ke­r­fi­les, que conforme van au­me­n­ta­n­do de tamaño se vuelven más difíciles de leer y gestionar, por lo que es mejor utilizar in­s­tru­c­cio­nes es­pe­cia­li­za­das si es posible.

In­s­tru­c­cio­nes CMD y EN­TR­Y­POI­NT

La in­s­tru­c­ción RUN ejecuta un comando durante el proceso build y crea una nueva capa en la Docker Image. En cambio, las in­s­tru­c­cio­nes CMD o EN­TR­Y­POI­NT ejecutan un comando cuando se inicia el co­n­te­ne­dor. La di­fe­re­n­cia entre ambas afi­r­ma­cio­nes es sutil:

  • EN­TR­Y­POI­NT se utiliza para crear un co­n­te­ne­dor que siempre realiza la misma acción cuando se inicia, por lo que el co­n­te­ne­dor se comporta como un archivo eje­cu­ta­ble.
  • CMD se utiliza para crear un co­n­te­ne­dor que ejecuta una acción definida cuando se inicia sin más pa­rá­me­tros. La acción pree­s­ta­ble­ci­da se puede so­bre­s­cri­bir fá­ci­l­me­n­te mediante pa­rá­me­tros adecuados.

Lo que ambas in­s­tru­c­cio­nes tienen en común es que solo pueden darse una vez por Do­c­ke­r­fi­le. Sin embargo, es posible combinar ambas in­s­tru­c­cio­nes. En este caso, EN­TR­Y­POI­NT define la acción estándar que se realizará al iniciar el co­n­te­ne­dor, mientras que CMD define pa­rá­me­tros fá­ci­l­me­n­te anulables para la acción.

Nuestra entrada en Do­c­ke­r­fi­le:

ENTRYPOINT ["echo", "Hello"]
CMD ["World"]

Los comandos co­rre­s­po­n­die­n­tes de la línea de comandos:

# Salida "Hello World"
docker run my_image
# Salida "Hello Moon"
docker run my_image Moon

In­s­tru­c­ción EXPOSE

Los co­n­te­ne­do­res Docker se comunican a través de la red. Los servicios que se ejecutan en el co­n­te­ne­dor se dirigen a través de los puertos es­pe­ci­fi­ca­dos. La in­s­tru­c­ción EXPOSE documenta la asi­g­na­ción de puertos y soporta los pro­to­co­los TCP y UDP. Si un co­n­te­ne­dor se inicia con “docker run -P”, el co­n­te­ne­dor escucha en los puertos definidos a través de EXPOSE. De manera al­te­r­na­ti­va, los puertos asignados pueden ser so­bre­s­cri­tos con “docker run -p”.

He aquí un ejemplo. Nuestro Do­c­ke­r­fi­le contiene la siguiente in­s­tru­c­ción EXPOSE:

EXPOSE 80/tcp
EXPOSE 80/udp

Luego, existen las si­guie­n­tes vías para hacer que los puertos se activen al iniciar el co­n­te­ne­dor:

# Container escucha para TCP / UDP Traffic en Port 80
docker run -P
# Container escucha para TCP Traffic en Port 81
docker run -p 81:81/tcp

In­s­tru­c­ción VOLUME

Un Do­c­ke­r­fi­le define una imagen Docker que consiste en capas apiladas las unas sobre las otras. Las capas son solo de lectura para que se garantice siempre el mismo estado cuando se inicie un co­n­te­ne­dor. Ne­ce­si­ta­mos un mecanismo para in­te­r­ca­m­biar datos entre el co­n­te­ne­dor en ejecución y el sistema anfitrión. La in­s­tru­c­ción VOLUME define un “mount point” dentro del co­n­te­ne­dor.

Co­n­si­de­re­mos el siguiente fragmento de un archivo Docker. Creamos un di­re­c­to­rio “shared” en el di­re­c­to­rio de nivel superior de la imagen. También es­pe­ci­fi­ca­mos que este di­re­c­to­rio se incluya en el sistema anfitrión cuando se inicie el co­n­te­ne­dor:

RUN mkdir /shared
VOLUME /shared

Ten en cuenta que dentro del Do­c­ke­r­fi­le no podemos definir la ruta real en el host system. De manera estándar, los di­re­c­to­rios definidos mediante la in­s­tru­c­ción VOLUME se incluyen en el sistema anfitrión bajo "/var/lib/docker/volumes/".

¿Cómo se modifica un Do­c­ke­r­fi­le?

Recuerda: un Do­c­ke­r­fi­le es un archivo de texto (plano). Se puede editar con los métodos típicos; un editor de texto plano es pro­ba­ble­me­n­te el más común. Puede ser un editor con una interfaz gráfica de usuario. ¡Será por opciones! Los editores más populares son VSCode, Sublime Text, Atom y Notepad++. Como al­te­r­na­ti­va, hay varios editores di­s­po­ni­bles en la línea de comandos. Además de los ori­gi­na­les Vim o Vi, los editores mínimos Pico y Nano también se usan mucho.

Nota

Edita archivos de texto plano úni­ca­me­n­te con editores adecuados. No uses bajo ningún concepto pro­ce­sa­do­res de textos como Microsoft Word, Apple Pages o Libre u Ope­nO­f­fi­ce para editar un Do­c­ke­r­fi­le.

Ir al menú principal