Practical guide
Docker: from the first container to a smaller attack surface.
What images and containers are, the commands you use every day, how to write a Dockerfile, choose between Ubuntu and Alpine, shrink the image with multi-stage builds and remove what an attacker would use, plus ports, networks, Compose, health checks and logs, with a lab you can run.
O que é Docker: contêiner, imagem e por que usarWhat Docker is: container, image and why use it
Todo mundo já ouviu “na minha máquina funciona”. O Docker resolve isso empacotando o app junto com tudo que ele precisa para rodar (runtime, bibliotecas, arquivos de sistema) numa unidade que se comporta igual no seu notebook, no CI e no servidor. Essa unidade em execução é um contêiner.Everyone has heard “it works on my machine”. Docker fixes that by packaging the app together with everything it needs to run (runtime, libraries, system files) into a unit that behaves the same on your laptop, in CI and on the server. That unit, running, is a container.
Um contêiner não é uma máquina virtual. A VM simula um computador e roda um sistema operacional inteiro por cima. O contêiner é só um processo comum do Linux, isolado por recursos do próprio kernel (namespaces escondem o resto do sistema; cgroups limitam CPU e memória). Por isso sobe em milissegundos e pesa megabytes em vez de gigabytes.A container is not a virtual machine. A VM simulates a computer and runs a whole operating system on top. A container is just an ordinary Linux process, isolated by features of the kernel itself (namespaces hide the rest of the system; cgroups cap CPU and memory). That is why it starts in milliseconds and weighs megabytes instead of gigabytes.
| TermoTerm | O que éWhat it is | AnalogiaAnalogy |
|---|---|---|
| ImagemImage | Pacote imutável e em camadas com o sistema de arquivos e o comando de partida do app. Tem nome e tag (postgres:17-alpine).Immutable, layered package with the app's file system and start command. It has a name and a tag (postgres:17-alpine). | A receita pronta e congeladaThe finished, frozen recipe |
| ContêinerContainer | Uma imagem em execução: processo isolado + uma camada gravável por cima.An image running: an isolated process + a writable layer on top. | O prato servidoThe dish served |
| Dockerfile | Texto com os passos para construir uma imagem.Text with the steps to build an image. | O caderno de receitasThe recipe book |
| Registry | Servidor de imagens: Docker Hub, GitHub Container Registry, AWS ECR.Image server: Docker Hub, GitHub Container Registry, AWS ECR. | A estante de onde se pega a receitaThe shelf the recipe comes from |
| VolumeVolume | Pasta gerenciada pelo Docker que sobrevive ao contêiner.Docker-managed folder that outlives the container. | O armário externoThe storage locker |
| RedeNetwork | Rede virtual que liga contêineres e dá DNS por nome.Virtual network that connects containers and gives DNS by name. | O ramal internoThe internal phone line |
| Compose | Um arquivo YAML que descreve vários contêineres, redes e volumes e sobe tudo junto.A YAML file describing several containers, networks and volumes, started together. | O cardápio completoThe full menu |
O exemplo que acompanha a nota é um mini serviço em Go (o código completo está na atividade 0) com três rotas: / responde com o nome do contêiner, /health diz que está vivo e /db tenta abrir uma conexão com um Postgres vizinho. Go foi escolhido porque gera um binário único, o que deixa o multi-stage build (mais abaixo) bem visível; nada aqui depende de Go.The example that runs through the note is a tiny Go service (full code in activity 0) with three routes: / answers with the container name, /health says it is alive and /db tries to open a connection to a neighbouring Postgres. Go was chosen because it produces a single binary, which makes the multi-stage build (below) very visible; nothing here depends on Go.
Do Dockerfile ao contêiner: o ciclo de vidaFrom Dockerfile to container: the life cycle
O fluxo é sempre o mesmo: build (Dockerfile → imagem), push/pull (levar a imagem de e para um registry) e run (imagem → contêiner). Uma imagem pode virar muitos contêineres, e apagar um contêiner nunca apaga a imagem.The flow is always the same: build (Dockerfile → image), push/pull (move the image to and from a registry) and run (image → container). One image can become many containers, and deleting a container never deletes the image.
A imagem é feita de camadas, uma por instrução do Dockerfile. O Docker guarda cada camada em cache: se a instrução e tudo acima dela não mudaram, ele reaproveita. Por isso a ordem das linhas importa (veja a seção do Dockerfile). Dois contêineres da mesma imagem compartilham as camadas de leitura e só cada um tem a sua camada gravável, que desaparece junto com o contêiner.The image is made of layers, one per Dockerfile instruction. Docker caches each layer: if the instruction and everything above it did not change, it reuses it. That is why the order of the lines matters (see the Dockerfile section). Two containers from the same image share the read-only layers and each has its own writable layer, which vanishes with the container.
Comandos essenciaisEssential commands
# --- imagens ---
docker pull alpine:3.22 # baixa do registry
docker images # lista imagens locais
docker build -t meu-app:1.0 . # constroi a imagem a partir do ./Dockerfile
docker history meu-app:1.0 # camadas e tamanho de cada uma
docker rmi meu-app:1.0 # apaga a imagem
# --- contêineres ---
docker run -d --name web -p 8080:8080 meu-app:1.0 # cria + inicia em segundo plano
docker run --rm -it alpine:3.22 sh # descartavel e interativo
docker ps # os que estao rodando (-a: todos)
docker stop web # SIGTERM, espera 10 s, depois SIGKILL
docker start web # liga de novo o mesmo contêiner
docker rm web # apaga o contêiner parado (-f: forca)
docker exec -it web sh # abre um shell DENTRO de um contêiner rodando
docker cp web:/app/arquivo.txt . # copia arquivo para fora
# --- investigar ---
docker logs -f --tail 50 web # ultimas 50 linhas e segue
docker inspect web # JSON com tudo (IP, volumes, saude, usuario)
docker stats # CPU e memoria ao vivo
docker top web # processos dentro do contêiner
# --- limpeza ---
docker system df # quanto espaco o Docker usa
docker system prune # apaga parados, redes e imagens sem tag
Flag do docker rundocker run flag | EfeitoEffect |
|---|---|
-d | Roda em segundo plano (detached); sem ele o terminal fica preso ao contêiner.Runs in the background (detached); without it the terminal stays attached to the container. |
--name | Nome fixo. Sem ele o Docker inventa um (vigilant_curie), e o nome também vira o host na rede.A fixed name. Without it Docker invents one (vigilant_curie), and the name also becomes the host on the network. |
-p H:C | Publica a porta C do contêiner na porta H do host (veja portas).Publishes container port C on host port H (see ports). |
-e CHAVE=valor | Variável de ambiente. --env-file .env lê de um arquivo.Environment variable. --env-file .env reads from a file. |
-v nome:/caminho | Monta um volume (veja volumes).Mounts a volume (see volumes). |
--rm | Apaga o contêiner quando ele termina. Ótimo para testes.Deletes the container when it exits. Great for experiments. |
--restart | no (padrão), on-failure, always, unless-stopped: o que fazer quando o processo sai.no (default), on-failure, always, unless-stopped: what to do when the process exits. |
--memory, --cpus | Limites de recurso. Sem eles um contêiner com bug pode consumir a máquina inteira.Resource limits. Without them a buggy container can eat the whole machine. |
--network | Em qual rede o contêiner entra (veja redes).Which network the container joins (see networks). |
docker rm. Dados que importam vão em volume (ou num banco gerenciado); configuração vai em variável de ambiente; logs vão para a saída padrão. É a base de tudo que vem a seguir.A container is disposable. Anything the app writes inside the container disappears on docker rm. Data that matters goes in a volume (or a managed database); configuration goes in environment variables; logs go to standard output. This is the basis of everything that follows.Dockerfile: escrevendo a receitaDockerfile: writing the recipe
Primeiro uma versão ingênua, de um único estágio, para entender as instruções. Ela funciona, e é exatamente o tipo de imagem que você não quer em produção (veja o motivo na seção de multi-stage):First a naive, single-stage version, to understand the instructions. It works, and it is exactly the kind of image you do not want in production (see why in the multi-stage section):
FROM golang:1.25-alpine # imagem base: ponto de partida
WORKDIR /app # pasta de trabalho (criada se nao existir)
COPY go.mod ./ # 1) so o arquivo de dependencias...
RUN go mod download # ...e baixa as dependencias (camada em cache)
COPY . . # 2) depois o codigo (muda toda hora)
RUN go build -o /app/server . # compila
EXPOSE 8080 # documenta a porta (nao publica nada)
CMD ["/app/server"] # comando de partida
| InstruçãoInstruction | O que fazWhat it does |
|---|---|
FROM | Escolhe a imagem base. Toda imagem começa em outra. Use tag fixa (1.25-alpine), nunca latest.Picks the base image. Every image starts from another. Use a fixed tag (1.25-alpine), never latest. |
WORKDIR | Define a pasta atual para as instruções seguintes.Sets the current folder for the following instructions. |
COPY | Copia arquivos do seu projeto para a imagem. Prefira COPY a ADD (que também baixa URLs e descompacta, comportamento surpreendente).Copies files from your project into the image. Prefer COPY to ADD (which also fetches URLs and unpacks archives, surprising behaviour). |
RUN | Executa um comando durante o build e grava o resultado numa camada (instalar pacote, compilar).Runs a command at build time and stores the result in a layer (install a package, compile). |
ENV / ARG | ENV vale também em execução. ARG só no build. Nenhum dos dois é lugar de segredo: ficam visíveis em docker history e docker inspect.ENV also applies at run time. ARG only at build time. Neither is a place for secrets: both show up in docker history and docker inspect. |
EXPOSE | Só documenta a porta em que o app escuta. Não abre nada para fora (isso é o -p).Only documents the port the app listens on. It opens nothing to the outside (that is -p). |
USER | Roda o que vem depois (e o contêiner) como esse usuário, em vez de root.Runs what follows (and the container) as that user instead of root. |
HEALTHCHECK | Ensina o Docker a testar se o app está bem (veja saúde).Teaches Docker how to test whether the app is well (see health). |
ENTRYPOINT / CMD | O que executar quando o contêiner inicia. ENTRYPOINT é o programa; CMD são os argumentos padrão, que o docker run imagem args substitui.What to run when the container starts. ENTRYPOINT is the program; CMD are the default arguments, which docker run image args replaces. |
Cache: a ordem das linhas importaCache: line order matters
Repare que o Dockerfile acima copia o go.mod e baixa as dependências antes de copiar o resto do código. Como o código muda a cada commit e as dependências quase nunca, o Docker reaproveita a camada pesada do go mod download e só refaz o COPY . . e o build. A regra geral: o que muda menos vem primeiro. No Node seria COPY package*.json + npm ci antes do COPY . .; no Python, requirements.txt + pip install.Notice that the Dockerfile above copies go.mod and downloads dependencies before copying the rest of the code. Since code changes on every commit and dependencies almost never do, Docker reuses the heavy go mod download layer and only redoes COPY . . and the build. General rule: what changes least comes first. In Node it would be COPY package*.json + npm ci before COPY . .; in Python, requirements.txt + pip install.
.dockerignore.dockerignore
O COPY . . leva a pasta inteira. O arquivo .dockerignore (mesma sintaxe do .gitignore) diz o que ficar de fora: acelera o build e, mais importante, evita que .env, chaves e a pasta .git entrem na imagem, onde qualquer pessoa com a imagem os encontraria.The COPY . . takes the whole folder. The .dockerignore file (same syntax as .gitignore) says what to leave out: it speeds up the build and, more importantly, keeps .env, keys and the .git folder out of the image, where anyone holding the image would find them.
.git
.env
*.pem
node_modules
*.md
Dockerfile*
compose.yaml
Executar o app: PID 1, sinais e um processo por contêinerRunning the app: PID 1, signals and one process per container
- Um processo principal por contêiner. O contêiner vive enquanto esse processo viver; quando ele sai, o contêiner para. Banco, API e fila ficam em contêineres separados, ligados por rede (veja Compose), não todos dentro de um só com systemd ou
supervisord.One main process per container. The container lives as long as that process lives; when it exits, the container stops. Database, API and queue go in separate containers, joined by a network (see Compose), not all inside one with systemd orsupervisord. - Use a forma exec (
CMD ["/server"], com colchetes), não a forma shell (CMD /server). A forma shell executash -c "/server": o shell vira o PID 1 e não repassa o SIGTERM ao app, então odocker stopespera 10 s e acaba matando com SIGKILL, sem desligamento limpo.Use the exec form (CMD ["/server"], with brackets), not the shell form (CMD /server). The shell form runssh -c "/server": the shell becomes PID 1 and does not forward SIGTERM to the app, sodocker stopwaits 10 s and ends up killing it with SIGKILL, with no clean shutdown. - O app deve tratar o SIGTERM (parar de aceitar requisições, terminar as em andamento, fechar conexões). Se ele cria processos filhos, rode com
--init(ouinit: trueno Compose) para um init mínimo recolher os zumbis.The app should handle SIGTERM (stop accepting requests, finish in-flight ones, close connections). If it spawns child processes, run with--init(orinit: truein Compose) so a minimal init reaps the zombies. - O app deve escutar em
0.0.0.0, não em127.0.0.1: dentro do contêiner, “localhost” é o próprio contêiner, e a porta publicada chega pela interface de rede dele, não pelo loopback.The app must listen on0.0.0.0, not127.0.0.1: inside the container “localhost” is the container itself, and the published port arrives through its network interface, not the loopback. - Configuração por variáveis de ambiente (
PORT,DB_ADDR), e logs na saída padrão. Assim a mesma imagem serve para dev, teste e produção.Configuration through environment variables (PORT,DB_ADDR) and logs on standard output. That way the same image serves dev, test and production.
Ubuntu ou Alpine? Escolhendo a imagem baseUbuntu or Alpine? Choosing the base image
Quando um Dockerfile diz FROM ubuntu:24.04, o contêiner não roda o Ubuntu como sistema: ele roda o kernel do host com o sistema de arquivos do Ubuntu por cima (/bin, /etc, o apt, a glibc). Não há boot, systemd nem login. A escolha da base decide quais ferramentas e bibliotecas existem dentro, e portanto o tamanho e a superfície de ataque.When a Dockerfile says FROM ubuntu:24.04, the container does not run Ubuntu as a system: it runs the host kernel with the Ubuntu file system on top (/bin, /etc, apt, glibc). There is no boot, no systemd, no login. The base you pick decides which tools and libraries exist inside, and therefore the size and the attack surface.
| Ubuntu / Debian | Alpine | Distroless / scratch | |
|---|---|---|---|
| Tamanho da baseBase size | ≈ 30 a 80 MB (-slim é menor)≈ 30 to 80 MB (-slim is smaller) | ≈ 8 MB | ≈ 2 MB (static) a 20 MB≈ 2 MB (static) to 20 MB |
| Biblioteca CC library | glibc | musl | glibc ou nenhumaglibc or none |
| Gerenciador de pacotesPackage manager | apt | apk | nenhumnone |
| Shell | bash | sh (BusyBox)sh (BusyBox) | nenhumnone |
| CompatibilidadeCompatibility | A mais ampla; quase todo binário pré-compilado funcionaThe widest; nearly every prebuilt binary works | Alguns binários e wheels Python assumem glibc e quebram ou compilam devagarSome binaries and Python wheels assume glibc and break or build slowly | Só roda o que você já trouxe pronto (binário estático)Only runs what you bring prebuilt (static binary) |
| DepuraçãoDebugging | Fácil (tem tudo)Easy (has everything) | Boa (BusyBox)Good (BusyBox) | Difícil de propósitoHard on purpose |
| Superfície de ataqueAttack surface | Maior (muitos pacotes, mais CVEs a acompanhar)Larger (many packages, more CVEs to follow) | PequenaSmall | MínimaMinimal |
Medido no exemplo desta nota (o mesmo binário copiado para cada base, tamanho descomprimido de docker image inspect; varia com a versão e a arquitetura):Measured on this note's example (the same binary copied onto each base, uncompressed size from docker image inspect; varies with version and architecture):
| Imagem finalFinal image | TamanhoSize |
|---|---|
Um estágio, sobre golang:1.25-alpine (compilador e fontes inclusos)Single stage on golang:1.25-alpine (compiler and sources included) | 452 MB |
ubuntu:24.04 + binário+ binary | 126 MB |
alpine:3.22 + binário+ binary | 22 MB |
distroless/static:nonroot + binário+ binary | 15 MB |
Como decidirHow to decide
- Comece pela variante
-slim(Debian) ou-alpineda imagem oficial da sua linguagem (node:22-slim,python:3.13-slim), em vez de partir deubuntue instalar o runtime. Já vem configurada, atualizada e menor.Start from the-slim(Debian) or-alpinevariant of your language's official image (node:22-slim,python:3.13-slim) instead of starting fromubuntuand installing the runtime. It comes configured, maintained and smaller. - Alpine quando o app roda bem em musl (Go, Node, a maioria dos apps Java e Python puros) e você quer uma base pequena. Cuidado com dependências nativas: se algo falhar com erros de biblioteca ou o
pip installdemorar para compilar, troque para-slim.Alpine when the app runs fine on musl (Go, Node, most Java and pure Python apps) and you want a small base. Mind native dependencies: if something fails with library errors orpip installtakes forever compiling, switch to-slim. - Ubuntu quando você precisa de pacotes específicos do
apt, de compatibilidade máxima ou segue o padrão da empresa. Não é “errado”, só maior; mantenha-o enxuto (exemplo abaixo).Ubuntu when you need specificaptpackages, maximum compatibility, or your company standard. It is not “wrong”, just larger; keep it lean (example below). - Distroless ou
scratchpara produção de binários estáticos (Go, Rust) ou apps com runtime mínimo, geralmente como estágio final de um multi-stage.Distroless orscratchfor production of static binaries (Go, Rust) or minimal-runtime apps, usually as the final stage of a multi-stage build.
# Ubuntu enxuto: um RUN so, sem recomendados, limpando o cache do apt na mesma camada
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 app
USER app
# Alpine equivalente
FROM alpine:3.22
RUN apk add --no-cache ca-certificates curl \
&& adduser -S -u 10001 app
USER app
RUN A e apaga no RUN B, os arquivos continuam na camada A e a imagem não diminui. Instalar e limpar precisam estar no mesmo RUN.Why “clean in the same layer”: a layer never shrinks. If you install in RUN A and delete in RUN B, the files stay in layer A and the image does not get smaller. Installing and cleaning must happen in the same RUN.Multi-stage build: levar para produção só o que rodaMulti-stage build: ship only what runs
Compilar um app exige compilador, cabeçalhos, gerenciador de pacotes, código-fonte e, às vezes, chaves de acesso a repositórios privados. Nada disso é necessário para executar o resultado, mas num Dockerfile de um estágio tudo fica na imagem final, pesando e dando ao atacante ferramentas prontas (um compilador, curl, apk). O multi-stage build usa vários FROM no mesmo arquivo: o primeiro estágio constrói, o último recebe só o artefato.Compiling an app needs a compiler, headers, a package manager, source code and sometimes access keys for private repositories. None of that is needed to run the result, but in a single-stage Dockerfile it all stays in the final image, adding weight and handing an attacker ready-made tools (a compiler, curl, apk). A multi-stage build uses several FROM lines in one file: the first stage builds, the last receives only the artifact.
# ---- estagio 1: compila (tem compilador, shell, apk; nada disso vai para o final)
FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server .
# ---- estagio 2: so o binario, sem shell, sem apt/apk, usuario nao-root
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["/server", "healthcheck"]
ENTRYPOINT ["/server"]
AS builddá nome ao primeiro estágio;COPY --from=buildtraz de lá apenas o binário. Os outros arquivos do estágio 1 são descartados e não entram na imagem final nem nas suas camadas.AS buildnames the first stage;COPY --from=buildbrings over only the binary. The other files of stage 1 are discarded and do not enter the final image or its layers.CGO_ENABLED=0gera um binário estático (não depende de glibc nem musl), o que permite rodar sobre a base mínimadistroless/staticou atéscratch(imagem vazia).-s -wremove símbolos de depuração e-trimpathtira caminhos da máquina de build.CGO_ENABLED=0produces a static binary (depends on neither glibc nor musl), which lets it run on the minimaldistroless/staticbase or evenscratch(an empty image).-s -wstrips debug symbols and-trimpathremoves build-machine paths.- A tag
:nonrootda distroless já defineUSER 65532: o app não roda como root.The distroless:nonroottag already setsUSER 65532: the app does not run as root. - Em linguagens interpretadas o desenho é o mesmo: estágio de build instala dependências de desenvolvimento e gera o bundle (
npm run build,pip wheel); o final copia sódist/ou o virtualenv para uma imagem-slimou distroless do runtime (gcr.io/distroless/nodejs22-debian12).In interpreted languages the design is the same: the build stage installs development dependencies and produces the bundle (npm run build,pip wheel); the final one copies onlydist/or the virtualenv onto a-slimor distroless runtime image (gcr.io/distroless/nodejs22-debian12). - É possível construir só um estágio com
docker build --target build ., útil para rodar testes dentro do estágio de build.You can build just one stage withdocker build --target build ., handy for running tests inside the build stage.
Reduzir a superfície de ataqueReducing the attack surface
Se alguém conseguir executar código dentro do seu contêiner (uma falha no app, uma dependência comprometida), o estrago depende do que existe lá dentro. Um shell permite navegar e rodar comandos; um curl baixa ferramentas; o root altera o que quiser; capacidades extras permitem mexer na rede. A estratégia é tirar tudo o que o app não usa, em camadas:If someone manages to execute code inside your container (a flaw in the app, a compromised dependency), the damage depends on what exists in there. A shell lets them roam and run commands; curl downloads tools; root can change anything; extra capabilities let them touch the network. The strategy is to remove everything the app does not use, in layers:
| CamadaLayer | O que fazerWhat to do | O que o atacante perdeWhat the attacker loses |
|---|---|---|
| ImagemImage | Base mínima (alpine, -slim, distroless) + multi-stage. Sem shell, sem gerenciador de pacotes, sem curl na imagem final.Minimal base (alpine, -slim, distroless) + multi-stage. No shell, no package manager, no curl in the final image. | Terminal interativo, apt install, download de ferramentas, compiladorInteractive terminal, apt install, tool download, compiler |
| UsuárioUser | USER não-root (uid fixo, ex.: 10001 ou 65532).Non-root USER (fixed uid, e.g. 10001 or 65532). | Escrever em áreas do sistema, instalar, abrir portas < 1024Writing to system areas, installing, opening ports < 1024 |
| Sistema de arquivosFile system | --read-only (e --tmpfs /tmp se o app precisar de temporários).--read-only (and --tmpfs /tmp if the app needs temp files). | Gravar um binário ou script e executá-loWriting a binary or script and running it |
| CapacidadesCapabilities | --cap-drop ALL (devolva só as necessárias com --cap-add) e --security-opt no-new-privileges.--cap-drop ALL (add back only what is needed with --cap-add) and --security-opt no-new-privileges. | Operações privilegiadas de rede e de sistema; escalar privilégios via binários setuidPrivileged network and system operations; escalating through setuid binaries |
| RecursosResources | --memory, --cpus, --pids-limit.--memory, --cpus, --pids-limit. | Derrubar o host por consumo (DoS), fork bombTaking the host down by consumption (DoS), fork bombs |
| RedeNetwork | Só as portas necessárias, em 127.0.0.1 quando possível; redes internal para o que não precisa de internet (veja redes).Only the necessary ports, on 127.0.0.1 when possible; internal networks for what does not need the internet (see networks). | Alcançar o banco de fora; exfiltrar dados pela internetReaching the database from outside; exfiltrating data to the internet |
| SegredosSecrets | Nunca em ENV, ARG ou COPY. Em execução, secrets do Compose/orquestrador (arquivo em /run/secrets); no build, RUN --mount=type=secret.Never in ENV, ARG or COPY. At run time, Compose/orchestrator secrets (a file in /run/secrets); at build time, RUN --mount=type=secret. | Ler senhas com docker inspect, docker history ou no env do processoReading passwords with docker inspect, docker history or the process env |
| Cadeia de suprimentosSupply chain | Tags fixas ou digest (FROM img@sha256:…), imagens oficiais, escaneamento (Trivy, Docker Scout) no CI, rebuild periódico para pegar correções.Fixed tags or digest (FROM img@sha256:…), official images, scanning (Trivy, Docker Scout) in CI, periodic rebuilds to pick up fixes. | Explorar CVEs conhecidos de pacotes que você nem usaExploiting known CVEs in packages you do not even use |
Na prática, as opções de execução ficam assim (e viram campos do Compose, veja adiante):In practice the run options look like this (and become Compose fields, see below):
docker run -d --name app \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges:true \
--memory 128m --pids-limit 100 \
-p 127.0.0.1:8080:8080 \
meu-app:1.0
“Mas e se eu precisar de um terminal para depurar?”“But what if I need a terminal to debug?”
Em uma imagem sem shell, docker exec -it app sh falha de propósito:On an image with no shell, docker exec -it app sh fails on purpose:
$ docker exec -it app sh
OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
Isso é o objetivo, mas você ainda consegue investigar sem pôr ferramentas na imagem de produção: use docker logs e docker inspect; ou suba um contêiner de depuração descartável que compartilha a rede e os processos do alvo:That is the point, but you can still investigate without putting tools in the production image: use docker logs and docker inspect; or start a throwaway debug container that shares the network and processes of the target:
docker run --rm -it --network container:app --pid container:app nicolaka/netshoot
# dentro dele: ss -ltn, curl localhost:8080, ps aux (veem o alvo)
Outra opção é construir uma variante de depuração da mesma imagem (por exemplo, a tag :debug da distroless inclui um BusyBox) e usá-la só em desenvolvimento. O que não se faz é deixar o shell na imagem de produção “para o caso de precisar”.Another option is to build a debug variant of the same image (for example, the distroless :debug tag includes a BusyBox) and use it in development only. What you do not do is leave the shell in the production image “just in case”.
--privileged (o contêiner ganha quase os poderes do host), montar /var/run/docker.sock dentro de um contêiner (quem controla o socket controla o Docker e, na prática, o host) e rodar como root com o diretório do host montado. Se uma ferramenta pede isso, procure outra forma. Veja também OWASP Top 10 (configuração insegura e cadeia de suprimentos).Three things that undo everything else: --privileged (the container gets nearly the host's powers), mounting /var/run/docker.sock inside a container (whoever controls the socket controls Docker and, in practice, the host) and running as root with a host directory mounted. If a tool asks for that, look for another way. See also the OWASP Top 10 (security misconfiguration and supply chain).Mapeamento de portasPort mapping
Cada contêiner tem sua própria rede, e as portas dele não são visíveis fora até você publicá-las. O -p liga uma porta do host a uma porta do contêiner:Each container has its own network, and its ports are not visible outside until you publish them. -p ties a host port to a container port:
-p 8080:8080 # host:contêiner (escuta em TODAS as interfaces do host)
-p 127.0.0.1:8080:8080 # só a sua própria máquina acessa
-p 3000:8080 # porta 3000 do host -> 8080 do contêiner
-p 8080 # porta aleatória do host -> 8080 (veja com docker port)
-p 8080:8080/udp # UDP em vez de TCP (o padrão é TCP)
-P # publica todas as portas do EXPOSE em portas aleatórias
- Ordem é
HOST:CONTÊINER. A do contêiner é a que o app escuta (variávelPORT); a do host é livre, mas dois contêineres não podem usar a mesma porta do host.The order isHOST:CONTAINER. The container side is the one the app listens on (thePORTvariable); the host side is your choice, but two containers cannot use the same host port. EXPOSEno Dockerfile não publica nada: é documentação (e o que o-Pusa). Quem abre a porta é o-pou o campoportsdo Compose.EXPOSEin the Dockerfile publishes nothing: it is documentation (and what-Puses). What opens the port is-por the Composeportsfield.- Para conversa entre contêineres você não precisa publicar porta nenhuma: eles se falam pela rede interna, direto na porta do contêiner (veja redes). Publique só o que precisa ser acessado de fora, como a API ou um proxy reverso.For container-to-container traffic you do not need to publish any port: they talk over the internal network, straight to the container port (see networks). Publish only what must be reached from outside, like the API or a reverse proxy.
- Pergunte ao Docker o que está publicado:
docker port appou a coluna PORTS dodocker ps.Ask Docker what is published:docker port appor the PORTS column ofdocker ps.
-p 5432:5432, ele escreve suas próprias regras de iptables, que valem antes do UFW e do firewalld. Resultado clássico: o banco “protegido” pelo UFW fica aberto para a internet. Para bancos e painéis internos, não publique ou publique em 127.0.0.1 (e acesse por túnel SSH).Docker bypasses the host firewall. When you publish with -p 5432:5432 it writes its own iptables rules, which apply before UFW and firewalld. The classic result: the database “protected” by UFW is open to the internet. For databases and internal dashboards, do not publish or publish on 127.0.0.1 (and reach it through an SSH tunnel).Redes e comunicação entre contêineresNetworks and container-to-container communication
O Docker cria redes virtuais e liga cada contêiner a uma ou mais. Os tipos que importam:Docker creates virtual networks and attaches each container to one or more. The types that matter:
| DriverDriver | Como funcionaHow it works | Quando usarWhen to use |
|---|---|---|
bridge (padrãodefault) | Rede privada no host; os contêineres ganham IPs internos e saem para a internet por NAT.Private network on the host; containers get internal IPs and reach the internet through NAT. | Quase sempre. Use uma bridge criada por você (ou pelo Compose), não a rede bridge padrão.Almost always. Use a bridge you create (or Compose creates), not the built-in default bridge network. |
host | O contêiner usa diretamente a rede do host: sem isolamento e sem -p.The container uses the host network directly: no isolation and no -p. | Raro (desempenho extremo, ferramentas de rede). Perde o isolamento.Rare (extreme performance, network tools). Loses isolation. |
none | Sem rede além do loopback.No network except loopback. | Jobs que processam arquivos e nunca devem falar com a rede.Jobs that process files and must never talk to the network. |
overlay, macvlan | Redes entre vários hosts (Swarm) ou com IP próprio na LAN.Networks across several hosts (Swarm) or with their own LAN IP. | Cenários avançados; em vários hosts o normal é Kubernetes/ECS.Advanced scenarios; across several hosts the norm is Kubernetes/ECS. |
DNS por nome: a regra mais importanteDNS by name: the most important rule
Numa rede criada pelo usuário (e em toda rede do Compose), o Docker roda um DNS interno: o nome do contêiner (ou do serviço) vira o endereço. O app não precisa saber o IP do banco, que muda a cada recriação; ele conecta em db:5432. Na rede bridge padrão isso não funciona, e é por isso que ela deve ser evitada.On a user-created network (and every Compose network), Docker runs an internal DNS: the container (or service) name becomes the address. The app does not need to know the database IP, which changes on every recreation; it connects to db:5432. On the default bridge network this does not work, which is why it should be avoided.
docker network create minha-rede
docker run -d --name db --network minha-rede -e POSTGRES_PASSWORD=dev postgres:17-alpine
docker run --rm --network minha-rede alpine:3.22 nc -zv db 5432 # db (172.x.x.x:5432) open
docker network ls # redes existentes
docker network inspect minha-rede # quem esta nela, sub-rede, IPs
docker network connect minha-rede app # liga um contêiner existente a outra rede
- Dentro de um contêiner,
localhosté ele mesmo. O erro mais comum é configurar o app comDB_HOST=localhost: ele procura o banco dentro de si. Use o nome do serviço (db).Inside a container,localhostis itself. The most common mistake is configuring the app withDB_HOST=localhost: it looks for the database inside itself. Use the service name (db). - Entre contêineres vale a porta do contêiner, não a publicada. Se o Compose diz
"5433:5432", os outros serviços usamdb:5432; o 5433 é só para você, no host.Between containers the container port applies, not the published one. If Compose says"5433:5432", other services usedb:5432; 5433 is only for you, on the host. - Para o contêiner falar com algo no host (um banco rodando fora do Docker), use
host.docker.internal(Docker Desktop; no Linux adicioneextra_hosts: ["host.docker.internal:host-gateway"]).For a container to reach something on the host (a database running outside Docker), usehost.docker.internal(Docker Desktop; on Linux addextra_hosts: ["host.docker.internal:host-gateway"]). - Segmentação: contêineres só se enxergam se estiverem em uma rede em comum. Separe em
frontend(proxy e API) ebackend(API e banco): o proxy nunca alcança o banco. Marcar a rede comointernal: truetambém corta a saída para a internet, o que impede um banco comprometido de “ligar para casa”.Segmentation: containers only see each other if they share a network. Split intofrontend(proxy and API) andbackend(API and database): the proxy never reaches the database. Marking the networkinternal: truealso cuts outbound internet, which stops a compromised database from “phoning home”.
Volumes: dados que sobrevivemVolumes: data that survives
Como a camada gravável some com o contêiner, o que precisa persistir (arquivos do Postgres, uploads) fica fora dela:Since the writable layer vanishes with the container, whatever must persist (Postgres files, uploads) lives outside it:
| TipoType | SintaxeSyntax | Para quêWhat for |
|---|---|---|
| Volume nomeadoNamed volume | -v dbdata:/var/lib/postgresql/data | Dados de produção; o Docker gerencia onde ficam. Sobrevive a docker rm.Production data; Docker manages where it lives. Survives docker rm. |
| Bind mount | -v ./src:/app/src | Mapeia uma pasta do host. Bom em desenvolvimento (editar sem rebuild); em produção expõe o host, use com cuidado e :ro.Maps a host folder. Good in development (edit without rebuilding); in production it exposes the host, use with care and :ro. |
| tmpfs | --tmpfs /tmp | Memória RAM, some ao parar. Ideal para temporários com --read-only.RAM, gone on stop. Ideal for temp files with --read-only. |
docker volume ls
docker volume inspect dbdata # onde fica no host
docker volume rm dbdata # APAGA os dados
Docker Compose: vários contêineres num arquivo YAMLDocker Compose: several containers in one YAML file
Subir app, banco e proxy com três docker run cheios de flags não escala. O Compose descreve tudo num compose.yaml (também aceita docker-compose.yml) e sobe com um comando. É declarativo, como o Kubernetes ou o Terraform, só que para uma máquina só.Starting app, database and proxy with three flag-heavy docker run commands does not scale. Compose describes everything in a compose.yaml (docker-compose.yml is also accepted) and starts it with one command. It is declarative, like Kubernetes or Terraform, only for a single machine.
chave: valor é um mapa; linhas começando com - são uma lista; [a, b] e {x: 1} são as versões numa linha; # comenta. Valores como "8080:8080" ficam entre aspas para o YAML não interpretar : como número. A chave version: do topo é obsoleta e pode ser omitida. O comando docker compose (com espaço) é o atual; docker-compose com hífen é a versão antiga.YAML in 30 seconds. Structure comes from indentation (spaces, never tabs). key: value is a map; lines starting with - are a list; [a, b] and {x: 1} are the one-line forms; # comments. Values like "8080:8080" are quoted so YAML does not read : as a number. The top-level version: key is obsolete and can be omitted. The docker compose command (with a space) is the current one; docker-compose with a hyphen is the old version.services:
app:
build: . # constroi a imagem do ./Dockerfile
ports:
- "127.0.0.1:8080:8080" # so a sua maquina acessa
environment:
DB_ADDR: db:5432 # o NOME do servico e o host
depends_on:
db:
condition: service_healthy # so inicia quando o db estiver saudavel
networks: [frontend, backend]
restart: unless-stopped
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
mem_limit: 128m
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
db:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
volumes:
- dbdata:/var/lib/postgresql/data
networks: [backend] # sem "ports": ninguem de fora chega aqui
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
networks:
frontend:
backend:
internal: true # sem saida para a internet
volumes:
dbdata:
secrets:
db_password:
file: ./db_password.txt
| CampoField | O que fazWhat it does |
|---|---|
services | Cada entrada é um contêiner (ou vários, com deploy.replicas). O nome da chave (app, db) é o nome DNS na rede.Each entry is a container (or several, with deploy.replicas). The key name (app, db) is the DNS name on the network. |
build / image | build constrói a partir de um Dockerfile; image baixa pronta de um registry. Pode ter os dois (constrói e nomeia).build builds from a Dockerfile; image pulls a ready one from a registry. You can have both (builds and names). |
ports | Publica portas, igual ao -p. Ausente = só acessível por outros serviços da mesma rede.Publishes ports, same as -p. Absent = reachable only by other services on the same network. |
environment / env_file | Variáveis de ambiente. ${VAR} é substituída pelo valor do seu shell ou de um arquivo .env ao lado do compose.Environment variables. ${VAR} is replaced by your shell's value or by a .env file next to the compose file. |
depends_on | Ordem de partida. Sem condition, espera só o contêiner iniciar, não o app estar pronto; com service_healthy, espera o healthcheck passar.Start order. Without condition it waits only for the container to start, not for the app to be ready; with service_healthy it waits for the healthcheck to pass. |
networks | Em quais redes o serviço entra. Sem a chave, o Compose cria uma rede default para o projeto.Which networks the service joins. Without the key, Compose creates a default network for the project. |
volumes | Monta volumes nomeados (declarados no bloco volumes: de baixo) ou pastas do host.Mounts named volumes (declared in the bottom volumes: block) or host folders. |
restart | unless-stopped religa se o processo cair ou o Docker reiniciar, exceto se você parou de propósito.unless-stopped restarts if the process dies or Docker restarts, unless you stopped it on purpose. |
secrets | Monta o arquivo em /run/secrets/NOME dentro do contêiner. A imagem oficial do Postgres lê a senha de POSTGRES_PASSWORD_FILE.Mounts the file at /run/secrets/NAME inside the container. The official Postgres image reads the password from POSTGRES_PASSWORD_FILE. |
docker compose up -d --build # constroi (se preciso) e sobe tudo em segundo plano
docker compose ps # estado e saude de cada servico
docker compose logs -f app # logs de um servico (sem o nome: de todos)
docker compose exec db psql -U postgres # comando dentro de um servico rodando
docker compose config # valida o YAML e mostra o resultado final
docker compose stop # para sem apagar
docker compose down # para e APAGA contêineres e redes (volumes ficam)
docker compose down -v # idem, e apaga os volumes: os dados vao junto
db_password.txt do exemplo é só para o laboratório: ponha-o no .gitignore, e em produção use o gerenciador de segredos da plataforma (AWS Secrets Manager, Docker Swarm secrets, Kubernetes Secrets). Um .env com senhas também fica fora do Git.Do not commit secrets. The example's db_password.txt is for the lab only: put it in .gitignore, and in production use the platform's secret manager (AWS Secrets Manager, Docker Swarm secrets, Kubernetes Secrets). A .env with passwords also stays out of Git.Saúde do contêiner: HEALTHCHECKContainer health: HEALTHCHECK
“Contêiner rodando” só diz que o processo existe. Um app pode estar de pé e travado, sem conexão com o banco ou sem memória. O healthcheck é um comando que o Docker executa de tempos em tempos dentro do contêiner; o código de saída decide o estado: 0 saudável, 1 doente.“Container running” only says the process exists. An app can be up and stuck, with no database connection or out of memory. The healthcheck is a command Docker runs from time to time inside the container; the exit code decides the state: 0 healthy, 1 unhealthy.
# no Dockerfile
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["/server", "healthcheck"]
# ou no Compose (sobrepoe o do Dockerfile)
healthcheck:
test: ["CMD", "/server", "healthcheck"]
interval: 10s
timeout: 3s
start_period: 5s
retries: 3
| ParâmetroParameter | SignificadoMeaning |
|---|---|
interval | De quanto em quanto tempo testar (padrão 30 s).How often to test (default 30 s). |
timeout | Quanto esperar pelo comando antes de contar como falha (padrão 30 s).How long to wait for the command before counting a failure (default 30 s). |
retries | Falhas seguidas até virar unhealthy (padrão 3).Consecutive failures until it becomes unhealthy (default 3). |
start_period | Tempo de graça na partida: falhas aqui não contam. Dê ao app o tempo que ele leva para subir.Grace time at start: failures here do not count. Give the app the time it takes to boot. |
Os estados são starting → healthy ou unhealthy, visíveis em docker ps (coluna STATUS) e em docker inspect:The states are starting → healthy or unhealthy, visible in docker ps (STATUS column) and in docker inspect:
$ docker compose ps
NAME SERVICE STATUS
lab-app-1 app Up 12 seconds (healthy)
lab-db-1 db Up 18 seconds (healthy)
$ docker inspect --format '{{.State.Health.Status}}' lab-app-1
healthy
$ docker inspect --format '{{json .State.Health.Log}}' lab-app-1 # ultimas execucoes e saidas
- O que o Docker faz com um contêiner
unhealthy? Sozinho, nada:restart: unless-stoppedsó reage a um processo que saiu. O estado serve para odepends_on: service_healthy, para balanceadores e para orquestradores (Kubernetes, ECS, Swarm), que trocam a instância doente. Em um host único, alerte sobre ele (veja observabilidade) ou use uma ferramenta que reinicie por saúde.What does Docker do with anunhealthycontainer? By itself, nothing:restart: unless-stoppedonly reacts to a process that exited. The state servesdepends_on: service_healthy, load balancers and orchestrators (Kubernetes, ECS, Swarm), which replace the sick instance. On a single host, alert on it (see observability) or use a tool that restarts on health. - Teste algo que importa, mas barato. Um
/healthque só devolve 200 prova que o servidor HTTP responde. Um que também testa uma consulta simples ao banco prova mais, mas se o banco cair todas as instâncias ficam doentes ao mesmo tempo. Separe liveness (“estou vivo?”) de readiness (“posso receber tráfego?”) quando usar orquestrador.Test something that matters, but cheap. A/healththat only returns 200 proves the HTTP server answers. One that also runs a simple database query proves more, but if the database goes down every instance turns sick at once. Separate liveness (“am I alive?”) from readiness (“can I take traffic?”) when you use an orchestrator. - Imagens sem
curle sem shell (distroless, scratch) não conseguem rodarcurl localhost/health. A saída é o próprio binário ter um modo de checagem, como no exemplo (/server healthcheckfaz a requisição e sai com 0 ou 1). Em Alpine,wget -qO- http://127.0.0.1:8080/health(BusyBox) dispensa instalar o curl.Images with nocurland no shell (distroless, scratch) cannot runcurl localhost/health. The way out is for the binary itself to have a check mode, as in the example (/server healthcheckmakes the request and exits 0 or 1). On Alpine,wget -qO- http://127.0.0.1:8080/health(BusyBox) saves installing curl.
Logs e observabilidadeLogs and observability
A regra do contêiner é simples: o app escreve seus logs na saída padrão (stdout) e de erro (stderr), não em arquivos dentro dele. O Docker captura esses dois fluxos por um driver de log e deixa você ler com docker logs.The container rule is simple: the app writes its logs to standard output (stdout) and standard error (stderr), not to files inside it. Docker captures those two streams through a log driver and lets you read them with docker logs.
docker logs web # tudo
docker logs -f --tail 100 web # ultimas 100 linhas e segue (Ctrl+C sai)
docker logs --since 10m -t web # ultimos 10 min, com data/hora
docker compose logs -f --no-log-prefix app | jq 'select(.path == "/db")' # filtra JSON
- Logue em JSON (uma linha por evento, como no exemplo:
{"msg":"request","method":"GET","path":"/db"}). Texto livre é difícil de filtrar; JSON entra direto emjq, Loki ou Elasticsearch.Log in JSON (one line per event, as in the example:{"msg":"request","method":"GET","path":"/db"}). Free text is hard to filter; JSON feeds straight intojq, Loki or Elasticsearch. - Sem rotação, o log enche o disco. O driver padrão (
json-file) guarda tudo sem limite. Configuremax-sizeemax-file(como no Compose acima) ou global em/etc/docker/daemon.json. O driverlocaljá faz rotação por padrão.Without rotation, logs fill the disk. The default driver (json-file) keeps everything with no limit. Setmax-sizeandmax-file(as in the Compose above) or globally in/etc/docker/daemon.json. Thelocaldriver rotates by default. - Não registre segredos nem dados pessoais: o log é copiado, indexado e guardado por meses. Veja Logging and alerting failures no OWASP Top 10.Do not log secrets or personal data: logs are copied, indexed and kept for months. See Logging and alerting failures in the OWASP Top 10.
- Para centralizar, um coletor (Grafana Alloy, Fluent Bit, Promtail) lê os logs dos contêineres e envia ao Loki/Elasticsearch; alternativamente, um driver como
syslog,fluentdouawslogsenvia direto. Isso é montado passo a passo na nota de observabilidade.To centralise, a collector (Grafana Alloy, Fluent Bit, Promtail) reads the containers' logs and ships them to Loki/Elasticsearch; alternatively a driver such assyslog,fluentdorawslogsships directly. This is built step by step in the observability note.
Além dos logs: o que olhar num contêinerBeyond logs: what to look at in a container
| PerguntaQuestion | FerramentaTool |
|---|---|
| Está de pé e saudável? Quantas vezes reiniciou?Is it up and healthy? How many times has it restarted? | docker ps, docker inspect --format '{{.State.Health.Status}} {{.RestartCount}}' app |
| Por que parou? Foi falta de memória?Why did it stop? Was it out of memory? | docker inspect --format '{{.State.ExitCode}} {{.State.OOMKilled}}' app (137 = SIGKILL; OOMKilled=true = estourou o limite de memóriahit the memory limit) |
| Quanta CPU e memória usa agora?How much CPU and memory does it use now? | docker stats --no-stream |
| Que processos rodam lá dentro e com qual usuário?Which processes run in there and as which user? | docker top app -o user,pid,comm |
| O que aconteceu no Docker (start, die, oom, health_status)?What happened in Docker (start, die, oom, health_status)? | docker events --since 30m |
| Histórico de métricas e alertas?Metrics history and alerts? | cAdvisor expõe CPU, memória, rede e disco por contêiner ao Prometheus; Grafana desenha e alerta (nota de observabilidade)cAdvisor exposes per-container CPU, memory, network and disk to Prometheus; Grafana draws and alerts (observability note) |
Resumindo: docker logs e docker stats resolvem o dia a dia numa máquina; assim que você tem vários contêineres ou hosts, precisa de métricas históricas e logs centralizados, senão só descobre o problema depois que o usuário reclama.In short: docker logs and docker stats cover day-to-day work on one machine; as soon as you have several containers or hosts you need historical metrics and centralised logs, or you only learn about the problem after the user complains.
Praticar na sua máquinaPractise on your machine
Pré-requisito: Docker com Compose v2 (docker version e docker compose version funcionando). Todos os comandos abaixo foram executados nesta ordem para escrever a nota. Cada atividade é pequena e usa a anterior.Prerequisite: Docker with Compose v2 (docker version and docker compose version working). Every command below was run in this order while writing the note. Each activity is small and builds on the previous one.
| # | AtividadeActivity | Você aprendeYou learn |
|---|---|---|
| 0 | Criar os arquivosCreate the files | Estrutura do projetoProject layout |
| 1 | Build ingênuo × multi-stageNaive × multi-stage build | Camadas, tamanho, docker imagesLayers, size, docker images |
| 2 | Rodar, mapear porta, ler logsRun, map a port, read logs | run, -p, logs, stats |
| 3 | Tentar abrir um terminalTry to open a terminal | Superfície de ataque reduzidaReduced attack surface |
| 4 | Subir app + banco com ComposeStart app + database with Compose | Redes, DNS por nome, healthcheckNetworks, DNS by name, healthcheck |
| 5 | Provar o isolamento da redeProve the network isolation | internal, porta não publicadaunpublished port |
| 6 | Derrubar o banco e observarTake the database down and observe | Logs JSON, falha de dependênciaJSON logs, dependency failure |
| 7 | Limpar tudoClean up | down -v, prune |
Atividade 0: criar os arquivosActivity 0: create the files
mkdir lab && cd lab
# arquivos: main.go go.mod .dockerignore Dockerfile compose.yaml db_password.txt
echo "troque-esta-senha" > db_password.txt
printf 'db_password.txt\n' > .gitignore
main.go (o serviço de exemplo; a rota /db só abre uma conexão TCP com db:5432 para provar a rede, sem driver de banco):main.go (the example service; the /db route just opens a TCP connection to db:5432 to prove the network, no database driver):
package main
import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"time"
)
func main() {
port := getenv("PORT", "8080")
// Modo healthcheck: o proprio binario testa /health e sai com 0 ou 1.
if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
c := http.Client{Timeout: 2 * time.Second}
res, err := c.Get("http://127.0.0.1:" + port + "/health")
if err != nil || res.StatusCode != 200 {
os.Exit(1)
}
return
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
host, _ := os.Hostname()
fmt.Fprintf(w, "ola do container %s\n", host)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
// Prova a comunicacao entre containers: abre TCP para o banco pelo NOME do servico.
http.HandleFunc("/db", func(w http.ResponseWriter, r *http.Request) {
addr := getenv("DB_ADDR", "db:5432")
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
http.Error(w, "db inacessivel: "+err.Error(), http.StatusBadGateway)
return
}
conn.Close()
fmt.Fprintf(w, "db alcancavel em %s\n", addr)
})
logJSON("start", map[string]any{"port": port})
log.Fatal(http.ListenAndServe(":"+port, logged(http.DefaultServeMux)))
}
func logged(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" { // o healthcheck roda a cada 10 s; nao polui o log
logJSON("request", map[string]any{"method": r.Method, "path": r.URL.Path})
}
next.ServeHTTP(w, r)
})
}
func logJSON(msg string, fields map[string]any) {
fields["msg"], fields["time"] = msg, time.Now().UTC().Format(time.RFC3339)
b, _ := json.Marshal(fields)
fmt.Println(string(b))
}
func getenv(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
# go.mod
module hello
go 1.25
O .dockerignore é o da seção do Dockerfile; o Dockerfile é o do multi-stage e o compose.yaml é o da seção do Compose. Para o contraste da próxima atividade, salve também o Dockerfile ingênuo como Dockerfile.naive.The .dockerignore is the one from the Dockerfile section; the Dockerfile is the multi-stage one and compose.yaml is the one from the Compose section. For the next activity's contrast, also save the naive Dockerfile as Dockerfile.naive.
Atividade 1: build ingênuo × multi-stageActivity 1: naive × multi-stage build
docker build -f Dockerfile.naive -t lab:naive .
docker build -t lab:final .
docker images lab
docker history lab:final # poucas camadas, nenhuma com compilador
Resultado esperado: lab:naive com centenas de megabytes (452 MB nesta máquina) e lab:final com cerca de 15 MB. Rode o build de novo sem mudar nada e veja as linhas CACHED; depois mude uma linha do main.go e note que o go mod download continua em cache.Expected result: lab:naive at hundreds of megabytes (452 MB on this machine) and lab:final at about 15 MB. Run the build again without changes and watch the CACHED lines; then change one line of main.go and notice that go mod download stays cached.
Atividade 2: rodar, mapear porta, ler logsActivity 2: run, map a port, read logs
docker run -d --name app -p 127.0.0.1:8080:8080 lab:final
curl localhost:8080/ # ola do container 3f2a...
curl localhost:8080/db # 502: nao existe "db" nesta rede (ainda)
docker ps # STATUS: Up ... (healthy)
docker logs app # linhas JSON: start, request, request
docker stats --no-stream app # poucos MB de memoria
docker stop app && docker rm app
Troque o mapeamento para -p 3000:8080 e acesse localhost:3000; depois use -e PORT=9090 -p 9090:9090 e veja que a porta do contêiner acompanha a variável.Change the mapping to -p 3000:8080 and reach localhost:3000; then use -e PORT=9090 -p 9090:9090 and see that the container port follows the variable.
Atividade 3: tentar abrir um terminalActivity 3: try to open a terminal
docker run -d --name app lab:final
docker exec -it app sh # falha: "sh": executable file not found
docker exec app id # falha tambem: nao ha nem o comando id
docker inspect app --format '{{.Config.User}}' # 65532 (nao e root)
docker top app -o user,pid,comm # 65532 ... server
docker rm -f app
# compare com uma base que tem tudo:
docker run --rm -it ubuntu:24.04 bash # aqui voce teria shell, apt, e seria root
whoami # root
Resultado esperado: na imagem final não há shell, gerenciador de pacotes nem root. Quem invadir o app encontra um único binário.Expected result: the final image has no shell, no package manager and no root. Whoever breaks into the app finds a single binary.
Atividade 4: app + banco com ComposeActivity 4: app + database with Compose
docker compose up -d --build
docker compose ps # db healthy primeiro, depois app healthy
curl localhost:8080/db # db alcancavel em db:5432
docker compose logs app # o app resolveu "db" pelo DNS do Docker
docker inspect lab-app-1 --format 'user={{.Config.User}} ro={{.HostConfig.ReadonlyRootfs}} caps={{.HostConfig.CapDrop}}'
# user=65532 ro=true caps=[ALL]
Repare na ordem: o Compose esperou o Postgres ficar healthy antes de iniciar o app (service_healthy). Sem isso, o app subiria antes do banco aceitar conexões.Notice the order: Compose waited for Postgres to be healthy before starting the app (service_healthy). Without it the app would start before the database accepts connections.
Atividade 5: provar o isolamento da redeActivity 5: prove the network isolation
# 1) o banco NAO tem porta publicada: PORTS mostra so "5432/tcp", sem "0.0.0.0:"
docker compose ps db
ss -ltn | grep 5432 # nada escutando no host
# 2) a rede backend e interna: quem esta so nela nao sai para a internet
docker run --rm --network lab_backend alpine:3.22 wget -qO- -T2 http://example.com
# wget: bad address 'example.com'
# 3) mas dentro da rede os nomes resolvem
docker run --rm --network lab_backend alpine:3.22 wget -qO- -T2 http://app:8080/db
# db alcancavel em db:5432
O nome da rede é <pasta>_<rede>: se sua pasta não se chama lab, veja o nome real em docker network ls.The network name is <folder>_<network>: if your folder is not called lab, see the real name in docker network ls.
Atividade 6: derrubar o banco e observarActivity 6: take the database down and observe
docker compose stop db
curl -i localhost:8080/db # 502 Bad Gateway: db inacessivel
docker compose logs --no-log-prefix app | jq . # cada requisicao como JSON
docker compose start db
curl localhost:8080/db # volta a responder
docker events --since 2m --filter type=container # start, die, health_status...
Desafio: coloque healthcheck no app que também chama /db (um modo healthcheck que testa o banco) e veja o estado do app virar unhealthy quando o banco cai. Depois discuta: isso é desejável? (Dica: leia o texto sobre liveness e readiness.)Challenge: give the app a healthcheck that also calls /db (a healthcheck mode that tests the database) and watch the app turn unhealthy when the database goes down. Then discuss: is that desirable? (Hint: re-read the liveness and readiness text.)
Atividade 7: limpar tudoActivity 7: clean up
docker compose down -v # contêineres, redes e o volume dbdata
docker rmi lab:naive lab:final
docker system df # confira o que sobrou
docker system prune # apaga o que esta parado e sem uso
Para ir alémGoing further
- Escaneie a imagem:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image lab:final(oudocker scout cves lab:final) e compare o número de vulnerabilidades com a variante Ubuntu. Note que aqui você montou o socket de propósito, num contêiner descartável de ferramenta confiável; é o tipo de exceção que deve ser consciente.Scan the image:docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image lab:final(ordocker scout cves lab:final) and compare the number of vulnerabilities with the Ubuntu variant. Note that you mounted the socket on purpose here, in a throwaway container of a trusted tool; that is the kind of exception that should be a conscious one. - Reverse proxy: adicione um serviço
caddyounginxna redefrontend, publique só a porta dele e remova oportsdo app.Reverse proxy: add acaddyornginxservice on thefrontendnetwork, publish only its port and remove the app'sports. - Tag por digest: troque o
FROMporgcr.io/distroless/static-debian12:nonroot@sha256:…(veja o digest comdocker buildx imagetools inspect) e entenda por que isso torna o build reproduzível.Pin by digest: change theFROMtogcr.io/distroless/static-debian12:nonroot@sha256:…(see the digest withdocker buildx imagetools inspect) and understand why that makes the build reproducible. - Observabilidade: acrescente Prometheus, Grafana e Loki como na nota de observabilidade e leve os logs JSON deste app para o Grafana.Observability: add Prometheus, Grafana and Loki as in the observability note and ship this app's JSON logs to Grafana.
- Para produção em mais de uma máquina: publique a imagem num registry e rode no ECS ou no Kubernetes; os conceitos de imagem, porta, healthcheck e variáveis são os mesmos.For production on more than one machine: push the image to a registry and run it on ECS or Kubernetes; the concepts of image, port, healthcheck and variables are the same.
Quando algo dá erradoWhen something goes wrong
| SintomaSymptom | Causa comumCommon cause | Como investigarHow to investigate |
|---|---|---|
| Contêiner sai logo ao iniciarContainer exits right after start | O processo principal terminou ou falhou (config faltando, porta ocupada, comando errado)The main process finished or failed (missing config, busy port, wrong command) | docker ps -a, docker logs, docker inspect --format '{{.State.ExitCode}}' |
| Código de saída 137Exit code 137 | Morto por SIGKILL: falta de memória (OOMKilled) ou docker stop que estourou o prazoKilled by SIGKILL: out of memory (OOMKilled) or a docker stop that hit the timeout | docker inspect --format '{{.State.OOMKilled}}'; aumente --memory ou trate o SIGTERMraise --memory or handle SIGTERM |
“Connection refused” no localhost:8080“Connection refused” on localhost:8080 | Faltou o -p, ou o app escuta em 127.0.0.1 dentro do contêinerMissing -p, or the app listens on 127.0.0.1 inside the container | docker port app; configure o app para 0.0.0.0set the app to 0.0.0.0 |
App não acha o banco (no such host, connection refused)App cannot find the database (no such host, connection refused) | Usou localhost; contêineres em redes diferentes; banco ainda subindoUsed localhost; containers on different networks; database still starting | docker network inspect; use o nome do serviço; adicione depends_on com service_healthyuse the service name; add depends_on with service_healthy |
Fica starting ou vira unhealthyStays starting or turns unhealthy | O comando do healthcheck usa uma ferramenta que não existe na imagem (curl), ou o app demora mais que o start_periodThe healthcheck command uses a tool missing from the image (curl), or the app takes longer than start_period | docker inspect --format '{{json .State.Health.Log}}' |
| Dados sumiram depois de recriarData vanished after recreating | Gravava na camada do contêiner, sem volume; ou rodou down -vWas writing to the container layer, with no volume; or ran down -v | docker volume ls, docker inspect (seção Mounts)(Mounts section) |
| Build lento ou sempre refaz tudoBuild slow or always redoes everything | COPY . . antes das dependências; falta .dockerignoreCOPY . . before the dependencies; no .dockerignore | Reordene as linhas; veja o que o docker build marca como CACHEDReorder the lines; see what docker build marks as CACHED |
| Disco cheioDisk full | Logs sem rotação, imagens e volumes antigosLogs with no rotation, old images and volumes | docker system df, docker system prune; configure max-sizeset max-size |
Cola e revisãoCheat sheet and review
ColaCheat sheet
| AssuntoTopic | RegraRule |
|---|---|
| Imagem × contêinerImage × container | Imagem é imutável; contêiner é descartável. O que importa vai em volume.The image is immutable; the container is disposable. What matters goes in a volume. |
| Dockerfile | O que muda menos vem primeiro; tag fixa; exec form; .dockerignore; sem segredos.What changes least goes first; fixed tag; exec form; .dockerignore; no secrets. |
| BaseBase | Comece em -slim ou alpine; Ubuntu quando precisar do apt; distroless/scratch no final.Start from -slim or alpine; Ubuntu when you need apt; distroless/scratch at the end. |
| Multi-stage | Estágio 1 compila, o último recebe só o artefato com COPY --from.Stage 1 compiles, the last one receives only the artifact via COPY --from. |
| Superfície de ataqueAttack surface | Sem shell, não-root, --read-only, --cap-drop ALL, limites, escaneamento. Nunca --privileged nem docker.sock.No shell, non-root, --read-only, --cap-drop ALL, limits, scanning. Never --privileged or docker.sock. |
| PortasPorts | -p HOST:CONTÊINER; EXPOSE só documenta; 127.0.0.1: para uso local; o app escuta em 0.0.0.0.-p HOST:CONTAINER; EXPOSE only documents; 127.0.0.1: for local use; the app listens on 0.0.0.0. |
| RedesNetworks | Rede criada por você + DNS por nome; localhost é o próprio contêiner; internal para o que não sai.A network you create + DNS by name; localhost is the container itself; internal for what must not go out. |
| Compose | up -d --build, ps, logs -f, down; down -v apaga dados.up -d --build, ps, logs -f, down; down -v deletes data. |
| SaúdeHealth | HEALTHCHECK + start_period; Docker sozinho não reinicia unhealthy; use service_healthy.HEALTHCHECK + start_period; Docker alone does not restart unhealthy; use service_healthy. |
| Logs | stdout/stderr, em JSON, com max-size; docker logs -f; coletor para centralizar.stdout/stderr, in JSON, with max-size; docker logs -f; a collector to centralise. |
Perguntas de revisãoReview questions
- Por que um contêiner sobe em milissegundos e uma VM não? O que eles compartilham com o host?Why does a container start in milliseconds and a VM does not? What do they share with the host?
- Apagar o contêiner apaga a imagem? E os dados gravados dentro dele? Onde você os guardaria?Does deleting the container delete the image? And the data written inside it? Where would you keep it?
- Por que
CMD ["/server"]é melhor queCMD /serverpara odocker stop?Why isCMD ["/server"]better thanCMD /serverfordocker stop? - Quando você escolheria Alpine,
-slim, Ubuntu e distroless? Dê um exemplo de dependência que quebra no Alpine.When would you choose Alpine,-slim, Ubuntu and distroless? Give an example of a dependency that breaks on Alpine. - Por que apagar um arquivo em um
RUNposterior não reduz a imagem? Como o multi-stage resolve isso?Why does deleting a file in a laterRUNnot shrink the image? How does multi-stage solve it? - Liste cinco medidas que reduzem a superfície de ataque de um contêiner e o que cada uma impede.List five measures that reduce a container's attack surface and what each prevents.
- O que
-p 5433:5432faz? Que porta um outro contêiner usa para falar com esse banco? E por que publicar o banco pode abrir a internet mesmo com o UFW ativo?What does-p 5433:5432do? Which port does another container use to talk to that database? And why can publishing the database open it to the internet even with UFW enabled? - Por que
DB_HOST=localhostfalha dentro de um contêiner? O que usar no lugar?Why doesDB_HOST=localhostfail inside a container? What to use instead? - O que o Docker faz quando um contêiner fica
unhealthy? E o que muda num orquestrador?What does Docker do when a container turnsunhealthy? And what changes in an orchestrator? - Seu contêiner morreu com código 137. Quais são as duas causas prováveis e como diferenciá-las?Your container died with exit code 137. What are the two likely causes and how do you tell them apart?
- Docker docs: What is Docker? · Dockerfile reference · Building best practices · Multi-stage builds
- Running containers (docker run) · Networking overview · Port publishing and mapping · Packet filtering and firewalls
- Compose file reference · Control startup order · Secrets in Compose
- Configure logging drivers · docker stats · cAdvisor
- OWASP Docker Security Cheat Sheet · CIS Docker Benchmark · Docker Engine security
- Distroless images · Alpine on Docker Hub · Trivy · Docker Scout · hadolint · netshoot