Construa sua própria imagem com um Dockerfile
Saia de consumir imagens para produzi-las. Escreva um Dockerfile para um app web mínimo, construa-o numa imagem com tag, rode-o como um container no navegador e otimize com um .dockerignore e uma ordem de instruções amiga do cache de camadas.
Problema
No Lab 1 você rodou containers a partir de imagens que outras pessoas construíram. Isso é metade do Docker. A outra metade — a que faz o Docker valer a pena aprender — é empacotar a **sua própria** aplicação numa imagem para que ela rode identicamente no seu laptop, na máquina de um colega, na CI e em produção. A ferramenta para isso é o **Dockerfile**: uma receita em texto puro de instruções que o Docker lê de cima para baixo para montar uma imagem, camada por camada. Acerte e "funciona na minha máquina" deixa de ser uma frase que alguém no seu time diz. Erre — uma imagem base inchada, um cache que você nunca reaproveita, segredos copiados por acidente — e seus builds ficam lentos, gigantes e inseguros. Neste lab você escreve um Dockerfile real para um app web Flask mínimo, o constrói numa imagem com tag usando `docker build`, o roda e o abre no navegador, e então aplica as boas práticas do ebook — imagens base leves, um `.dockerignore`, camadas `RUN` agrupadas e ordenação amiga do cache — e *vê o build ficar mais rápido*. Você termina capaz de containerizar qualquer app que escrever.
Objetivos
- Explicar o que é um Dockerfile e como cada instrução adiciona uma camada à imagem resultante
- Ler e escrever as instruções centrais:
FROM,WORKDIR,COPY,RUN,EXPOSE,CMD - Construir uma imagem a partir de um Dockerfile com
docker build -t nome:tag ., entendendo a flag-te o contexto de build. - Rodar sua própria imagem como um container, publicar sua porta e alcançar o app num navegador
- Otimizar o build com um
.dockerignore, uma imagem base leve, camadasRUNagrupadas com limpeza de cache e ordenação de instruções amiga do cache - Gerenciar as imagens que você produz com
docker images,docker tagedocker rmi
Pré-requisitos
- Docker instalado e rodando (Docker Desktop no Windows/macOS, ou Docker Engine no Linux). Rode
docker version— se imprimir uma seção Client e uma Server, você está pronto. - Ter concluído o Lab 1 ("Rode seus primeiros containers Docker"), ou conforto equivalente com
docker run,docker ps,docker imagese o modelo mental imagem-vs-container - Um terminal (PowerShell, bash ou zsh), um editor de texto e um navegador
- Acesso à internet para baixar a imagem base do Python no primeiro build
- Git instalado, para entregar seu deliverable no fim
- Nenhuma experiência com Python é exigida — o app é um exemplo Flask de ~10 linhas que você copia como está
O que você vai construir
Uma pasta chamada docker-lab contendo um app web Flask mínimo (app.py + requirements.txt)
e um Dockerfile que o empacota. Você vai construir esse Dockerfile numa imagem chamada
minha-app:1.0, rodá-la como um container e abrir o app em http://localhost:5000. Depois vai
deixar a imagem menor e o build mais rápido adicionando um .dockerignore e reordenando as
instruções. No fim você domina todo o lado produtor do Docker: seu código → sua imagem → seu
container.
O modelo mental: um Dockerfile é uma receita, camadas são os passos
Um Dockerfile é um arquivo de texto puro com uma lista de instruções. O Docker o lê de cima
para baixo e, para a maioria das instruções, adiciona uma camada (layer) à imagem — um diff
de sistema de arquivos empilhado e cacheado.
| Instrução | O que faz | Exemplo |
|---|---|---|
FROM |
A imagem base sobre a qual você constrói | FROM python:3.12-slim |
WORKDIR |
Define o diretório de trabalho para as instruções seguintes | WORKDIR /app |
COPY |
Copia arquivos do contexto de build para dentro da imagem | COPY . /app |
RUN |
Executa um comando em tempo de build, gravando o resultado numa camada | RUN pip install -r requirements.txt |
EXPOSE |
Documenta em qual porta o container escuta | EXPOSE 5000 |
CMD |
O comando padrão executado quando o container inicia | CMD ["python", "app.py"] |
Duas coisas valem ser internalizadas agora, porque toda otimização mais adiante decorre delas:
-
Cada camada é cacheada. Num rebuild, o Docker reaproveita uma camada inalterada da vez
anterior — a menos que aquela camada, ou qualquer camada antes dela, tenha mudado. É por
isso que a ordem das instruções importa. -
RUNroda em tempo de build;CMDroda em tempo de início.RUN pip install ...
acontece uma vez, ao construir a imagem.CMD ["python", "app.py"]é o que dispara quando
você fazdocker runna imagem pronta.
Tempo de build vs. tempo de execução
Mantenha esses dois momentos separados na cabeça:
-
docker buildlê o Dockerfile e produz uma imagem (executa todoFROM/COPY/RUN). -
docker runpega essa imagem e inicia um container (executa oCMD).
Você já conhece o docker run do Lab 1. Este lab é principalmente sobre o primeiro momento —
docker build — que é a parte que você não controlava antes.
Como trabalhar neste lab
Faça os passos em ordem — cada um constrói o artefato que o próximo precisa. Rode cada comando
você mesmo e leia a saída real; o objetivo do Passo 5 é ver o cache deixar um segundo build
mais rápido, o que só faz sentido se você de fato rodou o primeiro. Você vai coletar as saídas
para a entrega do Passo 6 ao longo do caminho.
Passos
-
Prepare o projeto: o app e o contexto de build
Antes de escrever um Dockerfile você precisa de algo para containerizar. Você vai criar um app
web Flask mínimo — a menor coisa que responde numa porta para você abrir no navegador.Crie a pasta do projeto
Crie uma pasta chamada
docker-labonde preferir, e entre nela:mkdir docker-lab cd docker-labAdicione o app
Crie
app.pycom um servidor Flask minúsculo que devolve uma linha:# app.py from flask import Flask app = Flask(__name__) @app.route("/") def home(): return "Hello from my own Docker image!" if __name__ == "__main__": # 0.0.0.0 para o app ser alcançável de fora do container, não só do localhost interno app.run(host="0.0.0.0", port=5000)O
host="0.0.0.0"importa: um servidor amarrado a127.0.0.1dentro do container só
responderia dentro dele, e o mapeamento de porta-pdo Passo 4 não alcançaria nada.
Amarrar em0.0.0.0faz ele escutar em todas as interfaces para o encaminhamento de porta do
Docker conseguir atingi-lo.Declare a dependência
Crie
requirements.txtlistando exatamente o que o app precisa:flask==3.0.3Fixar a versão (não só
flask) é o mesmo hábito de reprodutibilidade que você aprendeu para
tags de imagem no Lab 1 — um rebuild daqui a meses instala o mesmo Flask, não o que for mais
novo.Entenda o contexto de build
Quando você depois rodar
docker build ... ., aquele.final é o contexto de build: o
diretório que o Docker empacota e envia ao engine para instruções comoCOPYpuxarem arquivos
dele. Agora seu contexto são esses dois arquivos. Nada que esteja fora do contexto pode ser
COPY'ado para a imagem — e, tão importante quanto, tudo que está dentro do contexto é
enviado ao daemon, e é por isso que você vai enxugá-lo com um.dockerignoreno Passo 5.Checkpoint
- O que significa o
.final nodocker build? (O contexto de build — a pasta que o Docker envia ao engine, e a fonte doCOPY.) - Por que amarrar o app em
0.0.0.0em vez de127.0.0.1? (Para ele ser alcançável pela porta publicada do Docker, de fora do container.)
Entregável deste passo: a pasta
docker-labcontendoapp.pyerequirements.txt. - O que significa o
-
Escreva o Dockerfile, instrução por instrução
Agora o coração do lab: a receita que transforma seus dois arquivos numa imagem.
Crie o Dockerfile
Na mesma pasta
docker-lab, crie um arquivo chamado exatamenteDockerfile(sem extensão)
com este conteúdo:# 1. Imagem base: um Python pequeno e oficial FROM python:3.12-slim # 2. Diretório de trabalho dentro da imagem WORKDIR /app # 3. Copie a lista de dependências PRIMEIRO (o porquê está no Passo 5) COPY requirements.txt . # 4. Instale as dependências em tempo de build RUN pip install --no-cache-dir -r requirements.txt # 5. Agora copie o resto do app COPY . . # 6. Documente a porta em que o app escuta EXPOSE 5000 # 7. Comando padrão quando o container inicia CMD ["python", "app.py"]O que cada instrução faz
-
FROM python:3.12-slim— a imagem base.python:3.12já vem com o Python instalado; a
variante-slimremove a documentação e os extras que um Debian completo carrega, então é
bem menor. Escolher uma base leve, oficial e fixada é a decisão número um de todo
Dockerfile. -
WORKDIR /app— cria e entra em/appdentro da imagem. TodoCOPY,RUNeCMD
seguinte roda relativo a aqui, então você não repete caminhos absolutos. -
COPY requirements.txt .— copia só o arquivo de requisitos para/app. Copiá-lo
antes do código-fonte é um truque de cache deliberado que você vai explorar no Passo 5. -
RUN pip install --no-cache-dir -r requirements.txt— roda em tempo de build e grava os
pacotes instalados numa camada.--no-cache-dirdiz ao pip para não guardar seu cache de
download, que só incharia a imagem (a prática "limpe após instalações" do ebook). -
COPY . .— copia o resto do contexto de build (seuapp.py) para/app. -
EXPOSE 5000— documentação: registra que o app escuta na 5000. Ele não publica a
porta sozinho — você ainda passa-pnodocker run(Passo 4). -
CMD ["python", "app.py"]— o processo que inicia quando o container roda. A "forma
exec" em JSON (uma lista de strings) é preferida aCMD python app.pyporque executa o
processo diretamente, sem embrulhá-lo num shell, então sinais como Ctrl+C chegam limpos até
ele.
RUN vs CMD — a confusão clássica
RUNexecuta enquanto a imagem é construída e seu resultado vira uma camada permanente.
CMDexecuta quando o container inicia e pode ser sobrescrito nodocker run.pip installé um passo de build (RUN); subir seu servidor é um passo de execução (CMD).
Misturar os dois é o erro de iniciante mais comum em Dockerfile.Entregável deste passo: o
Dockerfilesalvo emdocker-lab. -
-
Construa a imagem com docker build
Receita escrita — agora cozinhe-a numa imagem.
Rode o build
De dentro de
docker-lab(a pasta com o Dockerfile), rode:docker build -t minha-app:1.0 .Decodifique o comando:
-
docker build→ leia um Dockerfile e produza uma imagem. -
-t minha-app:1.0→ tagueia a imagem resultante comominha-appna versão1.0. O
formato énome:tag. Sem-ta imagem ganha só um ID aleatório e é chata de referenciar. -
.→ o contexto de build: esta pasta. O Docker a envia ao engine e resolve o
COPYcontra ela. O Dockerfile é encontrado aqui por padrão (use-fpara apontar outro).
Leia a saída: camadas e passos
Observe o log do build. O Docker executa suas instruções em ordem e reporta cada uma como um
passo:- Ele baixa
python:3.12-slimno primeiro build (cacheado nos seguintes). - Cada
COPY/RUNproduz uma camada. - O passo
RUN pip installé o lento — ele baixa e instala o Flask. - No fim você recebe
naming to ... minha-app:1.0e uma linha de sucesso.
Toda instrução que altera o sistema de arquivos adiciona uma camada, e cada camada é
armazenada e cacheada independentemente. Guarde essa imagem mental — é exatamente o que faz a
otimização do Passo 5 compensar.Confirme que a imagem existe
docker imagesVocê verá uma linha onde REPOSITORY é
minha-app, TAG é1.0, mais um ID de imagem,
um horário de criação e um tamanho. Essa linha é a sua imagem — construída a partir do seu
Dockerfile, não baixada de ninguém. Anote o tamanho; você vai comparar com ele depois de
otimizar.Checkpoint
- O que o
-tfez, e o que aconteceria sem ele? (Nomeou+tagueou a imagem comominha-app:1.0; sem ele você teria só um ID aleatório difícil de referenciar.) - Qual passo do build foi o mais lento, e por quê? (
RUN pip install— ele baixa e instala a dependência.)
Entregável deste passo: o final da saída de
docker build(a linha de sucesso) e a linha dedocker imagesparaminha-app:1.0. -
-
Rode sua imagem e alcance-a no navegador
Você construiu uma imagem; agora instancie-a como um container — o mesmo
docker runque você
conhece do Lab 1, mas desta vez a imagem é sua.Inicie o container
docker run -d -p 5000:5000 --name minha-app-c minha-app:1.0As flags, exatamente como no Lab 1:
-
-d→ detached, roda em segundo plano e devolve seu prompt. -
-p 5000:5000→ mapeia a porta 5000 do host para a porta 5000 do container
(HOST:CONTAINER). Seu app escuta na 5000 por dentro; você o alcança na 5000 por fora. Esta
é a linha que de fato publica a porta — lembre, oEXPOSEno Dockerfile só a documentou. -
--name minha-app-c→ um nome amigável para o container. -
minha-app:1.0→ a sua imagem, a que você acabou de construir.
Abra
Vá para http://localhost:5000. Você deve ver:
Hello from my own Docker image!Essa string veio do seu
app.py, rodando dentro de um container construído a partir do
seu Dockerfile. É exatamente aqui que o lab aterrissa.Verifique pela CLI
docker psVocê verá
minha-app-c, sua imagemminha-app:1.0, statusUp ...e o mapeamento
0.0.0.0:5000->5000/tcp. Cheque os logs para ver o startup do Flask e as linhas de
requisição:docker logs minha-app-cVocê deve ver o Flask reportando que está rodando em
http://0.0.0.0:5000, mais uma linha de
log de requisição cada vez que você atualiza o navegador.Checkpoint
- O Dockerfile tinha
EXPOSE 5000. Por que você ainda precisou de-p 5000:5000nodocker run? (EXPOSEsó documenta a porta;-pé o que de fato a publica para o host.)
Entregável deste passo: a saída de
docker pse a confirmação (uma nota ou screenshot) de quehttp://localhost:5000retornou sua mensagem. -
-
Otimize: .dockerignore, base leve, camadas e ordem do cache
Sua imagem funciona, mas uma imagem que funciona não é o mesmo que uma boa imagem. Aplique as
boas práticas do ebook e meça a diferença.Adicione um .dockerignore
Assim como o
.gitignore, um.dockerignoremantém arquivos de fora — aqui, de fora do
contexto de build enviado ao engine e de fora do seuCOPY . .. Crie.dockerignoreem
docker-lab:__pycache__/ *.pyc .git/ .gitignore .venv/ venv/ *.md Dockerfile .dockerignorePor que importa: sem ele, um
COPY . .pode arrastar seu histórico.git/, um virtualenv
local, lixo de editor, até segredos — inchando a imagem e arriscando vazamentos. Excluí-los
deixa o contexto menor (builds mais rápidos) e a imagem mais limpa e segura.A ordem das instruções já está fazendo o trabalho de verdade
Volte ao seu Dockerfile. Você copiou
requirements.txte rodoupip installantes do
COPY . .. Essa ordenação é o maior ganho isolado de velocidade de build, e aqui está a
prova.Mude só seu app, não suas dependências. Edite a mensagem do
app.py:return "Hello from my OPTIMIZED Docker image!"Reconstrua:
docker build -t minha-app:1.1 .Observe de perto: o passo
pip installreporta CACHED e é instantâneo. Como o
requirements.txtnão mudou, o Docker reaproveitou aquela camada; só as camadas deCOPY . .
em diante foram reconstruídas. Se você tivesse escritoCOPY . .antes dopip install,
qualquer mudança de código invalidaria a camada de instalação e reinstalaria o Flask toda
santa vez. Esta é a prática de ordem-do-cache do ebook, tornada concreta.Uma camada RUN em vez de várias
A prática "minimize camadas" do ebook: agrupe trabalho de shell relacionado num único
RUN
com&&em vez de empilhar várias linhasRUN. CadaRUNé sua própria camada, então menos
RUNs agrupados significam menos camadas e uma imagem menor. Ilustração (você não precisa de
pacotes extras para este app, então este é um padrão para reconhecer, não para colar):# Muitas camadas, cache deixado para trás — evite: RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # Uma camada, limpa no mesmo passo — prefira: RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*Agrupar também permite limpar na mesma camada — apagar o cache do apt num
RUNposterior
não encolheria a imagem, porque a camada anterior já o gravou. O--no-cache-dirque você deu
ao pip é o equivalente dessa limpeza no mundo Python.Imagem base leve
Você já escolheu
python:3.12-slimem vez dopython:3.12completo. Essa é a prática
"prefira imagens base leves (alpine/slim)" do ebook: bases slim/alpine significam imagens
menores, pulls mais rápidos e uma superfície de ataque menor. Confirme suas imagens e compare
os tamanhos:docker imagesCheckpoint
- Por que o
pip installapareceu CACHED no rebuild1.1? (Orequirements.txtnão mudou, então o Docker reaproveitou aquela camada; só as camadas após oCOPYdo código foram reconstruídas.) - Por que limpar o cache de pacotes no mesmo
RUNda instalação, e não num posterior? (UmRUNposterior é uma nova camada; a camada anterior já contém o cache, então a imagem não encolheria.)
Entregável deste passo: o arquivo
.dockerignoree a saída dedocker build -t minha-app:1.1 .mostrando o passopip installcomo CACHED. - Por que o
-
Entregue: sua imagem, documentada
Prove que você construiu e rodou sua própria imagem capturando a evidência real num
repositório.Limpe (opcional, mas organizado)
Antes de entregar, pare o container em execução para nada ficar segurando a porta 5000:
docker stop minha-app-c docker rm minha-app-cAs imagens (
minha-app:1.0eminha-app:1.1) ficam — elas são seu deliverable. Pratique os
comandos de gerenciamento do ebook se quiser:docker tag minha-app:1.1 minha-app:latestcria
outra tag para a mesma imagem, edocker rmi <nome:tag>remove uma que você não quer mais.Monte sua entrega
Crie um repositório contendo seus arquivos reais do projeto mais um
SUBMISSION.md.
IncluaDockerfile,app.py,requirements.txte.dockerignore— quem for avaliar deve
conseguir rodardocker buildsozinho. Cole suas saídas reais, não texto inventado.# Docker Lab 2 — Construa sua própria imagem com um Dockerfile ## 1. Meu Dockerfile <cole o conteúdo do seu Dockerfile> ## 2. Saída do docker build <cole o final de `docker build -t minha-app:1.0 .` mostrando a linha de sucesso> ## 3. docker images (minha imagem presente) <cole a(s) linha(s) de `docker images` para minha-app, mostrando tag e tamanho> ## 4. App rodando no navegador Rodei `docker run -d -p 5000:5000 --name minha-app-c minha-app:1.0` e abri http://localhost:5000. Ele retornou: "Hello from my own Docker image!" (sim / não) <opcionalmente cole um screenshot ou a linha do `docker ps`> ## 5. Prova do cache (otimização) Depois de mudar só o app.py e reconstruir como minha-app:1.1, o passo `pip install` mostrou: <cole a linha CACHED do segundo build> ## 6. Reflexão (2-3 frases) Nas minhas palavras: a diferença entre RUN e CMD, e por que copiar o requirements.txt antes do resto do código deixa os rebuilds mais rápidos.Entregue
cd docker-lab git init git add Dockerfile app.py requirements.txt .dockerignore SUBMISSION.md git commit -m "Docker lab 2 — construa sua própria imagem" # faça push para um repo público ou crie um gist, depois entregue essa URLEntregue a URL do repositório ou gist como seu deliverable do lab.
Critério de submissão (autoverificação, anti-stub)
- Seu
Dockerfileé real e completo:FROMnuma base slim/alpine,WORKDIR,COPY requirements.txtantes doCOPY . ., umRUN pip install,EXPOSEe umCMDna forma exec - A saída de
docker buildmostra um build bem-sucedido tagueadominha-app:1.0(não só digitado — colado do seu terminal) -
docker imagesmostra sua imagemminha-appcom um tamanho real - Você confirmou que
http://localhost:5000retornou a sua mensagem, não a página padrão do nginx - Um
.dockerignoreexiste e exclui ao menos.git/,__pycache__/e pastas de virtualenv - A saída do rebuild prova que o cache funcionou: o passo
pip installmostra CACHED após uma mudança só de código - Sua reflexão diz corretamente, nas suas palavras, RUN vs CMD e por que a ordenação requirements-primeiro acelera os rebuilds
- Nenhum texto de placeholder (
<cole ...>,TODO, "lorem ipsum") ficou noSUBMISSION.md
O que vem a seguir
Você já sabe transformar um único app numa única imagem e rodá-la como container. Sistemas
reais raramente são um processo só, porém — um app web geralmente precisa de um banco de dados,
talvez um cache, talvez um worker em segundo plano, todos conectados. No próximo lab de Docker
você vai aprender Docker Compose: descrever vários serviços num sódocker-compose.ymle
orquestrá-los com um únicodocker compose up, para que apps multi-container subam tão fácil
quanto este subiu. - Seu