SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Docker Compose: build ou image em um VPS

Entenda por que image baixa uma tag e build cria localmente, por que compose up ignora mudanças no Dockerfile e qual comando corrige isso.

Docker Compose: build vs image — a resposta curta

Num ficheiro Docker Compose, image: indica uma imagem a obter de um registry, enquanto build: instrui o Compose a criar uma imagem nesta máquina a partir de um Dockerfile. Defina apenas image: e o Compose obtém essa tag e executa-a. Defina apenas build: e o Compose cria a imagem localmente, atribuindo-lhe um nome derivado do nome do projeto e do nome do serviço. Defina ambos e o Compose cria a imagem localmente e atribui-lhe a tag com o nome indicado em image:. É assim que se cria uma imagem e se publica com um nome escolhido por si.

Essa é toda a distinção. O restante explica o que isso significa na operação de um servidor. Isto pressupõe que o Docker Engine e o plugin Compose já estão instalados; executar Docker num VPS aborda essa parte.

As três formas completas

Extraia uma tag publicada e execute-a. Não há qualquer Dockerfile envolvido.

services:
  web:
    image: nginx:1.27
    restart: unless-stopped
    ports:
      - "80:80"

Compile a partir de um Dockerfile no diretório atual. Nada é extraído, exceto a imagem base indicada em FROM.

services:
  web:
    build: .
    restart: unless-stopped
    ports:
      - "80:80"

Compile localmente e atribua uma tag ao resultado. docker compose push pode então enviar essa tag exata para um registry.

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    image: registry.example.com/acme/web:1.4.2
    restart: unless-stopped
    ports:
      - "80:80"

context é o diretório enviado para o builder. dockerfile é resolvido relativamente a esse contexto, portanto context: . com dockerfile: docker/prod.Dockerfile é normal e correto. Execute docker compose images para ver o nome da imagem e o ID da imagem por trás de cada contentor de serviço. Esta é a forma mais rápida de confirmar qual destas três formas escreveu efetivamente.

Por que docker compose up não recria a imagem depois de eu alterar o Dockerfile?

Porque up verifica se a imagem existe, não se está atualizada.

Quando o Compose inicia um serviço que tem uma secção build:, procura a imagem no armazenamento local de imagens. Se já existir uma imagem com esse nome, o Compose utiliza-a. Não lê o Dockerfile, não compara os ficheiros de origem nem verifica qualquer data de modificação. A especificação do Compose define esta regra através do atributo pull_policy, e o comportamento predefinido cria uma imagem apenas quando ela não existe. Uma imagem existente é considerada suficiente.

Assim, altera app.py, executa docker compose up -d, vê o Compose indicar que o contentor está em execução e continua a servir o código antigo. Nada falhou, por isso não foi apresentada nenhuma mensagem de aviso. Este é o caso mais comum de “a minha alteração não teve efeito” no Compose. A indicação está na palavra de estado que o Compose apresenta junto ao nome do contentor: um contentor que o Compose substituiu aparece como recriado ou iniciado, enquanto um contentor que o Compose decidiu manter aparece como em execução.

Duas verificações permitem confirmar o problema. docker compose images apresenta o ID da imagem utilizada por cada contentor; anote-o antes da implementação e compare-o depois. docker image ls inclui uma coluna CREATED, e uma imagem criada antes do seu último commit é uma imagem desatualizada, independentemente do que o script de implementação apresentou.

Que flags forçam uma reconstrução

  • docker compose up -d --build faz primeiro o build e, em seguida, recria qualquer container cuja imagem tenha mudado. Este é o flag que a maioria das pessoas procura.
  • docker compose build web faz o build de um serviço e não inicia nada. Use-o com docker compose up --no-deps -d web para substituir apenas esse container e manter o restante da stack em execução.
  • docker compose build --no-cache web descarta todas as camadas em cache e faz o build desde a primeira instrução.
  • docker compose build --pull tenta obter uma versão mais recente da imagem base em FROM. Assim, uma tag mutável como node:22 recebe o conteúdo atual, em vez da cópia que foi descarregada em março.
  • docker compose up -d --force-recreate recria containers a partir da imagem que já utilizam. Nunca faz build. Usar este flag quando a intenção era usar --build é um erro comum.

Também pode transferir essa decisão para o ficheiro. Segundo a especificação do Compose, pull_policy: build significa que o Compose faz o build da imagem e volta a fazê-lo se ela já estiver presente. Cada up passa então a executar um build, o que é adequado num portátil e raramente é desejável num servidor.

services:
  web:
    build: .
    image: registry.example.com/acme/web:dev
    pull_policy: build

Também é importante conhecer outra interação. docker compose pull tenta obter imagens para os serviços que também têm uma secção build. Se essa operação falhar, informa que a imagem tem de ser construída. Passe --ignore-buildable para ignorar esses serviços silenciosamente.

Como a cache da build determina o tempo do deploy

