Docker · 45 min

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

Pré-requisitos

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:

  1. 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.
  2. RUN roda em tempo de build; CMD roda em tempo de início. RUN pip install ...
    acontece uma vez, ao construir a imagem. CMD ["python", "app.py"] é o que dispara quando
    você faz docker run na imagem pronta.

Tempo de build vs. tempo de execução

Mantenha esses dois momentos separados na cabeça:

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

  1. 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-lab onde preferir, e entre nela:

    mkdir docker-lab
    cd docker-lab
    

    Adicione o app

    Crie app.py com 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 a 127.0.0.1 dentro do container só
    responderia dentro dele, e o mapeamento de porta -p do Passo 4 não alcançaria nada.
    Amarrar em 0.0.0.0 faz ele escutar em todas as interfaces para o encaminhamento de porta do
    Docker conseguir atingi-lo.

    Declare a dependência

    Crie requirements.txt listando exatamente o que o app precisa:

    flask==3.0.3
    

    Fixar 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 como COPY puxarem 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 .dockerignore no Passo 5.

    Checkpoint

    • O que significa o . final no docker build? (O contexto de build — a pasta que o Docker envia ao engine, e a fonte do COPY.)
    • Por que amarrar o app em 0.0.0.0 em vez de 127.0.0.1? (Para ele ser alcançável pela porta publicada do Docker, de fora do container.)

    Entregável deste passo: a pasta docker-lab contendo app.py e requirements.txt.

  2. 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 exatamente Dockerfile (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.12 já vem com o Python instalado; a
      variante -slim remove 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 /app dentro da imagem. Todo COPY, RUN e CMD
      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-dir diz 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 (seu app.py) para /app.
    • EXPOSE 5000 — documentação: registra que o app escuta na 5000. Ele não publica a
      porta sozinho — você ainda passa -p no docker 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 a CMD python app.py porque 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

    RUN executa enquanto a imagem é construída e seu resultado vira uma camada permanente.
    CMD executa quando o container inicia e pode ser sobrescrito no docker 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 Dockerfile salvo em docker-lab.

  3. 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 como minha-app na versão 1.0. O
      formato é nome:tag. Sem -t a 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
      COPY contra ela. O Dockerfile é encontrado aqui por padrão (use -f para 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-slim no primeiro build (cacheado nos seguintes).
    • Cada COPY / RUN produz uma camada.
    • O passo RUN pip install é o lento — ele baixa e instala o Flask.
    • No fim você recebe naming to ... minha-app:1.0 e 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 images
    

    Você 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 -t fez, e o que aconteceria sem ele? (Nomeou+tagueou a imagem como minha-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 de docker images para minha-app:1.0.

  4. Rode sua imagem e alcance-a no navegador

    Você construiu uma imagem; agora instancie-a como um container — o mesmo docker run que 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.0
    

    As 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, o EXPOSE no 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 ps
    

    Você verá minha-app-c, sua imagem minha-app:1.0, status Up ... 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-c
    

    Você 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:5000 no docker run? (EXPOSE só documenta a porta; -p é o que de fato a publica para o host.)

    Entregável deste passo: a saída de docker ps e a confirmação (uma nota ou screenshot) de que http://localhost:5000 retornou sua mensagem.

  5. 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 .dockerignore mantém arquivos de fora — aqui, de fora do
    contexto de build enviado ao engine e de fora do seu COPY . .. Crie .dockerignore em
    docker-lab:

    __pycache__/
    *.pyc
    .git/
    .gitignore
    .venv/
    venv/
    *.md
    Dockerfile
    .dockerignore
    

    Por 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.txt e rodou pip install antes 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 install reporta CACHED e é instantâneo. Como o
    requirements.txt não mudou, o Docker reaproveitou aquela camada; só as camadas de COPY . .
    em diante foram reconstruídas. Se você tivesse escrito COPY . . antes do pip 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 linhas RUN. Cada RUN é 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 RUN posterior
    não encolheria a imagem, porque a camada anterior já o gravou. O --no-cache-dir que você deu
    ao pip é o equivalente dessa limpeza no mundo Python.

    Imagem base leve

    Você já escolheu python:3.12-slim em vez do python:3.12 completo. 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 images
    

    Checkpoint

    • Por que o pip install apareceu CACHED no rebuild 1.1? (O requirements.txt não mudou, então o Docker reaproveitou aquela camada; só as camadas após o COPY do código foram reconstruídas.)
    • Por que limpar o cache de pacotes no mesmo RUN da instalação, e não num posterior? (Um RUN posterior é uma nova camada; a camada anterior já contém o cache, então a imagem não encolheria.)

    Entregável deste passo: o arquivo .dockerignore e a saída de docker build -t minha-app:1.1 . mostrando o passo pip install como CACHED.

  6. 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-c
    

    As imagens (minha-app:1.0 e minha-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:latest cria
    outra tag para a mesma imagem, e docker 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.
    Inclua Dockerfile, app.py, requirements.txt e .dockerignore — quem for avaliar deve
    conseguir rodar docker build sozinho. 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 URL
    

    Entregue 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: FROM numa base slim/alpine, WORKDIR, COPY requirements.txt antes do COPY . ., um RUN pip install, EXPOSE e um CMD na forma exec
    • A saída de docker build mostra um build bem-sucedido tagueado minha-app:1.0 (não só digitado — colado do seu terminal)
    • docker images mostra sua imagem minha-app com um tamanho real
    • Você confirmou que http://localhost:5000 retornou a sua mensagem, não a página padrão do nginx
    • Um .dockerignore existe e exclui ao menos .git/, __pycache__/ e pastas de virtualenv
    • A saída do rebuild prova que o cache funcionou: o passo pip install mostra 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 no SUBMISSION.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.yml e
    orquestrá-los com um único docker compose up, para que apps multi-container subam tão fácil
    quanto este subiu.