Conteúdo

Tutorial de instruções do Dockerfile: guia completo de FROM, RUN, COPY, CMD e ENTRYPOINT

Na seção anterior, você criou seu primeiro Dockerfile com um exemplo simples. Nesta seção, vamos explicar cada instrução e comparar, com pequenos experimentos, alguns pares de instruções que costumam ser confundidos.

Regras básicas

  • As instruções não diferenciam maiúsculas de minúsculas, mas é comum escrevê-las em maiúsculas para distingui-las dos parâmetros.
  • Elas são executadas em sequência, de cima para baixo. A primeira instrução (exceto ARG e comentários) precisa ser FROM.
  • Linhas iniciadas com # são comentários.
  • Se uma linha ficar muito longa, você pode quebrá-la usando \.
As instruções do Dockerfile se dividem entre as que entram em vigor durante o build e durante a execução, além das formas de combinar ENTRYPOINT e CMD
Instruções que entram em vigor durante o build (docker build) e durante a execução (docker run), e a combinação de ENTRYPOINT + CMD

FROM: imagem base

FROM python:3.14-slim
FROM --platform=linux/amd64 node:24-alpine
FROM scratch                     # Imagem totalmente vazia, usada para programas compilados estaticamente
FROM golang:1.27 AS builder      # Dá um nome à etapa em um build em vários estágios

RUN: executar comandos durante o build

# Formato shell: executa por meio de /bin/sh -c; você pode usar && e pipes
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

# Formato exec: não passa pelo shell
RUN ["pip", "install", "--no-cache-dir", "flask"]

