Volumes no Docker Compose: bind mount ou nomeado?
Saiba quando usar bind mounts ou volumes nomeados no Docker Compose, evite erros de permissao e aprenda a inspecionar, fazer backup e migrar dados.
Bind mount ou volume nomeado: a resposta curta
Os volumes do Docker Compose têm dois tipos, e a escolha depende de quem gere os ficheiros. Use um bind mount para ficheiros que escreve e lê diretamente, como configuração, modelos e sites estáticos. Use um volume nomeado para dados geridos pela aplicação, como ficheiros de bases de dados, índices de pesquisa e ficheiros multimédia enviados. Um bind mount aponta para um caminho no host que pode abrir num editor. Um volume nomeado é armazenamento que o Docker cria e gere, e ao qual acede através do Docker.
Ambos aparecem sob a mesma chave volumes: dentro de um serviço, razão pela qual são frequentemente confundidos. A diferença está no lado esquerdo dos dois pontos. Um lado esquerdo que começa por . ou / é um caminho do host, portanto é um bind mount. Qualquer outro valor é um nome, portanto é um volume nomeado, e esse nome também tem de ser declarado no bloco volumes: de nível superior.
As duas sintaxes num ficheiro Compose
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data é um volume nomeado. ./nginx.conf:/etc/nginx/nginx.conf é uma montagem bind, e :ro monta-o apenas para leitura, que é a predefinição correta para uma configuração que um contentor nunca deve reescrever. Se esquecer a entrada de nível superior volumes:, o Compose termina com service "db" refers to undefined volume pgdata.
Inicie os serviços e liste o que o Docker criou:
docker compose up -d
docker volume lsO volume não se chama pgdata. Chama-se <project>_pgdata, porque, por predefinição, o nome do projeto é o nome do diretório que contém o ficheiro Compose. Um diretório chamado myapp resulta em myapp_pgdata. Isto é importante porque mudar o nome do diretório cria um novo volume vazio, e a aplicação parece ter perdido os dados. Não perdeu: o volume antigo continua listado por docker volume ls. Fixe o nome com name: no ficheiro Compose ou defina COMPOSE_PROJECT_NAME se o diretório puder mudar de localização. Definições desse tipo devem ficar junto dos seus outros ficheiros de ambiente e segredos do Compose.
Por que os erros de permissão afetam principalmente bind mounts
Esta é a maior diferença prática, e ela vem principalmente de uma regra: um named volume vazio no primeiro uso é inicializado a partir da imagem, enquanto um bind mount nunca é.
Quando o Docker monta um named volume vazio sobre um diretório que já contém dados na imagem, ele copia esses dados para o volume, com o proprietário e as permissões definidos pela imagem. A imagem oficial do Postgres fornece /var/lib/postgresql/data pertencente ao usuário postgres, então o volume fica pertencendo ao mesmo id numérico, e o banco de dados inicia.
Essa inicialização é a única vantagem inicial de um named volume. O Docker gerencia onde o volume fica, mas os containers leem e gravam nele usando operações normais do sistema de arquivos. Por isso, proprietário e permissões são aplicados da mesma forma que em um bind mount. Um named volume montado em um caminho que não existe na imagem, ou preenchido inicialmente por outra imagem ou usuário, não recebe proprietário da imagem. Um processo que não seja root também pode ter o acesso negado nesse caso. A verificação id abaixo funciona para os dois tipos.
Um bind mount faz o oposto. Tudo que existe no host é o que o container vê, incluindo o proprietário, e o conteúdo da imagem nesse caminho fica oculto. Se o diretório do host não existir, o Docker daemon o cria, e o daemon é executado como root. Assim, você obtém um diretório pertencente a root:root. Um processo do container executado como usuário não root não pode gravar nele:
PermissionError: [Errno 13] Permission denied: '/data/app.db'A correção é fazer os números coincidirem. A propriedade em um bind mount é comparada pelo id numérico do usuário, não pelo nome, porque o container tem seu próprio /etc/passwd. Um usuário chamado app dentro do container não significa nada no host. Uid 1000 significa uid 1000 nos dois lados, exceto quando o daemon é executado com remapeamento de user namespace (userns-remap, definido em /etc/docker/daemon.json ou na linha de comando dockerd). O remapeamento desloca cada id do container para um intervalo subordinado no host. No intervalo de exemplo do Docker, que começa em 231072, o uid 0 do container corresponde ao uid 231072 no host. Portanto, o uid 1000 do container corresponde ao uid 232072 no host.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id exibe o uid com o qual o processo do container é realmente executado. Sem remapeamento, atribua o diretório do host a esse número ou fixe o container no seu número usando user: "1000:1000" no serviço. Com o remapeamento ativado, user: não pode fixar o container no seu número do host, porque o id definido também é deslocado: user: "1000:1000" ainda corresponde ao uid 232072 no host nesse intervalo de exemplo. Altere o proprietário do diretório do host para o id do host após o mapeamento (seu próprio intervalo está em /etc/subuid e /etc/subgid). A documentação do Docker indica apenas --userns=host em docker container create, run ou exec como forma de desativar o remapeamento para um único container. Fixar user: é mais simples para uma aplicação desenvolvida por você. Alterar o proprietário do diretório do host é mais seguro para uma imagem que você não desenvolveu, porque algumas imagens iniciam um entrypoint como root, reduzem os privilégios e esperam uma propriedade específica nos diretórios internos.
Vale conhecer mais duas armadilhas. No Fedora, RHEL e em outros sistemas com SELinux (security-enhanced Linux) no modo enforcing, um bind mount é bloqueado até receber a relabeling. Por isso, adicione :z para um caminho compartilhado entre containers ou :Z para um caminho que apenas um container deve usar, escrito como - ./data:/data:Z. Além disso, um bind mount de um único arquivo, em vez de um diretório, deixa de funcionar quando um editor substitui o arquivo em vez de gravá-lo no local, porque o mount acompanha o inode original. O container continua vendo o conteúdo antigo até ser reiniciado. Monte o diretório pai quando o arquivo for editado com frequência.
Desempenho: onde a diferença é real
Num servidor Linux, os dois tipos seguem o mesmo caminho no kernel. Por isso, a diferença de throughput é pequena e não deve ser um critério de escolha. Os volumes nomeados que usam o driver padrão local ficam no mesmo sistema de ficheiros que o resto do Docker, em /var/lib/docker/volumes/. Um bind mount fica no local que definiu.
A diferença aparece no Docker Desktop para macOS e Windows, onde os contentores são executados numa máquina virtual. Nesse caso, um bind mount atravessa o sistema de ficheiros do host até à máquina virtual através de uma camada de partilha de ficheiros. Cargas de trabalho com muitas operações de ficheiros pequenos, como uma árvore de dependências do Node.js ou a cache de uma framework PHP, ficam visivelmente mais lentas. Os volumes nomeados permanecem dentro da máquina virtual e não têm esse custo. É por isso que tantos ficheiros Compose de desenvolvimento fazem bind mount do diretório do código-fonte, mas declaram um volume nomeado sobre node_modules.
A outra diferença real é o local onde os dados são gravados. Um bind mount para /mnt/backup grava os dados nesse disco. Um volume nomeado é gravado no sistema de ficheiros que contém /var/lib/docker, normalmente o disco raiz numa VPS. Uma base de dados que cresça dentro de um volume nomeado enche o mesmo disco onde estão os logs do sistema. Verifique o espaço antes que isso cause um incidente:
docker system df -v
df -h /var/lib/dockerdocker system df -v lista todos os volumes com o respetivo tamanho e marca os volumes que já não são referenciados por nenhum contentor.
Inspecionar um volume nomeado
Um volume nomeado não é uma caixa-preta. Pergunte ao Docker onde ele está:
docker volume inspect myapp_pgdataO campo Mountpoint apresenta um caminho real no host, normalmente /var/lib/docker/volumes/myapp_pgdata/_data. Pode lê-lo com sudo ls, o que é útil para uma verificação rápida. Não o trate como um local para editar ficheiros. Escrever nesse caminho como root recria o problema de propriedade descrito acima, e o caminho é um detalhe do controlador local que outros controladores de volumes não partilham.
A forma segura de consultar o conteúdo é usar um contentor temporário que monte o volume:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volIsto funciona com qualquer controlador, apresenta as mesmas permissões que o contentor real vê e não deixa resíduos devido a --rm.
Fazendo backup de cada tipo
Um bind mount é um diretório comum, portanto qualquer ferramenta de backup ao nível de ficheiros já o trata. Aponte o backup para o caminho no host e está concluído. Um volume nomeado requer um passo adicional, porque a ferramenta precisa de aceder ao seu conteúdo. Monte o volume e um diretório do host no mesmo contentor temporário e, em seguida, crie um arquivo:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .Restaure invertendo o processo num volume novo:
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar preserva a propriedade numérica quando é executado como root dentro do contentor. É isso que mantém o volume restaurado utilizável pela aplicação.
Há uma ressalva aplicável aos dois tipos. Copiar os ficheiros de uma base de dados enquanto ela está em execução cria um arquivo de um estado em mudança e pode restaurá-lo num estado corrompido. Pare o serviço primeiro ou faça um dump através da própria ferramenta da base de dados, como em docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. Isso produz um ficheiro simples que pode depois incluir numa rotina normal de backup encriptado com restic, juntamente com os ficheiros do compose.
Migrar um bind mount para um volume nomeado
A operação faz uma cópia, não uma renomeação, e demora cerca de um minuto.
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a preserva o proprietário, as permissões e os timestamps. Assim, o utilizador do contentor que conseguia ler o diretório antigo continua a conseguir ler o novo volume. Depois, altere o serviço para usar pgdata:/var/lib/postgresql/data, adicione pgdata ao bloco volumes: de nível superior, execute docker compose up -d e consulte os logs da aplicação antes de eliminar o diretório antigo. Para fazer o processo inverso, use o mesmo comando, trocando /from e /to de posição.
Tenha um aspeto em mente durante os testes. docker compose down não altera os volumes nomeados, mas docker compose down -v elimina todos os volumes nomeados declarados pelo projeto e não existe forma de desfazer essa operação. Um bind mount sobrevive a ambos, porque o Docker nunca foi proprietário desse diretório. Se os comandos do ciclo de vida ainda forem novos para si, o guia básico do Docker Compose para um VPS explica-os passo a passo.
Escolha, serviço a serviço
Pergunte quem escreve o ficheiro. A configuração que edita num editor de texto e guarda no git deve ficar num bind mount, montado em :ro, porque precisa de estar visível e sob controlo de versões. O estado da aplicação que nunca abre manualmente deve ficar num volume nomeado, porque o Docker inicializa as permissões a partir da imagem e os dados não dependem de um caminho no host.
O caso misto é o dos ficheiros multimédia. Uma biblioteca de fotografias é escrita pela aplicação, mas também é gerida por si e, muitas vezes, é suficientemente grande para justificar um disco específico. Monte-a por bind mount num caminho desse disco e defina explicitamente o proprietário uma vez. Este é o padrão adotado pela maioria das stacks self-hosted: volumes nomeados para bases de dados e caches, bind mounts para a configuração e para o diretório grande que pretende preservar. Um suporte técnico como o Chatwoot executado num VPS enquadra-se exatamente nesse modelo, com o Postgres num volume nomeado e os anexos carregados num caminho que pode indicar a uma rotina de backup.
FAQ
Qual é a diferença entre um bind mount e um named volume?
Um bind mount mapeia um caminho do host para dentro do container. Assim, os dois lados veem o mesmo diretório, que pode ser editado com ferramentas normais. Um named volume é um armazenamento criado e gerenciado pelo Docker, identificado por nome e declarado no bloco de nível superior volumes:. Na prática, a divisão é baseada na propriedade: use bind mounts para configurações que você mantém e named volumes para dados mantidos pela aplicação.
Por que recebo "permission denied" com um bind mount, mas não com um named volume?
Um named volume vazio é inicializado a partir da imagem. Por isso, normalmente herda a propriedade definida pela imagem, e o usuário do container pode gravar nele. As permissões continuam sendo aplicadas. Portanto, ele pode falhar da mesma forma se não houver nada nesse caminho da imagem. Um bind mount exibe o diretório do host exatamente como ele é. Se o Docker precisou criar esse diretório, ele o criou pertencendo a root. Execute docker compose exec <service> id para ver o id numérico usado pelo container. Depois, use sudo chown -R <uid>:<gid> no diretório do host ou, quando o daemon não remapear user namespaces, defina user: "1000:1000" no serviço. Se o daemon usar remapeamento de user namespace, altere o proprietário para o id correspondente no host. Os ids do container são deslocados no host, e user: não altera isso.
Onde o Docker armazena named volumes no disco?
Com o driver padrão local, eles ficam em /var/lib/docker/volumes/<volume>/_data, e docker volume inspect <volume> exibe o Mountpoint exato. Leia esse caminho se precisar verificar algo, mas grave nele apenas por meio de um container. Editar como root no host altera a propriedade de formas que o container não espera.
Como faço backup de um named volume?
Execute um container temporário com o volume e um diretório do host montados. Depois, arquive os dados de um para o outro usando docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. Para um banco de dados, faça um dump com a ferramenta do próprio banco em vez de copiar arquivos ativos. Uma cópia feita enquanto há gravações em andamento pode ser restaurada em um estado corrompido.
O docker compose down exclui meus volumes?
docker compose down remove containers e redes, mas mantém os named volumes. docker compose down -v também exclui permanentemente todos os named volumes declarados pelo projeto. Os bind mounts nunca são removidos por nenhum dos dois comandos, porque esse diretório pertence ao host, não ao Docker.