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 --buildfaz 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 webfaz o build de um serviço e não inicia nada. Use-o comdocker compose up --no-deps -d webpara substituir apenas esse container e manter o restante da stack em execução.docker compose build --no-cache webdescarta todas as camadas em cache e faz o build desde a primeira instrução.docker compose build --pulltenta obter uma versão mais recente da imagem base emFROM. Assim, uma tag mutável comonode:22recebe o conteúdo atual, em vez da cópia que foi descarregada em março.docker compose up -d --force-recreaterecria 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: buildTambé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/
.envHá 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 -dExecute 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: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --buildDê 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 imagesmostra 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 configmostra 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 webdurante 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 lsmostra 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.