Cada RUN cria uma camada. Escreva apt-get update e apt-get install no mesmo RUN. Caso contrário, a camada de update em cache pode ficar desatualizada e instalar pacotes antigos. Limpar a lista de pacotes no final (rm -rf /var/lib/apt/lists/*) reduz o tamanho da imagem. Veja a diferença na prática em Cache de build e otimização.

COPY e ADD: copiar arquivos

COPY package.json package-lock.json ./
COPY src/ ./src/
COPY --chown=app:app . .
COPY --from=builder /app/dist ./dist     # Copia arquivos de outra etapa do build

ADD é parecido com COPY, mas se comporta de outra forma com arquivos tar compactados. Experimente: prepare um files.tar.gz (com files/a.txt e files/b.txt) e copie-o usando cada instrução:

FROM alpine:3.23
COPY files.tar.gz /copy/
ADD files.tar.gz /add/
CMD ["sh", "-c", "echo '--- COPY:'; ls -R /copy; echo '--- ADD:'; ls -R /add"]
$ docker build -q -t add-demo .
sha256:2466246e51b76cd547fa913fce223fccc227ae19b9e53006b9ce203e1afc8ec4

$ docker run --rm add-demo
--- COPY:
/copy:
files.tar.gz
--- ADD:
/add:
files

/add/files:
a.txt
b.txt
  • COPY copiou files.tar.gz sem alterações.
  • ADD descompactou automaticamente o arquivo. Assim, /add contém diretamente files/a.txt e files/b.txt.

A origem de ADD também pode ser uma URL (que será baixada automaticamente). Esses comportamentos automáticos podem causar surpresas. A recomendação oficial é usar COPY em situações comuns e recorrer a ADD apenas quando precisar descompactar arquivos.

WORKDIR: diretório de trabalho

WORKDIR /app

As instruções seguintes, como RUN, COPY e CMD, serão executadas nesse diretório. Se ele não existir, será criado automaticamente. Use WORKDIR em vez de escrever RUN cd /app: cada RUN usa um shell independente, então o cd não continua na linha seguinte.

ENV e ARG: variáveis

ARGENV
Quando entra em vigorApenas durante o buildDurante o build e a execução do contêiner
Como definirdocker build --build-arg NAME=valorNo Dockerfile ou com docker run -e
Fica visível dentro do contêiner?NãoSim

Experimento:

FROM alpine:3.23
ARG APP_VERSION=dev
ENV APP_ENV=production
RUN echo "building version $APP_VERSION" > /build-info.txt
CMD ["sh", "-c", "cat /build-info.txt; echo APP_ENV=$APP_ENV; echo APP_VERSION=${APP_VERSION:-<not set>}"]
$ docker build -q --build-arg APP_VERSION=1.2.0 -t args-demo .
sha256:3537c3e70baef148c3481f97bf325963b4809a15b806a2e38fed7e8eb5f50619

$ docker run --rm args-demo
building version 1.2.0
APP_ENV=production
APP_VERSION=<not set>

$ docker history --no-trunc --format "{{.CreatedBy}}" args-demo | head -n 3
CMD ["sh" "-c" "cat /build-info.txt; echo APP_ENV=$APP_ENV; echo APP_VERSION=${APP_VERSION:-<not set>}"]
RUN |1 APP_VERSION=1.2.0 /bin/sh -c echo "building version $APP_VERSION" > /build-info.txt # buildkit
ENV APP_ENV=production
  • O valor APP_VERSION=1.2.0 informado durante o build foi usado por RUN (e gravado em /build-info.txt), mas já não fica visível durante a execução do contêiner (<not set>).
  • O APP_ENV definido por ENV fica visível dentro do contêiner.
  • Observe a segunda linha de docker history: RUN |1 APP_VERSION=1.2.0 .... O valor de ARG fica registrado no histórico da imagem e qualquer pessoa que obtenha a imagem poderá vê-lo.

Portanto, nunca use ARG ou ENV para transmitir senhas ou chaves de API. Para usar dados confidenciais durante o build, use RUN --mount=type=secret. Consulte Variáveis de ambiente e arquivos .env.

Um ARG declarado antes de FROM só pode ser usado em FROM. Para usá-lo depois de FROM, declare novamente ARG nome.

EXPOSE: documentar portas

EXPOSE 3000
EXPOSE 53/udp

EXPOSE serve apenas para documentação: informa qual porta o contêiner usará, mas não a publica. Para mapear a porta de fato, você ainda precisa usar docker run -p. Seu único efeito prático é em conjunto com -P, que mapeia todas as portas de EXPOSE para portas aleatórias do host (a imagem nginx tem EXPOSE 80):

$ docker run --rm -d --name exp -P nginx:1.30
857560427b1218d038314394f03cc0eb8edf950062304efaf2f5f3dc995f6ef6

$ docker port exp
80/tcp -> 0.0.0.0:32768
80/tcp -> [::]:32768

$ docker rm -f exp
exp

USER: execute como usuário não root

Por padrão, o contêiner executa como root. Use um usuário comum para limitar os danos caso o programa seja comprometido:

FROM alpine:3.23
RUN addgroup -S app && adduser -S -G app app
USER app
CMD ["sh", "-c", "whoami; id"]
$ docker build -q -t user-demo .
sha256:bdcaab4c6133a5446cdaaa1ca3353e01a690dc9fd0647458543abcec0d6041f1

$ docker run --rm user-demo
app
uid=100(app) gid=101(app) groups=101(app),101(app)

$ docker run --rm alpine:3.23 whoami
root

Muitas imagens oficiais já incluem um usuário comum. Por exemplo, a imagem node tem o usuário node, então você pode usar USER node diretamente.

VOLUME: declarar um diretório de dados

VOLUME /var/lib/data

Declare que esse diretório deve armazenar dados persistentes. Se você não montar um volume ao executar o contêiner, o Docker criará automaticamente um Volume anônimo (a seção Volume traz um exemplo prático). Na prática, geralmente você especifica isso diretamente com docker run -v ou no Compose; talvez não seja necessário incluir VOLUME no seu Dockerfile.

HEALTHCHECK: verificação de integridade

O Docker executa periodicamente o comando de verificação de integridade. O código de saída 0 indica que está saudável; um valor diferente de 0 indica que não está saudável:

FROM nginx:1.30-alpine
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD wget -q --spider http://localhost/ || exit 1

Experimente: depois de iniciar, aguarde alguns segundos e então exclua a página inicial de propósito para que o nginx retorne um erro:

$ docker run -d --name health health-demo
52bfa4cce8024630797ba6f33ba99dea44f80399fbadd617b2d04a8ac7f8e3b1

$ docker ps --filter name=health
CONTAINER ID   IMAGE         COMMAND                  CREATED        STATUS                                     PORTS     NAMES
52bfa4cce802   health-demo   "/docker-entrypoint.…"   1 second ago   Up Less than a second (health: starting)   80/tcp    health

$ docker ps --filter name=health
CONTAINER ID   IMAGE         COMMAND                  CREATED         STATUS                   PORTS     NAMES
52bfa4cce802   health-demo   "/docker-entrypoint.…"   9 seconds ago   Up 8 seconds (healthy)   80/tcp    health

$ docker exec health rm /usr/share/nginx/html/index.html

$ docker ps --filter name=health
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS                      PORTS     NAMES
52bfa4cce802   health-demo   "/docker-entrypoint.…"   29 seconds ago   Up 28 seconds (unhealthy)   80/tcp    health

$ docker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{println}}{{end}}' health
0
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden

$ docker rm -f health
health

A sequência de estados: health: starting (acabou de iniciar) → healthy (verificação aprovada) → após excluir a página inicial, 3 falhas consecutivas (--retries=3) → unhealthy. .State.Health.Log registra o código de saída e a saída de cada verificação.

  • A imagem precisa incluir ferramentas como curl ou wget para que você possa fazer isso (wget já vem no alpine).
  • O Compose pode aguardar até que um serviço fique healthy para iniciar os serviços que dependem dele. Consulte a sintaxe de compose.yaml.

CMD e ENTRYPOINT: comando de inicialização

Ambos determinam o que será executado quando o contêiner iniciar. A diferença está em se podem ser substituídos e como são combinados.

CMD: pode ser substituído por completo

FROM alpine:3.23
CMD ["echo", "hello from CMD"]
$ docker build -q -t cmd-demo .
sha256:45b2af259ce828e227961bcc620b973758a1f757d948a7e23e056781afba324d

$ docker run --rm cmd-demo
hello from CMD

$ docker run --rm cmd-demo echo overridden
overridden

O comando que você escreve depois do nome da imagem ao executar docker run substitui por completo CMD.

ENTRYPOINT: programa principal fixo

FROM alpine:3.23
ENTRYPOINT ["echo", "ENTRYPOINT says:"]
CMD ["default argument"]
$ docker build -q -t ep-demo .
sha256:32414489000b71c255d808cfd4cda73764b526525229dece3a8f1fd5487fc6d1

$ docker run --rm ep-demo
ENTRYPOINT says: default argument

$ docker run --rm ep-demo my argument
ENTRYPOINT says: my argument

$ docker run --rm --entrypoint date ep-demo
Mon Oct 12 23:59:17 UTC 2026
  • Quando ENTRYPOINT e CMD são usados juntos, o que é executado é ENTRYPOINT + CMD.
  • O que você escreve depois de docker run substitui apenas CMD e passa a ser argumento de ENTRYPOINT.
  • Para substituir o próprio ENTRYPOINT, use --entrypoint.
CMDENTRYPOINT
FunçãoComando padrão ou argumentos padrãoExecutável principal do contêiner
docker run imagem xxxxxx substitui CMDxxx é acrescentado após ENTRYPOINT
Como substituirEscreva o comando diretamente depois do nome da imagem--entrypoint

Como escolher:

  • Aplicativo comum: escreva apenas CMD. Assim, quando quiser abrir um shell, o usuário pode executar diretamente docker run -it myapp sh.
  • Para usar o contêiner como “um comando” (por exemplo, uma ferramenta de CLI): use ENTRYPOINT para especificar o programa e coloque os argumentos padrão em CMD.

Formato exec e formato shell

CMD ["node", "server.js"]    # formato exec (array JSON)
CMD node server.js           # formato shell

O formato shell é executado como /bin/sh -c "node server.js" e permite a expansão de variáveis de ambiente e sintaxe de shell como &&. Já o formato exec executa o programa diretamente, sem passar pelo shell. Em geral, recomendamos o formato exec, pois seu comportamento é mais explícito.

PID 1 e o problema dos 10 segundos de espera do docker stop

O ciclo de vida do contêiner mencionou que, às vezes, docker stop leva 10 segundos. A seguir, vamos usar o mesmo programa Node.js (server.js da seção anterior, sem tratamento de SIGTERM) para comparar quatro formas de escrever CMD, ver os processos dentro do contêiner com docker top e cronometrar docker stop:

$ docker top shell-npm
PID                 USER                TIME                COMMAND
4155                root                0:00                npm start
4182                root                0:00                {MainThread} node server.js

$ time docker stop shell-npm
shell-npm
real	0m 0.66s
user	0m 0.00s
sys	0m 0.00s

$ docker top exec-npm
PID                 USER                TIME                COMMAND
4318                root                0:00                npm start
4347                root                0:00                {MainThread} node server.js

$ time docker stop exec-npm
exec-npm
real	0m 0.67s
user	0m 0.00s
sys	0m 0.00s

$ docker top shell-node
PID                 USER                TIME                COMMAND
4488                root                0:00                {MainThread} node server.js

$ time docker stop shell-node
shell-node
real	0m 10.14s
user	0m 0.01s
sys	0m 0.01s

$ docker top exec-node
PID                 USER                TIME                COMMAND
4660                root                0:00                {MainThread} node server.js

$ time docker stop exec-node
exec-node
real	0m 10.14s
user	0m 0.01s
sys	0m 0.00s
Forma de escrever CMDPID 1 dentro do contêinerdocker stop
CMD npm startnpm0.66 segundo
CMD ["npm", "start"]npm0.67 segundo
CMD node server.jsnode10.14 segundos
CMD ["node", "server.js"]node10.14 segundos

O ponto principal não é o formato shell ou exec, mas quem é o PID 1:

  • O Linux tem uma regra especial para o PID 1: se o PID 1 não tratar SIGTERM por conta própria, o sinal será ignorado. Quando node é diretamente o PID 1 e o programa não trata SIGTERM, docker stop só consegue aguardar 10 segundos e então encerrar o processo à força com SIGKILL.
  • Quando iniciado pelo npm, o npm é o PID 1 e encaminha SIGTERM para node. Como node não é o PID 1, ao receber SIGTERM ele termina conforme o comportamento padrão.
  • A primeira linha usa o formato shell sem adicionar outra camada sh porque o sh do Alpine substitui a si próprio por npm quando há apenas um comando.

Soluções (escolha uma):

  1. Trate SIGTERM no programa e encerre o servidor de forma graciosa (a melhor opção; veja Aplicativo Node.js).
  2. Adicione --init ao executar (no Compose, use init: true); assim, um pequeno processo init fica como PID 1 e encaminha os sinais.

Outros comandos

ComandoFinalidade
LABELAdicionar metadados, por exemplo, LABEL org.opencontainers.image.source="https://github.com/..."
SHELLAlterar o shell usado pelo formato shell, por exemplo, usar PowerShell em contêineres Windows
STOPSIGNALAlterar o sinal enviado por docker stop (a imagem nginx define SIGQUIT; veja docker history em Imagens)
ONBUILDExecutar um comando apenas quando outra imagem usar esta imagem como base (pouco usado)

Exemplo completo: site em Python

FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd --system app
USER app

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

PYTHONUNBUFFERED=1 faz com que a saída do Python seja gravada imediatamente, para que você possa vê-la em tempo real com docker logs. Veja o processo real de build e execução deste Dockerfile em Implante um aplicativo Python com Docker.

Resumo

  • RUN, COPY e ARG têm efeito durante o build; CMD, ENTRYPOINT, USER e ENV afetam a execução.
  • Em docker run imagem xxx, xxx substitui CMD ou é acrescentado depois de ENTRYPOINT.
  • Use o formato exec (array JSON) em CMD e ENTRYPOINT.
  • Não use ARG nem ENV para passar senhas.

Exercícios

Exercício 1: A imagem define ENTRYPOINT ["ping"] e CMD ["-c", "3", "localhost"]. O que é executado de fato ao executar docker run imagem -c 1 example.com?

Resposta

ping -c 1 example.com. Os argumentos informados na execução substituem CMD e são acrescentados depois de ENTRYPOINT.

Exercício 2: Por que ENV NODE_ENV production gera um aviso de docker build --check?

Resposta

Essa é a forma antiga; o correto é escrever ENV NODE_ENV=production. O nome do aviso é LegacyKeyValueFormat.

Perguntas frequentes

Quando devo usar COPY ou ADD?

Prefira sempre COPY. ADD descompacta arquivos tar automaticamente e pode baixar URLs. Esse comportamento é menos intuitivo, então use ADD apenas quando precisar desses recursos.

Quando devo usar CMD ou ENTRYPOINT?

A maioria dos aplicativos usa apenas CMD, que é fácil de substituir. Use ENTRYPOINT quando a imagem funcionar como uma ferramenta de comando (sempre executa o mesmo programa, mudando apenas os argumentos).

Como verifico se a sintaxe do Dockerfile está correta?

Execute docker build --check .. O comando lista avisos como JSONArgsRecommended e LegacyKeyValueFormat. Consulte [docker init](/pt/docker/docker-init/).

Atualizado 14 de outubro de 2026