Pular para o conteúdo
Categoria: Glossário do Programador18 min de leitura

O que é Docker? Containers explicados de forma simples

Por Schematize Blog ·

Descubra o que são containers, como o Docker funciona por baixo dos panos (namespaces, cgroups e camadas) e por que ele acabou de vez com o clássico problema do na minha maquina funciona.

"Na minha máquina funciona" foi por anos a frase mais frustrante do desenvolvimento de software. O Docker surgiu para enterrar essa desculpa de vez, empacotando aplicações junto com tudo de que precisam para rodar. Neste artigo você vai entender o que são containers, como o Docker funciona por baixo dos panos e por que ele revolucionou a forma como entregamos software — passando por namespaces, cgroups, volumes, redes, Docker Compose, segurança e troubleshooting.

O problema que o Docker resolve

Um programa raramente roda sozinho. Ele depende de uma versão específica da linguagem, de bibliotecas, de variáveis de ambiente e de configurações do sistema operacional. Quando esse conjunto difere entre a máquina do desenvolvedor e o servidor de produção, surgem bugs misteriosos — aqueles que só aparecem "lá" e nunca "aqui".

As soluções antigas eram trabalhosas:

    O Docker propôs um meio-termo: empacotar a aplicação e suas dependências em uma unidade leve e portátil chamada container (Merkel, 2014). Em vez de virtualizar o hardware e carregar um sistema operacional inteiro por aplicação, ele isola processos dentro do mesmo kernel do sistema host. O resultado é dramático: containers sobem em segundos, pesam megabytes em vez de gigabytes, e você consegue rodar dezenas deles na mesma máquina sem o desperdício de múltiplos sistemas operacionais.

    Além do desperdício de recursos, as VMs não resolviam bem o problema da consistência de ambiente. Você ainda precisava configurar manualmente cada VM. O Docker transforma essa configuração em um arquivo versionável e reproduzível, eliminando a divergência entre desenvolvimento, teste e produção.

    O que é um container

    Um container é um pacote isolado que contém sua aplicação e tudo de que ela precisa para funcionar: código, bibliotecas, dependências e configurações. Ele roda de forma isolada do resto do sistema, mas compartilha o núcleo (kernel) do sistema operacional da máquina hospedeira.

    É essa partilha do kernel que torna o container tão eficiente. A diferença para uma VM é fundamental:

      Na prática, você pode rodar dezenas de containers na mesma máquina onde caberiam apenas algumas VMs. Por compartilhar o kernel, um container Linux precisa rodar sobre um kernel Linux. É por isso que, no Windows e no macOS, o Docker Desktop roda uma pequena máquina virtual Linux nos bastidores — os containers Linux vivem dentro dela.

      Como os containers funcionam por baixo dos panos

      Containers não são mágica nem uma tecnologia totalmente nova: eles são construídos sobre recursos que já existem no kernel do Linux há anos. Entender esses dois pilares ajuda a desmistificar o Docker.

      Namespaces: o isolamento

      Os namespaces são o recurso do kernel responsável por dar a cada container a ilusão de que ele tem o sistema só para si. Cada tipo de namespace isola uma faceta diferente:

        Cgroups: o controle de recursos

        Enquanto os namespaces dizem o que um container pode ver, os control groups (cgroups) controlam quanto ele pode consumir. É por meio dos cgroups que você limita um container a, digamos, no máximo 512 MB de memória ou meio núcleo de CPU. Isso evita que um único container descontrolado derrube toda a máquina ao engolir todos os recursos.

        A combinação de namespaces (isolamento) e cgroups (limites) é o que torna possível rodar muitos containers lado a lado de forma segura e previsível. O Docker é, em boa parte, uma camada amigável de ferramentas e formatos sobre esses mecanismos do kernel.

        Imagens e containers: a diferença

        Dois conceitos costumam confundir iniciantes: imagem e container.

          De uma mesma imagem você pode criar vários containers idênticos, da mesma forma que várias casas seguem a mesma planta. Outra analogia útil é a da culinária: a imagem é a receita, e o container é o prato pronto que você cozinhou a partir dela.

          Camadas: o segredo da eficiência

          As imagens são montadas em camadas (layers) empilhadas: cada instrução de construção adiciona uma camada, e camadas iguais são reaproveitadas entre imagens, economizando espaço e tempo. Essas camadas são imutáveis: se duas imagens partem da mesma base, elas compartilham as camadas comuns no disco, sem duplicação.

          Esse modelo de camadas também alimenta o cache de build. Quando você reconstrói uma imagem, o Docker reaproveita as camadas que não mudaram e só refaz a partir do ponto em que houve alteração. É por isso que a ordem das instruções no Dockerfile importa tanto: colocar primeiro o que muda menos (a instalação de dependências) e depois o que muda mais (o seu código) torna os rebuilds muito mais rápidos.

          O Dockerfile: receita da sua imagem

          A imagem é construída a partir de um arquivo de texto chamado Dockerfile, que funciona como uma receita passo a passo. É um artefato de código que deve ser versionado junto com a aplicação. Veja um exemplo simples para uma aplicação Node.js:

          # Parte de uma imagem base já pronta
          FROM node:20
          
          # Define a pasta de trabalho dentro do container
          WORKDIR /app
          
          # Copia e instala as dependências
          COPY package*.json ./
          RUN npm install
          
          # Copia o restante do código
          COPY . .
          
          # Expõe a porta e define o comando inicial
          EXPOSE 3000
          CMD ["node", "index.js"]

          Com esse arquivo, qualquer pessoa consegue gerar exatamente a mesma imagem. Esse versionamento da infraestrutura combina muito bem com o versionamento do código no O que é Git? Controle de versão para iniciantes: o Dockerfile vive junto do projeto e evolui com ele.

          Repare na ordem: primeiro copiamos apenas o package*.json e instalamos as dependências, e só depois copiamos o restante do código. Enquanto você só altera o seu código (e não as dependências), o Docker reaproveita a camada do npm install em cache, economizando muito tempo a cada build.

          Vale conhecer as principais instruções:

            .dockerignore: não copie o que não precisa

            Assim como o .gitignore, o arquivo .dockerignore diz ao Docker quais arquivos e pastas devem ser ignorados ao construir a imagem. Isso evita enviar lixo para dentro da imagem (como node_modules local, logs ou a pasta .git), reduz o tamanho final e acelera o build:

            node_modules
            npm-debug.log
            .git
            .env
            dist

            Esquecer o .dockerignore é um erro comum: sem ele, você pode acabar copiando centenas de megabytes desnecessários para dentro da imagem e até vazar segredos (como um arquivo .env) por descuido.

            Multi-stage builds: imagens menores e mais seguras

            Um dos recursos mais valiosos do Docker são as construções em múltiplos estágios. A ideia é usar uma imagem maior, com todas as ferramentas de build, para compilar a aplicação, e depois copiar apenas o resultado final para uma imagem enxuta. Assim, a imagem que vai para produção não carrega compiladores, dependências de desenvolvimento nem código-fonte desnecessário.

            # Estágio 1: build
            FROM node:20 AS build
            WORKDIR /app
            COPY package*.json ./
            RUN npm install
            COPY . .
            RUN npm run build
            
            # Estágio 2: imagem final, enxuta
            FROM node:20-slim
            WORKDIR /app
            COPY --from=build /app/dist ./dist
            COPY package*.json ./
            RUN npm install --omit=dev
            EXPOSE 3000
            CMD ["node", "dist/index.js"]

            O resultado é uma imagem final menor, mais rápida de transferir e com menor superfície de ataque, já que tudo o que era necessário apenas para construir ficou para trás no primeiro estágio.

            Comandos essenciais do Docker

            O fluxo básico do Docker gira em torno de poucos comandos:

            # Constrói uma imagem a partir do Dockerfile
            docker build -t minha-app .
            
            # Cria e executa um container a partir da imagem
            docker run -p 3000:3000 minha-app
            
            # Lista os containers em execução
            docker ps
            
            # Para um container
            docker stop <id>

            Vale entender cada um:

              É comum que essa aplicação exponha uma O que é uma API? Definição clara para iniciantes, consumida por outros serviços ou pelo front-end. Outros comandos muito úteis no dia a dia:

              docker images          # lista as imagens locais
              docker rm <id>         # remove um container parado
              docker rmi <imagem>    # remove uma imagem
              docker logs <id>       # mostra os logs de um container
              docker exec -it <id> bash   # abre um shell dentro do container em execução

              Volumes e persistência de dados

              Containers são, por natureza, efêmeros: quando você remove um container, tudo o que foi escrito no seu sistema de arquivos interno desaparece junto. Isso é ótimo para reproducibilidade, mas péssimo quando você precisa guardar dados (de um banco de dados, por exemplo).

              A solução são os volumes. Um volume é um espaço de armazenamento gerenciado pelo Docker que vive fora do ciclo de vida do container, persistindo mesmo que o container seja destruído e recriado:

              # Cria um volume nomeado
              docker volume create dados-db
              
              # Monta o volume em um diretório do container
              docker run -v dados-db:/var/lib/postgresql/data postgres:16

              Há também os bind mounts, que mapeiam um diretório do seu host diretamente para dentro do container. Eles são muito usados em desenvolvimento, para que alterações no código no host reflitam instantaneamente dentro do container:

              docker run -v $(pwd):/app minha-app

              A regra prática é: use volumes nomeados para dados que precisam persistir (bancos de dados, uploads) e bind mounts para o fluxo de desenvolvimento local.

              Redes no Docker

              Por padrão, cada container roda em uma rede isolada. Para que vários containers conversem entre si — por exemplo, sua aplicação acessando um banco de dados em outro container — o Docker oferece redes (networks). Ao colocar dois containers na mesma rede definida pelo usuário, eles passam a se enxergar pelo nome do serviço, sem precisar saber o endereço IP um do outro:

              docker network create minha-rede
              docker run --network minha-rede --name banco postgres:16
              docker run --network minha-rede --name api minha-app

              Nesse exemplo, a aplicação api pode se conectar ao banco simplesmente usando o hostname banco. Esse mecanismo de descoberta por nome é a base para conectar os diversos serviços de uma aplicação distribuída.

              Docker Compose: orquestrando vários containers

              Subir um container por vez na mão funciona para testes rápidos, mas aplicações reais costumam ter vários serviços: a aplicação, o banco de dados, um cache, uma fila. Gerenciar isso comando a comando vira um pesadelo. É aí que entra o Docker Compose.

              O Compose permite descrever todos os serviços, redes e volumes da sua aplicação em um único arquivo YAML, e subir tudo com um comando só:

              services:
                api:
                  build: .
                  ports:
                    - "3000:3000"
                  environment:
                    - DATABASE_URL=postgres://user:senha@banco:5432/app
                  depends_on:
                    - banco
              
                banco:
                  image: postgres:16
                  environment:
                    - POSTGRES_USER=user
                    - POSTGRES_PASSWORD=senha
                    - POSTGRES_DB=app
                  volumes:
                    - dados-db:/var/lib/postgresql/data
              
              volumes:
                dados-db:

              Com esse arquivo docker-compose.yml, você sobe a stack inteira com docker compose up e derruba tudo com docker compose down. O Compose cria automaticamente uma rede compartilhada, conecta os serviços pelos seus nomes (a api acessa o banco pelo host banco) e cuida da ordem de inicialização com depends_on. Para desenvolvimento local, é a forma mais prática de reproduzir um ambiente completo.

              Registries: compartilhando imagens

              Depois de construir uma imagem, você pode publicá-la em um registry, um repositório de imagens. O mais conhecido é o Docker Hub, mas existem muitos outros (GitHub Container Registry, Amazon ECR, Google Artifact Registry) e empresas costumam ter registries privados.

              docker push minha-empresa/minha-app:1.0
              docker pull minha-empresa/minha-app:1.0

              Isso permite que qualquer máquina baixe e rode exatamente a mesma imagem. Repare na tag :1.0 no final do nome: as tags identificam versões da imagem. É uma boa prática evitar depender da tag latest em produção, porque ela é móvel e pode apontar para imagens diferentes ao longo do tempo, quebrando a reproducibilidade. Prefira tags explícitas e imutáveis (por número de versão ou hash de commit).

              É justamente esse poder de reprodutibilidade que faz o Docker brilhar em automação de testes e deploy, o tema do O que é CI/CD? Integração e entrega contínua: o pipeline constrói a imagem uma vez, roda os testes dentro dela e a promove, idêntica, por todos os ambientes.

              Docker e arquiteturas modernas

              A leveza dos containers casou perfeitamente com uma tendência arquitetural: dividir grandes aplicações em serviços menores e independentes. Cada serviço vira um container próprio, com seu ciclo de vida e suas dependências isoladas. Esse modelo é detalhado em O que são microsserviços? Vantagens, desafios e quando usar.

              Mas rodar muitos containers em produção traz novos desafios: como escalá-los, reiniciá-los quando caem e distribuí-los entre várias máquinas? É aí que entra a orquestração, explicada em O que é Kubernetes? Orquestração de containers. A própria evolução dos sistemas de orquestração na indústria nasceu da experiência do Google em operar containers em escala massiva (Burns et al., 2016).

              Em resumo: o Docker resolve o empacotamento e a execução de um container; o Kubernetes resolve a coordenação de muitos containers em produção. São camadas complementares.

              Segurança em containers

              Containers oferecem isolamento, mas não são uma barreira de segurança absoluta — afinal, eles compartilham o kernel do host. Alguns cuidados reduzem bastante os riscos:

                Troubleshooting: quando algo dá errado

                Mais cedo ou mais tarde, um container vai falhar. Estes são os primeiros comandos a usar para investigar:

                  Um erro clássico é o container que "sobe e morre" imediatamente. Geralmente isso acontece porque o processo principal terminou (um container vive enquanto o seu processo de PID 1 estiver rodando) ou porque houve uma falha de configuração. O docker logs costuma revelar a causa em segundos.

                  Vantagens de adotar containers

                  Resumindo os ganhos que o Docker trouxe:

                    Boas práticas ao usar Docker

                    Para tirar o máximo proveito, vale seguir alguns cuidados:

                      Perguntas frequentes

                      Docker substitui as máquinas virtuais? Não totalmente. Containers e VMs resolvem problemas parecidos com trade-offs diferentes. VMs oferecem isolamento mais forte (kernel próprio) e são necessárias quando você precisa rodar sistemas operacionais diferentes ou exige uma fronteira de segurança mais rígida. Na prática, é comum rodar containers dentro de VMs, combinando os dois.

                      Preciso saber Linux para usar Docker? Ajuda muito, mas não é pré-requisito para começar. Como containers Linux rodam sobre o kernel Linux, conhecer comandos básicos de terminal e de sistema de arquivos torna o aprendizado bem mais suave. No Windows e no macOS, o Docker Desktop cuida da camada Linux por baixo.

                      Qual a diferença entre Docker e Docker Compose? O Docker gerencia containers individualmente. O Docker Compose é uma ferramenta para definir e orquestrar vários containers de uma aplicação a partir de um único arquivo YAML, ideal para desenvolvimento local com múltiplos serviços.

                      Docker e Kubernetes são concorrentes? Não, são complementares. O Docker cuida de construir e rodar containers; o Kubernetes orquestra muitos containers em produção, em múltiplas máquinas, cuidando de escala, recuperação de falhas e distribuição de tráfego.

                      Containers são seguros? São razoavelmente isolados, mas compartilham o kernel do host, então não substituem uma fronteira de segurança forte por si só. Seguindo boas práticas — não rodar como root, usar imagens mínimas, escanear vulnerabilidades e não embutir segredos — você reduz bastante os riscos.

                      Por que meu container morre logo depois de subir? Quase sempre porque o processo principal (PID 1) terminou ou falhou. Um container só vive enquanto seu processo principal estiver rodando. Use docker logs <id> para ver a causa e docker ps -a para checar o código de saída.

                      Conclusão

                      O Docker resolveu um problema antigo e custoso ao empacotar aplicações em containers leves, isolados e portáteis. Por trás da simplicidade da interface estão recursos do kernel Linux — namespaces e cgroups — que oferecem isolamento eficiente, muito mais barato que as máquinas virtuais tradicionais. Entendendo a diferença entre imagens e containers, o papel do Dockerfile, os volumes, as redes e os comandos básicos, você já consegue rodar suas aplicações de forma consistente em qualquer lugar.

                      A partir daí, o Docker abre as portas para microsserviços, pipelines de CI/CD e orquestração em larga escala — os pilares do desenvolvimento moderno. Comece pequeno: escreva um Dockerfile para um projeto seu, suba-o com docker run, experimente o Docker Compose com um banco de dados, e veja a mágica do "funciona em qualquer máquina" acontecer.

                      Referências

                        Leituras relacionadas

                        Nenhum comentário ainda

                        Seja o primeiro a comentar.

                        Deixe seu comentário

                        Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

                        Entrar com Canverly