Volumes
Os volumes no Docker são uma maneira de persistir e compartilhar dados entre contêineres e o host. Eles fornecem um mecanismo flexível para armazenar dados gerados ou consumidos por aplicativos em contêineres Docker. Eles são fundamentais para garantir uma cópia segura de um volume existente. Os volumes asseguram segurança e conveniência quando surge a necessidade de recuperar um volume contendo arquivos e informações importantes.
Os Containers de dados são empregados para isolar dados e facilitar a comunicação entre múltiplos contêineres, de maneira descomplicada, compartilhando os mesmos dados entre eles. Para essa finalidade, é designado um volume dentro desses contêineres, que será acessado por outros contêineres.
Nesse cenário, não é preciso criar um diretório específico persistente, pois um volume padrão é automaticamente criado dentro da estrutura do Docker. A vantagem disso é evitar a necessidade de gerenciar diretamente esses diretórios, visto que todas as conexões são direcionadas para o container de dados e não para um diretório específico.
Os volumes são armazenados em uma parte do sistema de arquivos do host que é gerenciada pelo Docker (/var/lib/docker/volumes/ no Linux). Processos que não são do Docker não devem modificar esta parte do sistema de arquivos. As montagens tmpfs são armazenadas apenas na memória do sistema do host e nunca são gravadas no sistema de arquivos do host.
Quando usar Volumes?
Como já foi dito, os volumes são a forma preferida de persistir dados em contêineres e serviços Docker. Alguns casos de uso são:
Quando precisamos compartilhar dados entre múltiplos contêineres em execução.
Quando esse contêiner para ou é removido, o volume ainda existe. Múltiplos contêineres podem montar o mesmo volume simultaneamente, seja em modo de leitura-escrita ou somente leitura. Volumes são removidos apenas quando você os remove explicitamente.Quando precisamos fazer backup
Quase sempre precisamos fazer backup, restaurar ou migrar dados de um host Docker para outro, aqui novamente podemos usar volumes. Você pode parar os contêineres que usam o volume, então fazer o backup do diretório do volume (como/var/lib/docker/volumes/<volume-nome>).
Bind mounts
Bind mounts são um recurso do Docker que permite montar um diretório ou arquivo do sistema de arquivos do host diretamente dentro de um contêiner Docker. Isso significa que você pode vincular um diretório ou arquivo existente em seu sistema de arquivos local ao sistema de arquivos do contêiner.
Os Bind mounts têm funcionalidade limitada em comparação com os volumes. Quando você usa um bind mount, um arquivo ou diretório no sistema do host é montado em um contêiner. O arquivo ou diretório é referenciado pelo seu caminho completo no sistema do host. O arquivo ou diretório não precisa existir no sistema do host do Docker antecipadamente. Ele é criado sob demanda se ainda não existir.
As montagens de vinculação são rápidas, mas dependem da estrutura de diretórios específica disponível no sistema de arquivos do host. Se você estiver desenvolvendo novas aplicações Docker, considere usar volumes nomeados (named volumes) em vez disso. Você não pode usar comandos CLI do Docker para gerenciar diretamente montagens de vinculação.
Os Bind mounts permitem acesso de escrita a arquivos no sistema do host por padrão. Uma implicação significativa do uso de bind mounts é a capacidade de processos em execução dentro de um contêiner alterarem o sistema de arquivos do host. Isso inclui a criação, modificação ou exclusão de arquivos ou diretórios importantes do sistema.
Essa capacidade pode ser poderosa, mas também apresenta implicações de segurança, pois os processos dentro do contêiner podem afetar processos que não são do Docker no sistema do host.
Quando usar Bind mounts?
Alguns dos casos onde Bind mount são bons, incluem:
Compartilhar arquivos de configuração da máquina host com os contêineres. Um exemplo comum é o Docker usar bind mounts para fornecer resolução DNS aos contêineres, montando o
/etc/resolv.confda máquina host em cada contêiner.Compartilhar código-fonte ou artefatos de compilação entre um ambiente de desenvolvimento no host Docker e um contêiner. Por exemplo, você pode montar um diretório no contêiner para compartilhar o código-fonte entre o host Docker e o contêiner. Isso permite que os artefatos reconstruídos sejam acessados pelo contêiner a cada nova construção do projeto no host Docker.
Um volume é um tipo de armazenamento gerenciado pelo Docker. Ele é criado e controlado pelo próprio runtime, podendo ser compartilhado facilmente entre vários containers. Como é independente do ciclo de vida dos containers, os dados persistem mesmo após a remoção dos containers.
Já um bind mount referencia um diretório ou arquivo real do host, mapeado diretamente para dentro do container. Ele também pode ser compartilhado entre containers, mas não é isolado nem gerenciado pelo runtime, as permissões e o acesso dependem totalmente do sistema de arquivos do host.
Diferenças de UID/GID entre host e container
No Linux, arquivos e processos são identificados por UID (ID do usuário) e GID (ID do grupo). Uma complicação comum que a gente enfrenta ao usar bind mounts é que o usuário dentro do container pode não ter o mesmo UID/GID do usuário dono dos arquivos no host, gerando problemas de permissão.
Muitos containers executam processos como root ao invés de usar um usuário não root. Se um container rodando como root escreve em um diretório montado do host, os arquivos/criações ficarão como propriedade do root no host. Isso significa que o usuário normal do host não conseguirá modificar ou excluir esses arquivos sem privilégio elevado.
Do ponto de vista do host, arquivos criados pelo container podem imprdir a edição, já que pertencem a root (dentro do container é aplicado o UID 0, que é o mesmo UID de root fora do Container). Esse cenário é comum em ambientes onde o container gera arquivos de build ou logs.
Por outro lado, alguns containers seguem boas práticas de segurança e executam com um usuário não-root (por exemplo, appuser com UID distinto, como 1001). Nesse caso, ocorre o inverso, se o diretório do host montado for propriedade de um usuário comum (UID 1000) no host, o processo dentro do container (UID diferente, ex. 1001) não conseguirá escrever nesse diretório, resultando em erro "Permission denied". Ou seja, se os UIDs não coincidem (dentro e fora do container), o container pode ficar sem acesso de gravação nos arquivos montados.
Para lidar com essas diferenças de UID/GID e evitar conflitos de permissão, existem algumas abordagens que podemos seguir.
Mesmo UID/GID dentro e fora do Container
A solução mais direta é alinhar o usuário dentro do container com o usuário do host que possui os arquivos. Em Docker Compose, podemos usar a opção user: <UID>:<GID> no serviço.
Por exemplo, se os arquivos no host são do usuário com UID 1000, devemos configurar user: "1000:1000", isso faz o processo no container rodar com esse mesmo UID/GID.
Assim, qualquer arquivo criado no bind mount terá dono UID 1000, correspondendo ao usuário do host, evitando problemas. Conforme recomendação em fórum da Docker, o UID/GID do processo dentro do container deve casar com o UID/GID do dono da pasta no host para satisfazer as permissões Unix.
É possível definir isso diretamente via Compose (user) ou usando docker run -u UID:GID na linha de comando. Alguns contêineres oficiais oferecem variáveis de ambiente (como PUID/PGID) para configurar o usuário interno.
Criar usuário com UID do host no Container
Uma alternativa mais robusta é, criar no Dockerfile ou entrypoint, um usuário no container com o mesmo UID e GID do usuário host. Como o kernel Linux se baseia nos IDs numéricos (não nos nomes) para atribuir propriedade de arquivo, se o container tiver um usuário com UID/GID 1000 (mesmo do host), arquivos gravados por esse usuário dentro do container serão do usuário equivalente no host.
Na prática, podemos adicionar no Dockerfile algo como:
RUN addgroup --gid 1000 app && adduser --uid 1000 --gid 1000 --disabled-password app
USER app
Dessa forma, o processo rodará como UID 1000 dentro do container. Essa abordagem garante consistência, mas requer que a imagem Docker seja construída com esses ajustes (ou seja, gerenciamento do Dockerfile).
Vale notar que se não houver uma entrada de usuário no /etc/passwd do container para aquele UID, alguns programas podem não funcionar corretamente (por exemplo, esperando mapear UID para nome de usuário ou ter um diretório home). Por isso, criar a conta dentro do container (em vez de apenas usar -u) é preferível em ambientes mais complexos.
Permissão total nos arquivos
Em situações de desenvolvimento, algumas pessoas optam por conceder permissão ampla (como chmod 777 no diretório do host) para evitar problemas de escrita/leitura. Essa não é uma solução segura para produção, pois expõe demais os arquivos.
Em produção, é melhor controlar os IDs conforme citado acima ou usar volumes do Docker em vez de binds. Em testes locais, se usar essa abordagem temporária, lembre de revertê-la e buscar uma solução apropriada posteriormente.
Docker volumes
O comando docker volume é usado para gerenciar volumes no Docker. Com este comando, você pode criar, listar, inspecionar e remover volumes. Aqui estão algumas operações comuns que você pode realizar usando o comando docker volume:
| Comando | Descrição |
|---|---|
docker volume create VOLUME | Cria um novo volume com o nome especificado. |
docker volume ls | Lista todos os volumes disponíveis no sistema. |
docker volume inspect VOLUME | Exibe detalhes sobre um volume específico, como seu nome, driver de armazenamento e opções de montagem. |
docker volume rm VOLUME | Remove um volume específico. Observe que você não pode remover um volume em uso por um contêiner em execução. |
docker volume prune VOLUME | É usado para remover volumes Docker que não estão sendo usados por nenhum contêiner. |
# Montando um diretório com bind-mounts. Modo detalhado:
docker run -it -w 'app' --mount type=bind,source=$(pwd)/my-pc-dir,target=/app alpine sh
# Montando um diretório com bind-mounts. Modo curto:
docker run -it -w 'app' -v /tmp/my-pc-dir:/app alpine sh
# Executando o container e montando um volume. Modo detalhado:
docker run -d --mount type=volume,source=app-vol,target=/app alpine
# Executando o container e montando um volume. Modo curto:
docker run -d -v app-vol:/app alpine
# Montar um volume, sintaxe:
docker container run -d --volumes-from VOLUME_NAME --name IMAGE ID
# Criando uma imagem e montando o volume como apenas leitura:
docker container run -ti --mount type=bind,src=/root/docker/,dst=/volume,ro ubuntu
Data-Only Container
É um container apenas para prover volumes para outros containers, parecido com um samba, NFS server dentre outros. Essa abordagem era mais útil nas primeiras versões do Docker, quando o gerenciamento de volumes não era tão avançado quanto é hoje. Atualmente, com as funcionalidades nativas de volumes do Docker, os Data-Only Containers são considerados obsoletos ou desnecessários na maioria dos casos, mas vou descrever eles apenas para conhecimento.
O comando abaixo vai criar um contêiner com um volume chamado /dbdados. O volume será criado internamente no contêiner e não será associado a nenhum diretório no sistema de arquivos do host. O contêiner será mantido em execução apenas para manter o volume, já que o comando executado (/bin/true) não realiza nenhuma operação significativa.
╼ \# docker create -v /dbdados --name dbdados mysql /bin/true
O comando acima cria um novo container chamado dbdados com um volume
/dbdadosassociado, baseado na imagem do MySQL. O comando/bin/trueé executado para manter o contêiner em execução, já que não faz nada significativo.
Para usar o volume da dos existente:
docker container run -d --volumes-from VOLUME_NAME --name IMAGE ID
Para usar um diretório específico use o passo a passo abaixo.
# Crie a pasta volume no /tmp:
╼ $ mkdir /tmp/volume
# Criando uma imagem e montando o volume (volume vazio):
╼ $ docker container run -ti --mount type=bind,src=/tmp/volume,dst=/tmp/volume ubuntu
# Criando uma imagem e montando o volume (volume cheio):
╼ \# docker container run -ti --mount type=bind,src=/root/docker/,dst=/volume ubuntu
╼ $ df -h
Filesystem Size Used Avail Use% Mounted on
overlay 902G 595G 261G 70% /
tmpfs 64M 0 64M 0% /dev
tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
shm 64M 0 64M 0% /dev/shm
/dev/sda1 902G 595G 261G 70% /volume
tmpfs 7.8G 0 7.8G 0% /proc/asound
tmpfs 7.8G 0 7.8G 0% /proc/acpi
tmpfs 7.8G 0 7.8G 0% /proc/scsi
tmpfs 7.8G 0 7.8G 0% /sys/firmware
# Criando uma imagem e montando o volume como apenas leitura:
╼ \# docker container run -ti --mount type=bind,src=/root/docker/,dst=/volume,ro ubuntu
O comando acima cria um novo contêiner a partir da imagem do Ubuntu. Um volume é montado no contêiner usando a opção --mount. Neste caso, o tipo de montagem é bind, o que significa que um diretório específico no sistema de arquivos do host (/tmp/volume) é montado dentro do contêiner no mesmo local (/tmp/volume).
Qualquer alteração feita nos arquivos dentro deste diretório no contêiner também será refletida no sistema de arquivos do host e vice-versa. Este é um volume de bind, onde o diretório no host é vinculado diretamente ao diretório no contêiner.
Storage drivers
Os storage drivers são componentes cruciais na arquitetura do Docker, pois determinam como os dados são manipulados e armazenados no sistema de arquivos do contêiner. Cada storage driver implementa uma estratégia específica para lidar com a persistência de dados, a eficiência de armazenamento e o desempenho de E/S.
Existem vários storage drivers disponíveis no Docker, e a escolha do driver apropriado depende de vários fatores, como o sistema operacional do host, o ambiente de implantação, as características do sistema de arquivos subjacente e os requisitos de desempenho e segurança.
Alguns exemplos comuns de storage drivers incluem:
AUFS (Another Union File System)
Este foi o primeiro filesystem disponível para o Docker, ele funciona em nível de arquivo (não em bloco), tendo múltiplos diretórios que é apresentado ao S.O como apenas um único ponto de montagem.
Escrever em arquivos grandes causa lentidão na performance porque deve ser copiado o arquivo para a camada superior de escrita, sua busca é feito através do PATH, o problema é que, para cada camada, ele vai buscar os diretórios dentro do PATH, ou seja, um PATH com 2 diretórios serão pesquisados estes 2 diretórios em todas as camadas, até achar o que procura ou concluir que não existe.
Quando um arquivo é deletado, ele não é realmente deletado, ele é apenas renomeado para .wh.<nome> e fica indisponível para o container, já que não da para apagar por ser read-only.h
Device Mapper
Há uma semelhança muito grande entre AUFS e Device Mapper, o DM (Device Mapper) foi criado pela Red Hat e permite técnicas como RAID e LVM graças ao mapeamento de blocos físicos para lógicos. Ele corrige alguns problemas do AUFS, como o problema de copiar arquivos grandes quando formos alterar, a cópia é feita no nível de bloco, em teria, o problema de performance não ocorre mais.
OverlayFS e OverlayFS2
O OverlayFS é uma versão melhorada do AUFS, sua segunda versão é recomendada pela equipe do Docker. Sua segunda versão veio com multilayer, page caching sharing, ou seja, múltiplos containers acessando o mesmo arquivo e dividem a mesma entrada no arquivo de paginação, economizando mais memoria.
Como ele é a nível de arquivo, ainda temos o problema de copiar para camada superior, porém, após copiado, ele permanece lá, desta forma edições futuras serão mais rápidos.
BRTFS
Trabalha em nível de bloco, suporte inúmeras tecnologias avançadas de storage e suporte copy-on-write snapshots. Só é suportado na versão CE para Debian-like e EE para Suse Linux Enterprise Server.
VFS
Este é o driver mais básico e geralmente é usado apenas para fins de teste ou em ambientes onde outros storage drivers não estão disponíveis ou são incompatíveis.
NFS
O uso de volumes com NFS (Network File System) no Docker permite compartilhar dados entre contêineres e máquinas usando um sistema de arquivos em rede. Isso é útil para armazenar dados persistentes em um local centralizado, acessível por vários contêineres.
Primeiro temos que instalar o cliente NFS na máquina onde o Docker está rodando:
# No Ubuntu/Debian:
sudo apt install nfs-common
# No CentOS/RHEL:
sudo yum install nfs-utils
Agora nós podemos criar um volume Docker que monta um diretório NFS usando o comando docker volume create. Abaixo podemos ver o comando de criação do Volume Docker usando NFS:
docker volume create \
--driver local \
--opt type=nfs4 \
--opt o=addr=<nfs-server-ip>,rw,nolock \
--opt device=:/path/on/nfs \
nfs_volume
É obrigatório incluir o caractere
:no caminho do diretório ao configurar a montagem no NFS, como em:/path/on/nfs. Esse caractere separa o endereço IP do servidor do caminho exportado, resultando na sintaxe completa:<nfs-server-ip>:/path/on/nfs.
Depois de criar o volume, você pode montá-lo em um contêiner:
docker run -d \
--name app \
--mount source=nfs_volume,target=/app/data \
my-app:latest
Você também pode configurar volumes NFS em arquivos Compose.
version: '3.8'
services:
app:
image: my-app:latest
volumes:
- nfs_volume:/app/data
volumes:
nfs_volume:
driver: local
driver_opts:
type: "nfs4"
o: "addr=192.168.1.100,rw,nolock"
device: ":/data"
É fundamental garantir que o servidor NFS e o diretório exportado possuam permissões adequadas para o usuário e grupo utilizados pelo contêiner, assegurando acesso e operação sem interrupções. Além disso, o NFS deve ser implementado em redes de baixa latência para evitar problemas de desempenho. Embora um servidor mais robusto reduza eventuais gargalos, problemas de processamento são raramente perceptíveis em configurações bem otimizadas.
Recomenda-se configurar o NFS com estratégias de alta disponibilidade, como replicação ou backups regulares, para minimizar o risco de perda de dados. Adicionalmente, restringir o acesso ao servidor NFS a IPs confiáveis é uma prática essencial de segurança. A adoção de autenticação robusta e o uso de firewalls complementam essas medidas, protegendo o ambiente contra acessos não autorizados.