Trocar a imagem sem cortar ligações (e o rollback que não reverte tudo)
Começo pela parte honesta, que é a que falta em quase tudo o que está escrito sobre isto: um docker compose up -d para o contentor antigo e arranca o novo. Há um intervalo. São segundos, mas existem, e chamar-lhe «sem interrupção» é mentira.
O que um guião de deploy bem feito compra não é um intervalo de zero. É não ficar estragado — que é um problema completamente diferente, e muito mais frequente.
1. A etiqueta é fixa, nunca latest
services:
web:
image: gitea.exemplo.org/fnsp/fnsp_web:1.0.0
container_name: fnsp-web
restart: unless-stoppedCom :latest não se sabe o que está a correr, e o rollback passa a adivinhação: voltar atrás é voltar a quê? Uma etiqueta fixa transforma o deploy num número, e o guião reescreve essa linha a cada versão.
2. Verificar antes de mexer em alguma coisa
if ! docker pull "${IMAGE}:${VERSION}"; then
echo "Falhou o pull."
exit 1
fi
sed -i "s|${IMAGE}:[^[:space:]\"]*|${IMAGE}:${VERSION}|" "$COMPOSE_FILE"
docker compose up -dA ordem é tudo: o pull vem antes de reescrever o ficheiro. Uma versão que não existe no registo — um erro de escrita, uma publicação que falhou — pára aqui, com o servidor exactamente como estava. Ao contrário, ficava um docker-compose.yml a apontar para uma imagem inexistente, que é a pior forma de descobrir um erro de escrita.
A falha mais comum neste passo não é a imagem: é o próprio servidor não conseguir resolver o registo. Quando o registo vive na mesma máquina, atrás do mesmo domínio público, o pedido sai para o DNS público e tenta voltar a entrar pelo router — e se o router não fizer hairpinning, o pull fica pendurado até ao timeout. A saída é o servidor resolver aquele nome para o endereço local.
3. Esperar pelo healthcheck, e não pelo arranque
Um contentor que arrancou não é um contentor que serve — é o assunto do artigo dos healthchecks. O que interessa aqui é o guião esperar pelo veredito em vez de assumir:
while ((ELAPSED < TIMEOUT_SEGUNDOS)); do
STATUS=$(docker inspect -f '{{.State.Health.Status}}' "$CONTAINER")
if [[ "$STATUS" == "healthy" ]]; then
echo "Deploy concluido"
exit 0
fi
sleep 3
ELAPSED=$((ELAPSED + 3))
doneE há um parâmetro que decide se isto funciona ou se sabota tudo:
HEALTHCHECK --interval=30s --timeout=3s --start-period=25s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/healthz >/dev/null || exit 1O --start-period=25s. Sem ele, o contentor é declarado doente enquanto ainda está a aplicar as migrações no arranque, e o guião reverte um deploy que estava perfeitamente bem. Um período de arranque curto a mais não dá um erro — dá um rollback inexplicável.
4. O rollback, e o que ele não reverte
echo "Rollback para $CURRENT"
sed -i "s|${IMAGE}:[^[:space:]\"]*|${IMAGE}:${CURRENT}|" "$COMPOSE_FILE"
docker compose up -dAqui está a parte que convém saber antes de confiar: o rollback repõe a imagem, não o esquema da base. As migrações são aplicadas no arranque, e voltar à imagem anterior deixa a base no estado novo.
Na prática isso divide as migrações em duas famílias. As que acrescentam — uma coluna nova com valor por omissão, uma tabela nova — são seguras: a imagem antiga não conhece a coluna e ignora-a. As que retiram ou mudam o tipo de algo que a imagem antiga usa não têm rollback nenhum, e não há guião que resolva isso. Essas fazem-se em dois deploys: primeiro acrescentar e passar a escrever nos dois sítios, só depois remover o antigo.
É uma disciplina chata, e é a diferença entre ter rollback e achar que se tem.
5. O que faltaria para não haver intervalo nenhum
Dois contentores ao mesmo tempo e o proxy a mudar de destino quando o novo ficar saudável. É fazível, e traz duas condições que não são de graça: a aplicação tem de tolerar duas instâncias a correr em simultâneo, e — no caso de quem aplica migrações no arranque — as duas vão tentar migrar ao mesmo tempo.
Para um blogue pessoal, o intervalo são segundos às três da manhã. Não vale a complexidade, e dizê-lo é mais útil do que montar uma infraestrutura de banco por hábito.
6. O pormenor sem glamour que evita a chamada de madrugada
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'Sem isto, o registo do contentor cresce até acabar o disco — e um disco cheio não dá um erro honesto, dá o site em baixo com uma causa que não se parece nada com a causa. É a primeira das armadilhas do espaço em Linux, e custa quatro linhas.
O resumo
Etiqueta fixa para o rollback ser possível. pull antes de tocar na configuração. Esperar pelo healthcheck, com período de arranque. E saber que a base não volta atrás com a imagem.
São sessenta linhas de bash. O que elas dão não é um deploy sem intervalo — é um deploy que, quando corre mal, deixa o site de pé na versão anterior em vez de o deixar em baixo à espera de alguém.