Docker III: A Dockerfile
Agora sim, vamos ler a nossa Dockerfile pouco a pouco e entender o que houve. A mentalidade que é preciso ter lendo esse arquivo é a de que o Dockerfile é a interface entre seu computador real e a imagem sendo construída, portanto é com ela que você irá mover arquivos de sua máquina para dentro da imagem.
Relembrando, a Dockerfile completa pode ser encontrada na página Docker I. A maior parte dela está comentada para facil entendimento.
# Herda da imagem do debian
FROM debian
Falamos sobre herança de imagens na página anterior, é exatamente o que
fazemos aqui. Todos os comandos a seguir levam em conta que estamos
no ambiente provido pela imagem debian.
Note que essa imagem pode sempre mudar: Caso alguém atualize a imagem
debian, os nossos builds seguintes irão utilizar a imagem nova,
mantendo a nossa imagem sempre atualizada também. Quando essa não é a
intenção, e controle de versão é mais importante, é possível específicar
uma versão da imagem, por exemplo debian:jessie ou debian:buster.
A parte depois do : é a versão específica da imagem.
# Instala o npm
RUN apt update
RUN apt install -y npm
# Atualiza o npm
RUN npm install -g npm 
O comando RUN roda o comando na imagem. Por exemplo, RUN ls rodaria
o comando ls, simples o suficiente.
Mas como assim, imagens podem rodar comandos?
O que o Docker faz, na verdade, é criar um container intermediário com a
imagem anterior, executar o comando, e em sucesso, criar uma imagem
intermediária nova para os comandos seguintes.
# Cria o diretório /app
# e instala as dependências do /app lá
# note que WORKDIR tanto cria o diretório
# quanto troca para ele (cd /app)
WORKDIR /app
o comando WORKDIR foo é muito parecido com RUN mkdir -p foo && cd foo,
exceto que RUN cd foo não é levado para os comandos seguintes, pois
é parecido com criar um terminal novo, executar cd nele e logo após
isso fechá-lo. Isso não muda o diretório no terminal anterior!
Logo, é necessário usar esse comando para dizer que todos os próximos
comandos são executados dentro do diretório /app.
COPY package.json .
COPY package-lock.json .
Primeiramente, o comando COPY tem a seguinte sintaxe: COPY <arquivo(s) da máquina física> <destino na imagem>
Logo, estamos copiando o package.json e package-lock.json do nosso projeto
para dentro da pasta /app.
Segundo, por que não estamos movendo a pasta inteira de uma vez? Afinal, tudo vai estar lá alguma hora ou outra.
A razão para essa escolha tem a ver com o cacheamento do Docker (o que
vou falar mais a fundo em uma próxima página). Essencialmente, o Docker
é esperto, e o docker build só irá realizar o trabalho necessário.
Se esse trabalho já foi feito antes, o Docker só pega da memória o que
ele fez anteriormente.
Um exemplo disso é a linha que contém FROM debian. O Docker não irá
baixar a imagem do Debian toda vez que for reconstruir sua imagem!
Se nós tivessemos colocado para mover a pasta inteira de uma vez, toda
vez que houvesse uma alteração no index.js, essa operação teria que ser
repetida, já que não há garantias que esses dois arquivos permaneceram
os mesmos.
Isso é particularmente ruim pois a próxima linha é essa:
RUN npm install
que instala arquivos da internet e potencialmente é um comando que pode
demorar bastante tempo. Para evitar isso, copiamos o index.js depois
de instalarmos as dependências do projeto, para que uma alteração no
projeto não force todas as dependências dele a serem reinstaladas toda
vez.
# copia o nosso app para dentro da imagem
COPY index.js .
# Esse é o comando que será rodado quando você executar o `docker run`
CMD ["npm", "start"]
Por fim, temos o comando CMD. Esse comando especifica os argumentos
que serão passados para o comando especificado por ENTRYPOINT, que por
sua vez é o comando rodado pelo docker run. Por padrão, o ENTRYPOINT
é /bin/sh -c.
Logo, quando executamos docker run meu-app, Um container
com a imagem meu-app é criado, e esse container executa /bin/sh -c npm start. Para entender melhor a diferença entre ENTRYPOINT e CMD, veja
essa resposta no stack overflow.
Ahá! Desconstruímos a nossa Dockerfile. Para saber ainda mais sobre builds, contextos de builds, e a Dockerfile, consulte a própria documentação do Docker. Ela é fantástica e explica muito bem o que acontece.