Cada instrução num Dockerfile produz uma camada, e o builder reutiliza uma camada em cache quando essa instrução e as respetivas entradas não mudam. Para COPY, as entradas são o conteúdo dos ficheiros copiados. Quando uma camada deixa de ter correspondência na cache, todas as camadas seguintes são reconstruídas, porque cada camada é criada sobre o sistema de ficheiros produzido pela camada anterior.

Essa regra determina se o deploy demora segundos ou minutos. Ordene o Dockerfile do que muda menos vezes para o que muda a cada commit.

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

npm ci fica acima de COPY . ., por isso editar um ficheiro de código-fonte mantém a camada de instalação em cache e a build retoma no passo de cópia. Se trocar essas duas linhas, uma alteração de um único carácter reinstala todas as dependências, porque COPY . . invalida a camada sobre a qual npm ci é criado. A mesma estrutura aplica-se a pip install -r requirements.txt e a go mod download.

--no-cache é a ferramenta adequada quando suspeita que uma camada antiga está a ocultar a sua correção. Não é uma boa predefinição, porque elimina a reutilização que a ordenação do Dockerfile deve permitir.

Há uma definição que a imagem fornece e que o Compose pode substituir: o CMD do Dockerfile é o comando que a imagem executa por predefinição, e uma chave command: no serviço substitui-o. Como o comando e o entrypoint interagem é relevante neste caso, porque uma substituição no Compose pode fazer uma imagem recém-criada comportar-se exatamente como a antiga.

Contexto de compilação e .dockerignore

context: . significa que o Compose empacota esse diretório e o envia para o builder antes de executar a primeira instrução. Tudo o que estiver dentro dele é incluído, incluindo .git e qualquer diretório de dados que mantenha junto do código-fonte. Se uma compilação ficar parada na etapa de transferência do contexto num projeto que não sofreu alterações, isso indica que o contexto é demasiado grande.

Um ficheiro .dockerignore na raiz do contexto exclui caminhos dessa transferência. A sintaxe é semelhante à de .gitignore.

.git
node_modules
*.log
data/
.env

Há duas vantagens. A transferência diminui, pelo que cada compilação começa mais depressa. Além disso, COPY . . deixa de poder copiar .env para a imagem, onde qualquer pessoa que obtenha essa imagem pode ler o conteúdo.

O caso de compilação lenta que aumenta ao longo do tempo é um bind mount. Um volume nomeado fica fora do diretório do projeto, mas um bind mount como ./data:/var/lib/postgresql/data fica dentro do contexto de compilação. Assim, as compilações ficam mais lentas todas as semanas à medida que a base de dados cresce. Uma linha em .dockerignore resolve o problema. Bind mounts e volumes nomeados aborda o compromisso mais amplo.

Os argumentos de compilação apresentam uma versão menor do mesmo risco. Os valores passados através de args: ficam visíveis no histórico da imagem para qualquer pessoa que tenha a imagem. Por isso, coloque aí um número de versão e nunca um token. Ficheiros env e secrets no Compose explica onde devem ficar as credenciais.

Crie no VPS ou em outro local e faça o pull?

Criar no servidor que atende o seu tráfego é a opção padrão porque é o caminho mais curto: git pull e depois docker compose up -d --build. Isso funciona num servidor pequeno do qual ninguém depende ainda. Deixa de funcionar por duas razões que pode medir e por uma terceira que só aparece num dia problemático.

Memória. Um build executa compiladores e bundlers junto da aplicação em produção, e esses processos consomem muita memória na maioria das stacks. Num VPS com 1 GB, um bundler de JavaScript ou uma compilação de Rust é normalmente o maior processo do servidor. Quando o kernel fica sem memória, mata o maior processo: o build termina com Killed e o código de saída 137, ou a base de dados é terminada e o site fica indisponível a meio do deploy. dmesg -T | grep -i oom imprime a linha do encerramento com o nome do processo, para determinar qual dos dois casos ocorreu sem ter de adivinhar.

Disco. Cada build deixa camadas para trás, e o builder mantém a sua própria cache separada das suas imagens. docker system df mostra ambos, e a linha da cache de build só aumenta. Recupere espaço com docker image prune para imagens dangling e docker builder prune para camadas em cache. Um disco cheio interrompe mais do que o build. A base de dados também deixa de escrever, e essa falha custa muito mais do que um deploy lento.

Reprodutibilidade. Uma imagem criada no servidor existe apenas nesse servidor. Fazer rollback significa obter o commit antigo e criar a imagem novamente. Esse build não garante o mesmo resultado, porque a tag base mudou e os mirrors de pacotes também mudaram. Criar a imagem noutro local e fazer push de uma tag transforma o rollback numa edição: aponte image: para a tag anterior e execute docker compose up -d.

A configuração que se mantém estável é simples. A sua integração contínua executa o build e faz push de registry.example.com/acme/web:<git-sha>, enquanto o ficheiro Compose no VPS contém image: sem qualquer chave build:. O deploy passa então a ser feito com dois comandos que precisam de muito pouca memória.

docker compose pull
docker compose up -d

