Introdução
O Docker tornou-se essencial hoje em dia… quase todos os produtos são distribuídos como containers e até muitas vezes nem há outra opção (os scripts de instalação começam a ser raros). O Docker Compose leva isto ainda mais longe, permitindo instalar e executar aplicações constituídas por múltiplos containers.
E qual é o problema com isto?
Bem, o problema é que quando um upgrade docker compose falha já numa fase avançada, por exemplo quando falham as migrações da base de dados, ficamos sem forma de voltar à versão anterior… se alguma coisa nos dados foi alterada pela nova versão, como fazemos para voltar à versão anterior que até funcionava tão bem!???
Este post discute o problema, fornece uma solução manual utilizando funcionalidades padrão do Docker, e apresenta uma solução mais avançada e automatizada para fazer backup de stacks Docker Compose. Aviso – este é o 1º artigo que escrevi com a ajuda do meu assistente pessoal virtual (neste artigo usei o chatgpt, e penso usar outros no futuro para comparar). Fiz alterações no texto, mas acho que se nota bem as secções que são 100% artificiais porque parecem anúncios e o português é um pouco “abrasileirado”.
Por Que o Docker Compose é Tão Popular
O Docker Compose ganhou popularidade significativa devido a várias razões principais:
- Configuração Simplificada de Multi-Containers: O Docker Compose permite definir e executar aplicações Docker multi-container usando um único ficheiro
docker-compose.yml
. Isso simplifica o processo de gerir aplicações complexas com múltiplos serviços. - Facilidade de Uso: Com o Docker Compose, você pode facilmente iniciar, parar e configurar todos os seus serviços de aplicação usando comandos simples. Isso reduz a complexidade envolvida na gestão de containers individuais.
- Replicação de Ambiente: O Docker Compose facilita a replicação do mesmo ambiente em diferentes estágios de desenvolvimento, teste e produção. Isso garante consistência e reduz a probabilidade de problemas específicos do ambiente.
- Networking: O Docker Compose configura automaticamente a rede entre containers, permitindo que eles se comuniquem sem problemas. Isso é particularmente útil para arquiteturas de microserviços.
- Escalabilidade: O Docker Compose suporta a escalabilidade dos serviços para cima e para baixo com um único comando, facilitando o gerenciamento de cargas de trabalho variáveis.
O Problema
O problema é simples: como podemos fazer um backup de todos os volumes usados por todos os containers num docker-compose.yml? Vamos tentar resolver estas 4 questões:
- Consistência de Dados: Garantir que os dados sejam guardados num estado consistente , que é crucial especialmente para bases de dados e outros serviços em que o estado em disco é relevante na recuperação. O que isto quer dizer é que temos de parar os containers antes de fazer o backup. Vamos fazer stop em vez de down – explicação mais adiante.
- Recuperação de Desastres: Ter backups confiáveis é essencial para a recuperação de desastres. Em caso de perda ou corrupção de dados, você pode restaurar rapidamente seus serviços. “Obviously”…
- Rastreamento de Versões: Manter o controle das versões exatas das imagens Docker e das configurações usadas nos seus serviços garante que você possa recriar o mesmo ambiente se necessário. Ou seja, para voltarmos à “versão anterior” temos de guardar os ids das imagens que estavam em uso em cada container quando fizemos o backup.
- Eficiência: Usar compressão multi-thread acelera o processo de backup, economizando tempo e recursos. Isto porque hoje em dia, toda a gente que usa docker tem multi-cores… fazer uma compressão do backup usando apenas 1 thread é esperar mais tempo desnecessariamente.
O Processo Manual
Tanto quanto sei o Docker não fornece uma solução abrangente out-of-the-box (OOTB) para funcionalidades avançadas de backup, mas tem os blocos de construção básicos necessários para criar backups. Vamos ver este processo passo-a-passo…
Se alguém souber de um processo “oficial” backup-restore ou snapshot por favor deixe aqui um comentário.
Passo 1: Parar os Serviços
Primeiro, paramos os serviços para garantir a consistência dos dados.
docker compose stop
Também podemos fazer docker compose down. A diferença do comando stop é que não destrói os containers… ou seja, ao fazermos stop/start os containers mantém o estado. Por exemplo, se alterámos alguma coisa num container como instalar um pacote, essa alteração mantém-se ao fazer start. Se fizermos down/up os containers são recriados no seu estado inicial, perdendo-se todas as alterações que tenhamos feito (exceto claro o que estiver guardado em volumes).
Passo 2: Backup dos Volumes
Em seguida, usamos os comandos docker run
e tar
para criar backups dos seus volumes.
Dado que os containers Docker são como máquinas temporárias, em cada reinicio voltam ao seu estado inicial e tudo o que lá pusemos de novo ou atualizado desaparece. Os Volumes são áreas de ficheiros onde podemos guardar dados que são mantidos mesmo quando reiniciamos os containers.
Ora, como explica a AI o que são volumes?
“Volumes são áreas de armazenamento persistente usadas por containers Docker para armazenar dados. Eles permitem que os dados sejam armazenados fora do ciclo de vida dos containers, garantindo que as informações permaneçam intactas mesmo após reinicializações ou recriações dos containers.”
Por exemplo, os Volumes têm de ser usados em containers de bases de dados. De outra forma, quando se reinicializasse esse container a bd voltava ao 0.
Ou seja, um backup de um container é na verdade apenas o backup dos seus volumes. Fazer backup de um container não faz sentido algum…
# Listar volumes
docker volume ls
# Fazer backup de cada volume
docker run --rm -v <nome_do_volume>:/data -v <pasta_local_para_backup>:/backup busybox tar czf /backup/volume_name.tar.gz -C /data .
Aqui temos muito sumo para analisar… usamos 2 capacidades do docker: i) corremos um container temporário que é apagado assim que for terminado (opção -rm) e ii) ligamos este container temporário ao volume que queremos copiar (opção -v). Este é um truque elegante que tem várias coisas importantes:
- montamos o volume queremos guardar à pasta interna /data no container temporário;
- montamos a pasta local onde queremos guardar o backup na pasta interna /backup
- vamos usar o comando tar para comprimir a pasta interna /data para a pasta /backup;
- os ficheiros na pasta interna /backup vão aparecer na <pasta_local_para_backup> fora do container, no próprio host.
Pois é… um comando tão simples e afinal cheio de truques…
No script final, mais à frente, vamos fazer algumas coisas adicionais – vamos listar os volumes de cada container referido no docker-compose, incluindo volumes anónimos (sem nome). Isto permite automatizar o backup do grupo de containers.
Passo 3: Guardar o Ficheiro Docker Compose
Copiamos manualmente o ficheiro Docker Compose para o local do backup.
cp docker-compose.yml <pasta_local_para_backup>docker-compose_<data>.yml
Num processo de recriação do estado funcional anterior ao desastre vamos precisar do docker-compose.yml tal como estava antes de qualquer alteração. Também vamos precisar de saber a versão exata de cada container que estava em uso…
Passo 4: Registrar as Versões das Imagens
Registamos manualmente as versões das imagens usadas nos seus containers.
# Listar todos os containers em execução docker ps
# Para cada container, obter o ID da imagem
docker inspect --format='{{.Name}} {{.Image}}' container_id
Isto é fundamental… há containers com um ritmo acelerado de lançamento de novas versões. Para voltarmos ao estado inicial temos de saber as versões exatas, e alterar o docker-compose.yml para “tagar” essas versões, garantindo que puxamos as mesmas versões que estavam em uso no momento do backup (em geral são puxadas as versões mais recentes). Isto obrigará à edição do .yml antes de o usar.
Passo 5: Reiniciar os Serviços
Finalmente, voltamos a iniciar os containers:
docker-compose start
Isto serve apenas para voltar a ter os containers a funcionar. Uma vez que já fizemos o backup, podemos então prosseguir com o update dos containers.
Solução Automatizada com Script
Obviamente fazer isto tudo antes de fazermos um update aos nossos containers é absurdo… e a mim irrita-me ligeiramente que o docker não tenha um comando docker compose snapshot… mas enfim…
Já que vamos criar um script então mais vale usar compressão multi-thread e uma opção de dry run. Além disso procuramos volumes com e sem nome (anonymous). O nosso script vai chamar-se compose_snapshot.sh
.
Funcionalidades do Script
- Análise de Argumentos: O script aceita argumentos para o ficheiro Docker Compose, pasta de saída, número de threads para compressão e uma opção de dry run.
- Consistência de Dados: O script para todos os containers em execução antes de realizar o backup para garantir a consistência dos dados.
- Compressão Multi-Thread: O script usa
pigz
para compressão multi-thread, tornando o processo de backup mais rápido e eficiente. - Rastreamento de Versões: O script salva os IDs exatos das imagens e o ficheiro Docker Compose usado para o backup, permitindo a recriação precisa do ambiente.
- Funcionalidade de Dry Run: O script inclui uma opção de dry run para listar os volumes que seriam backupados sem realizar o backup real.
O Script
Aqui está o script compose_snapshot.sh
, escrito a meias por mim e pelo meu novo assistente virtual (tenho de lhe arranjar um nome… ART (Asshole Research Transport)*):
#!/bin/bash
# Função para exibir informações de uso
usage() {
echo "Uso: $0 -f <compose_file> -o <output_folder> -p <num_threads> [--dry-run]"
exit 1
}
DRY_RUN=false
NUM_THREADS=4
# Analisar argumentos
while getopts ":f:o:p:-:" opt; do
case ${opt} in
f )
COMPOSE_FILE=$OPTARG
;;
o )
OUTPUT_FOLDER=$OPTARG
;;
p )
NUM_THREADS=$OPTARG
;;
- )
case "${OPTARG}" in
dry-run)
DRY_RUN=true
;;
*)
usage
;;
esac
;;
\? )
usage
;;
esac
done
# Verificar se todos os argumentos necessários são fornecidos
if [ -z "$COMPOSE_FILE" ] || [ -z "$OUTPUT_FOLDER" ] || [ -z "$NUM_THREADS" ]; then
usage
fi
# Verificar se o ficheiro Docker Compose existe
if [ ! -f "$COMPOSE_FILE" ]; then
echo "Erro: Ficheiro Compose $COMPOSE_FILE não encontrado."
exit 1
fi
# Criar pasta de saída se não existir
mkdir -p "$OUTPUT_FOLDER"
# Obter o timestamp atual
timestamp=$(date +%Y%m%d%H%M%S)
# Copiar o ficheiro Docker Compose para a pasta de saída com o timestamp
cp "$COMPOSE_FILE" "$OUTPUT_FOLDER/$(basename "$COMPOSE_FILE" .yml)_${timestamp}.yml"
# Verificar se os serviços Docker Compose estão ativos
if ! docker compose -f "$COMPOSE_FILE" ps | grep -q "Up"; then
echo "Erro: Serviços Docker Compose não estão em execução. Por favor, inicie os serviços usando 'docker compose -f $COMPOSE_FILE up -d' e tente novamente."
exit 1
fi
# Obter todos os IDs dos containers do projeto Docker Compose
CONTAINER_IDS=$(docker compose -f "$COMPOSE_FILE" ps -q)
# Função para fazer backup de um volume
backup_volume() {
local volume_name=$1
local output_folder=$2
local timestamp=$3
local num_threads=$4
local backup_file="$output_folder/${volume_name}_${timestamp}.tar.gz"
if [ "$DRY_RUN" = true ]; then
echo "Faria backup do volume $volume_name para $backup_file"
else
echo "Fazendo backup do volume $volume_name para $backup_file"
docker run --rm -v "$volume_name:/mnt/volume" -v "$output_folder:/backup" alpine \
sh -c "apk add --no-cache pigz && tar cvf - -C /mnt/volume . | pigz -p $num_threads > /backup/${volume_name}_${timestamp}.tar.gz"
fi
}
if [ "$DRY_RUN" = false ]; then
# Parar containers
echo "Parando todos os containers..."
docker compose -f "$COMPOSE_FILE" stop
fi
# Fazer backup dos volumes de cada container
for container_id in $CONTAINER_IDS; do
# Obter os volumes montados de cada container
VOLUMES=$(docker inspect --format '{{ range .Mounts }}{{ .Name }} {{ end }}' $container_id)
for volume in $VOLUMES; do
# Ignorar nomes de volumes vazios (montagens não de volumes)
if [ -n "$volume" ]; then
backup_volume "$volume" "$OUTPUT_FOLDER" "$timestamp" "$NUM_THREADS"
fi
done
# Listar o ID da imagem de cada container
IMAGE_ID=$(docker inspect --format '{{.Image}}' $container_id)
CONTAINER_NAME=$(docker inspect --format '{{.Name}}' $container_id | cut -c2-)
echo "Container $CONTAINER_NAME está a usar a imagem ID $IMAGE_ID" >> "$OUTPUT_FOLDER/image_ids_${timestamp}.txt"
done
if [ "$DRY_RUN" = false ]; then
# Reiniciar containers
echo "Iniciando todos os containers..."
docker compose -f "$COMPOSE_FILE" start
fi
if [ "$DRY_RUN" = true ]; then
echo "Dry run concluído. Nenhum volume foi backupado e nenhum container foi parado."
else
echo "Backup concluído. IDs das imagens salvos em $OUTPUT_FOLDER/image_ids_${timestamp}.txt."
echo "Ficheiro Docker Compose salvo em $OUTPUT_FOLDER/$(basename "$COMPOSE_FILE" .yml)_${timestamp}.yml."
fi
Conclusão
O script compose_snapshot.sh
é uma ferramenta para facilitar o backup de stacks Docker Compose de forma rápida, com garantia de consistência dos dados, compressão multi-thread, rastreamento de versões e com uma opção de dry run. Implementar uma solução de backup como esta não só protege os seus dados, mas também garante que você pode rapidamente recuperar e recriar o seu ambiente quando necessário. Mas pessoalmente é algo que quero fazer sempre e rapidamente antes de qualquer update a um stack docker-compose.
Nota 1: falta o script de restore! Que pretendo em breve publicar…
Nota 2: o método que uso atualmente é colocar o docker dentro de um container LXD. Antes de fazer um update aos containers, faço apenas 1 comando: lxc snapshot <container> <nome_do_snapshot>. Fácil e rápido.
Mas há coisas que não funcionam bem nesta abordagem, como usar GPUs. Daí o script…
Nota 3: há ferramentas que devem fazer backup de containers, como o portainer. Mas mascaram a mecânica das coisas, impedindo que aprendamos como a tecnologia funciona, e substituindo essa aprendizagem por outra que me parece menos útil.
*ART dos fantásticos livros do MurderBot.