Execute docker login registry.example.com uma vez no servidor, e o Compose poderá fazer pull de tags privadas a partir daí.

Mantenha a secção de build para desenvolvimento em vez de a remover, num ficheiro com o nome que escolher.

# compose.dev.yaml
services:
  web:
    build:
      context: .
    pull_policy: build
docker compose -f compose.yaml -f compose.dev.yaml up -d --build

Dê a esse ficheiro o nome compose.dev.yaml, e não compose.override.yaml. O Compose carrega automaticamente um ficheiro de override quando este está presente. Por isso, um override copiado por engano para o servidor voltaria a iniciar builds nesse servidor sem emitir qualquer aviso. Estruturar vários ficheiros Compose explica como a mesclagem resolve cada chave.

A armadilha da arquitetura quando faz o build noutro local

Uma imagem inclui a arquitetura de CPU para a qual foi compilada. Faça o build num portátil com Apple Silicon, envie a imagem para um registry e depois faça pull dessa tag para um VPS x86_64. O Docker avisa que a plataforma da imagem solicitada não corresponde à plataforma detetada no host. O processo termina então com exec format error. A mensagem parece indicar um binário corrompido, mas não é esse o problema. Faça o build explicitamente para o destino:

docker buildx build --platform linux/amd64 \
  -t registry.example.com/acme/web:1.4.2 --push .

A mesma incompatibilidade ocorre no sentido inverso se o portátil usar x86 e executar um VPS ARM em vez de um VPS x86. Deixar o CI fazer o build na arquitetura em que a aplicação será implementada elimina essa dúvida.

O que verificar depois de uma implantação

  • docker compose images mostra a imagem e a tag usadas por cada contentor em execução. Um ID de imagem diferente confirma que a nova compilação está em serviço.
  • docker compose config mostra o ficheiro resultante depois da substituição das variáveis, para que possa ler o nome final da imagem que o Compose usará antes de executar qualquer comando.
  • docker compose logs -f web durante os primeiros 30 segundos depois da troca. Um contentor que inicia e termina entra num ciclo de reinícios em vez de permanecer ativo, e o ciclo não é visível sem monitorização.
  • docker image ls mostra uma coluna CREATED. Uma imagem mais antiga do que o seu último commit nunca foi recompilada.

Se ainda estiver a preparar o ficheiro usado por estas verificações, os fundamentos de um ficheiro Compose num VPS explica as chaves relacionadas, e a lista de referência dos comandos do Compose apresenta os restantes subcomandos.

FAQ

Posso usar build e image no mesmo serviço?

Sim. Esta é a configuração normal para um projeto que compila por si próprio. O Compose compila a partir da secção build: e atribui ao resultado a tag com o valor de image:. Essa tag é a que docker compose push envia para um registry e a que outra máquina obtém. Sem uma chave image:, o Compose continua a compilar, mas atribui à imagem um nome baseado no projeto e no serviço. Também avisa que a ausência desse atributo impede o envio da imagem.

Porque é que docker compose up não deteta a alteração no meu Dockerfile?

Porque up verifica apenas se existe uma imagem com esse nome. Quando existe, o Compose inicia-a e nunca a compara com o Dockerfile nem com os ficheiros de origem. Execute docker compose up -d --build ou execute docker compose build web seguido de docker compose up --no-deps -d web para substituir um único serviço. Definir pull_policy: build no serviço faz com que cada up volte a compilar, o que é adequado para uma máquina de desenvolvimento.

Qual é a diferença entre --build e --force-recreate?

--build compila novamente a imagem e recria depois os contentores cuja imagem mudou. --force-recreate recria os contentores a partir da imagem que já têm, pelo que nunca deteta uma alteração no código. Se a alteração estiver na origem ou no Dockerfile, --build é a flag correta. --force-recreate serve para repor o próprio contentor, por exemplo, para limpar a camada gravável mantendo a mesma imagem.

Devo compilar as minhas imagens Docker no VPS ou noutro local?

Compile noutro local e obtenha uma tag quando a máquina também servir tráfego. A compilação compete com a aplicação pela memória. Num VPS pequeno, o kernel resolve essa competição terminando o processo maior, que pode ser a compilação ou a base de dados. As compilações também deixam cache no disco que não é libertada automaticamente. Compilar no servidor continua a ser adequado para um projeto pequeno sem utilizadores. A migração posterior é simples se mantiver a secção build: num ficheiro Compose exclusivo para desenvolvimento.

Como impeço que a cache de compilação do Docker encha o disco?

Execute docker system df para ver quanto espaço as imagens e a cache de compilação ocupam. docker builder prune remove as camadas em cache e docker image prune remove as imagens dangling deixadas por compilações anteriores. Adicionar -a a qualquer um dos comandos é mais agressivo e faz com que a compilação seguinte comece sem cache. Não agende docker system prune -af --volumes num servidor, porque --volumes elimina qualquer volume que nenhum contentor esteja a utilizar. Uma stack parada para manutenção mantém a base de dados exatamente num volume desse tipo.