Para podermos salvar dados, primeiro devemos entender como o Docker funciona. Vamos supor que o Docker é formado por camadas, tipo uma cebola
.
As imagens que usamos, que estão nas camadas mais abaixo, são somente-leitura, ou seja, são blindadas, ninguém mexe
. Acima delas tem a camada de container, que é leitura e escrita. Quando a gente grava um arquivo, o conteúdo fica na camada do container, e a imagem não é alterada. Enquanto o container existir, os dados que estão dentro dele permanecerão, mesmo se pararmos ele (com o stop), é como deixar um carro na garagem. Já quando o container é removido (com o rm), essa camada é destruída e tudo que estava nela deixará de existir.
Pra gente salvar os dados, devemos furar
essa camada do container, pra conseguir gravar diretamente no HD do nosso computador (host). Para isso temos três maneiras:
Vamos supor que temos um arquivo index.html escrito que o site está em manutenção. Nós podemos atualizar o código no Docker sem dar rebuild e colocar ele no nosso container, na pasta html do Nginx.
Pra criar o volume, vamos utilizar a flag -v no prompt. O lado esquerdo representa a pasta do nosso computador e o lado direito representa o diretório do container (no caso, a pasta html do Nginx, de forma parecida com o mapeamento de portas).
Digite esse comando:
docker run -d -p 8080:80 -v %cd%:/usr/share/nginx/html --name site-bind nginx
PS: O %cd% representa o comando do CMD do Windows que retorna a pasta atual. Se usarmos outro shell, como o do Linux, usaremos $(pwd). Podemos passar o caminho literal no lugar dele também.
Ao rodar o comando, o container já estará rodando e fará a associação da pasta do nosso computador com o diretório do Nginx. Mas o projeto continuará no nosso computador, mesmo assim, poderemos acessar a URL pelo link http://localhost:8080/.
Dessa forma, podemos até excluir o container que os arquivos do projeto não serão perdidos, pois eles estão no nosso computador, e não no container. Também poderemos alterar o arquivo no nosso computador e ele atualizará o link do localhost também, sem a necessidade de reconstruir o container do Docker.
No entanto, isso tem uma desvantagem: Ele só rodaria sem problemas no nosso computador, podendo dar problemas caso transfira o mesmo pra outro computador. Pra isso usaremos outros recursos.
Como visto anteriormente, ao usar o Bind Mounts para ligar uma pasta do nosso computador a um container, de forma que a exclusão do container não afetará nossos arquivos. Mas isso ainda não deixa os arquivos totalmente seguros, já que podemos excluir os mesmos sem querer. Pra resolver isso, usaremos os volumes.
Os volumes gerenciados são criados em uma área segura do Docker, longe dos arquivos do usuário. Basicamente usaremos o mesmo comando do Bind Mounts, mas com algumas diferenças, ao invés de especificar um local no computador, especificaremos um nome para o volume.
Vamos exemplificar com um simples arquivo HTML, e crie o container com o comando docker run -d -p 8080:80 -v meu-volume:/usr/share/nginx/html --name site-vol nginx, no caso, declararemos o nome do volume a ser criado no lugar da pasta do computador. O Docker perceberá que não é um caminho de sistema e criará um volume em uma área segura dentro dele. O container rodará em segundo plano.
Só que, ao criar e rodar o container, o arquivo index não estará lá ainda, podemos conferir acessando http://localhost:8080/. Nós teremos que copiar os arquivos do nosso dispositivo para dentro do volume, pra isso usaremos a opção cp
. Digite docker cp . site-vol:/usr/share/nginx/html (o ponto significa todos os arquivos na pasta atual, podemos especificar um arquivo específico). Nesse caso o arquivo exibido em localhost estará dentro do volume, sem nenhum vínculo ao arquivo no nosso computador, já que ele não estará com a pasta associada.
PS: Para atualizar o arquivo já existente, basta fazer um novo cp pro volume, da mesma forma, como docker cp index.html site-vol:/usr/share/nginx/html.
Caso o container seja excluído, o volume criado (no caso meu-volume) ainda estará na área segura do Docker, portanto, poderemos associar um novo container com esse mesmo volume, que os arquivos estarão todos lá intactos. Podemos ver no Docker Desktop os volumes criados. Lembrando que, ao enviar o container pra outro computador, os arquivos só poderão ser acessados caso enviemos também o volume associado.
Vamos fazer um teste com um banco de dados, como o MySQL, e que nós cadastremos vários clientes, por exemplo. Caso o container seja removido, perderemos esses dados, mas podemos persistir os dados com volumes também.
Crie um container com a imagem MySQL, informando a senha root do MySQL, com o comando docker run -d --name mysql-teste -e MYSQL_ROOT_PASSWORD=123 mysql. Ele criará e rodará o container em segundo plano. Caso deseje criar uma senha vazia, passe no parâmetro -e
o valor -e MYSQL_ALLOW_EMPTY_PASSWORD=yes.
PS: Como não usaremos mais o container site-vol, podemos parar ele com docker stop site-vol.
Para criar um banco de dados dentro desse container, usaremos o comando exec
, na forma docker exec -it mysql-teste mysql -p (atenção que ele pedirá a senha padrão na primeira vez, que é vazia, e espere alguns segundos pro servidor MySQL iniciar totalmente, caso dê erro rode o comando docker exec -it mysql-teste mysqld para iniciar totalmente o MySQL). Ele entrará no MySQL do container, pedirá a senha e já poderemos manipular o banco de dados. Rode esses comandos dentro do MySQL:
create database cursos;
use cursos;
create table alunos (
nome varchar(50)
)
default charset = utf8;
insert into alunos values
('Fulano'),
('Beltrano'),
('Sicrano');
select * from alunos;
Agora vamos excluir o container de forma forçada usando o comando docker rm -f mysql-teste, pra simular um container excluído acidentalmente. No caso, todos os dados estariam perdidos.
Ao rodar docker volume ls, veremos todos os volumes que estão no nosso Docker, como o meu-volume criado, e outros com nomes aleatórios.
Vamos rodar novamente o comando pra criar o container da mesma forma, docker run -d --name mysql-teste -e MYSQL_ROOT_PASSWORD=123 mysql. O banco de dados criado anteriormente, assim como seus dados, não estarão mais lá. Podemos entrar no MySQL com o comando docker exec -it mysql-teste mysql -p e rodar dentro do MySQL o comando show databases;.
Pra criar um container com banco de dados persistente com um volume, vamos usar o comando docker run -d --name mysql-pro -e MYSQL_ROOT_PASSWORD=123 -v meus-dados:/var/lib/mysql mysql pra ele criar um volume chamado meus-dados
que armazenará nossos dados. A pasta /var/lib/mysql é a pasta padrão do MySQL. Veja se ele criou o volume rodando o comando docker volume ls.
Vamos executar o comando docker exec -it mysql-pro mysql -p e rodar esses comandos no MySQL (lembre-se que pode dar erro caso o servidor não esteja rodando totalmente, então aguarde, e na primeira vez pode pedir a senha padrão, que é vazia). Dentro do MySQL, digite novamente esses comandos:
create database cursos;
use cursos;
create table alunos (
nome varchar(50)
)
default charset = utf8;
insert into alunos values
('Fulano'),
('Beltrano'),
('Sicrano');
select * from alunos;
Saia do MySQL e acidentalmente
exclua o container com o comando docker rm -f mysql-pro, e depois rode o comando docker ps -a pra ver se o container realmente foi excluído, e depois rode o comando docker volume ls pra ver se o volume meus-dados
continua lá.
Crie novamente o container com o comando docker run -d --name mysql-pro -e MYSQL_ROOT_PASSWORD=123 -v meus-dados:/var/lib/mysql mysql, aguarde o banco de dados carregar, rode o comando docker exec -it mysql-pro mysql -p e dentro do MySQL, rode esses comandos:
show databases;
use cursos;
select * from alunos;
Podemos ver que os dados colocados anteriormente continuaram no volume (incluindo a senha configurada anteriormente), e estarão guardados exatamente o que colocamos no container anteriormente excluído.
Lembrando que o Docker é Linux, e os arquivos do volume estarão lá dentro. Podemos rodar o comando docker volume inspect meus-dados para ver os dados referentes a esse volume que criamos. Os dados estarão no nosso HD, mas dentro do Docker.
Para excluir um volume, use o nome do volume com o rm, como docker volume rm meu-volume. Só poderemos excluir volumes que não estejam sendo usados por nenhum container, nesse caso exclua todos os container que usam esse volume primeiro. Para excluir todos os volumes não utilizados, use o comando docker volume prune.
Vamos pensar no Docker como um condomínio fechado, e os container são as casas do mesmo, e de dentro delas, nós temos saída pras ruas, mas ninguém que tá na rua tem acesso ao condomínio sem a autorização da portaria. Isso tem a ver com o parâmetro -p que usamos na criação do container. Para ver as redes do container, nós usamos o comando docker network, digitando assim, nós listamos as opções do comando, para listar as redes usamos docker network ls
Por padrão, ele lista três tipos de redes: Bridge, host e none, com essas diferenças:
Vamos criar dois containers com o Alpine, com os comandos docker run -dit --name container1 alpine e docker run -dit --name container2 alpine. Os dois rodarão em segundo plano e podemos ver isso com o comando docker ps. Para visualizar as configurações de um dos containers, digite o comando docker inspect container1, ele mostrará um JSON com as configurações, e procurando por NetworkSettings
, podemos ver o IP privado do container, que será específico dele (algo como 172.17.0.2, por exemplo). Faça o mesmo com o segundo container e pegue o IP dele (que deverá ser, no caso, 172.17.0.3, portanto estarão na mesma rede os dois containers, como se tivessem ligados a um switch
virtual).
Com essas informações, nós podemos acessar um container e acessar ou executar algo do outro container pela rede. Vamos supor que no container1 nós executamos o ping no container2, pelo IP. Pra isso digite o comando docker exec -it container1 ping 172.17.0.3, e nós conseguiremos pingar
o outro container. Mas os containers são voláteis, e ao reiniciar o container, ele pode pegar outro IP diferente, nesse caso usamos o DNS (nomes), mas no modo bridge, que é o que usamos agora, o DNS não vai funcionar.
Vamos tentar pingar
o container de novo, mas com o nome dele ao invés do IP, com docker exec -it container1 ping container2, ele não conseguirá acessar o outro container e dará erro.
Podemos melhorar o desempenho da rede sem que tenhamos que passar pelo roteador
do Docker, nesse caso usaremos o modo host. No modo host, o container rouba
o IP da nossa máquina, ele deixa de ter um isolamento por porta. Podemos utilizar por exemplo a imagem no Nginx, que rodará a porta 80 do nosso computador, sem ter que linkarmos as portas com o parâmetro -p
. Vamos criar um container no modo host com o comando docker run -dit --network host --name nginx-direto nginx, se a porta 80 não tiver sendo usada por outro processo, poderemos acessar ela. Vamos criar um outro container com o comando docker run -dit --network host --name container3 alpine., e depois digite o comando docker exec -it container3 ping google.com pra ele pingar
o Google, e nesse caso ele usará a placa de rede do nosso computador, sem passar pela rede do Docker.
Vamos criar um quarto container totalmente isolado com o none, com o comando docker run -dit --name container4 --network none alpine, depois tente pingar
o Google com o comando docker exec -it container4 ping google.com, ele não conseguirá por estar totalmente isolado de qualquer rede.
No entanto, nós ainda temos um problema, como um container acessar outro sem saber o IP dele? Nesse caso nós usaremos DNS (nomes), que aprenderemos mais pra frente.
Para não termos problemas com mudança de IP ao reinicializar o container, podemos associar os mesmos com um nome.
Primeiramente, digite o comando docker network para vermos as opções para redes disponíveis no Docker. Para criar uma rede com um nome digite o comando docker network create rede-teste. Para listar as redes e vermos a rede criada, digite docker network ls
Ao digitar esse comando, nós criamos uma espécie de switch
virtual onde nossos container a utilizarão. Vamos criar o container de forma que ele use essa rede, com o comando docker run -dit --name ctrede1 --network rede-teste alpine ash e crie um segundo container com docker run -dit --name ctrede2 --network rede-teste alpine ash.
Vamos pingar o ctrede2 pelo ctrede1. Para isso use o comando docker exec -it ctrede1 ping ctrede2. Ele pingará pelo nome, ao invés do IP. Poderemos reiniciar os containers que, mesmo se o IP mudar, o nome permanecerá o mesmo, e poderemos pingar normalmente, da mesma forma caso o container 2 seja excluído e seja criado outro semelhante com o mesmo nome.
Temos também como associar um container criado anteriormente com a rede que criamos, posteriormente. Para testar isso, crie um container sem indicar a rede, com docker run -dit --name rede-intrusa alpine ash.
Podemos tentar pingar o rede-intrusa com o comando docker exec -it ctrede1 ping rede-intrusa, ele não encontrará o container criado. Para associarmos esse container com a rede que criamos, use o comando docker network connect rede-teste rede-intrusa, e depois disso tente pingar novamente com o mesmo comando anterior, agora podemos conectar com o nome do container, mesmo se o IP mudar.
Para ver os containers que estão usando a nossa rede, digite o comando docker network inspect rede-teste, ele retornará um JSON enorme com todos os container conectados a ele.
No entanto, ainda temos um problema, para subir uma aplicação, como um site ou um banco de dados, se usarmos vários containers diferentes dará muito trabalho e ficará mais propenso a erros, no entanto, poderemos facilitar, escrevendo esses comandos num arquivo de texto, conhecido como Docker Compose, que veremos mais pra frente.
Para facilitar a criação de container, com volume, redes e tudo o que for necessário, podemos usar o Docker Compose. Isso é útil inclusive pra compartilhar os containers. Basicamente, o Docker Compose lê um script e faz um passo-a-passo e vai executar tudo o que precisa ser feito. Para tanto, usaremos um arquivo com o nome docker-compose.yaml
(pode ser yml ou yaml).
PS: Para criar um Docker Compose, temos que prestar atenção no espaçamento e na indentação.
Vamos supor uma pasta com algum site (pode ser um simples index.html), vamos colocar ele no nosso projeto. Vamos criar na mesma pasta do projeto (não é dentro da pasta do site) um arquivo com o nome docker-compose.yaml, que terá esse código:
services:
meusite:
image: nginx:latest
container_name: meuexemplodesite
ports:
- "8080:80" # Isso são arrays, tudo que tiver após a cerquilha é comentário
volumes:
- ./website:/usr/share/nginx/html/
O service é obrigatório, e onde está o nome meusite pode ser qualquer nome, e dentro dele definimos a imagem da qual criaremos nosso container, o nome do container e o mapeamento das portas que ele utilizará, sendo a primeira do host (nosso PC) e a segunda do container. Definimos o volume a ligação da pasta do nosso computador à pasta do container.
Para vermos as opções dos comandos do Docker Compose, basta usar o comando docker compose, e pra criar o container com tudo planejado, vá na mesma pasta do arquivo yaml e digite docker compose up. Depois disso, no nosso PC, acesse o link localhost, com http://localhost:8080.
Para criar um container com o modo detach, use o comando docker compose up -d. Ele não dará erro, pois atualizará e iniciará o container criado.
Podemos também usar o Compose pra remover tudo, com o comando docker compose down, sem ter que parar coisa com coisa até remover tudo. O down não remove imagens nem volumes, apenas os containers.
No caso de nós querermos alterar algo no nosso container, como ao adicionar mais serviços, podemos editar no arquivo Docker Compose da mesma forma. Deixe o Docker Compose assim:
services:
meusite:
image: nginx:latest
container_name: meuexemplodesite
ports:
- "8080:80"
volumes:
- ./website:/usr/share/nginx/html/
meubanco:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: 123
MYSQL_DATABASE: nomedobanco
volumes:
- dados-mysql:/var/lib/mysql/
volumes:
dados-mysql:
Onde está meubanco pode ser qualquer nome também, seguindo a mesma regra do meusite. Mas ainda não execute o Docker Compose, pois podemos exigir a dependência de algo que deverá estar no container (no caso, nosso banco de dados), assim:
services:
meusite:
image: nginx:latest
container_name: meuexemplodesite
ports:
- "8080:80"
volumes:
- ./website:/usr/share/nginx/html/
depends_on:
- meubanco
meubanco:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: 123
MYSQL_DATABASE: nomedobanco
volumes:
- dados-mysql:/var/lib/mysql/
volumes:
dados-mysql:
Agora sim digite o comando docker compose up. Podemos rodar também no modo detach, com docker compose up -d, sem precisar parar e reconstruir nada. Depois podemos limpar depois com docker compose down.
PS: O Docker Compose, de certa forma, também é tipo uma documentação do nosso container.
Para administrar nosso banco de dados MySQL, podemos usar o Adminer, que é semelhante ao PHPMyAdmin, mas bem mais leve. Mas precisaremos configurar ele pra isso. Não poderemos colocar no IP localhost ou 127.0.0.1, nem o IP do container. A configuração vai ser no nosso Docker Compose. Deixe o Docker Compose assim:
services:
# Vamos deixar só o banco:
meubanco:
image: mysql:5.7
container_name: mysql-curso
environment:
MYSQL_ROOT_PASSWORD: 123
MYSQL_DATABASE: nomedobanco
volumes:
- dados-mysql:/var/lib/mysql/
minhabase:
image: adminer
container_name: minhabase
ports:
- "8090:8080"
depends_on:
- meubanco
volumes:
dados-mysql:
Para subir o serviço no modo detach, digite docker compose up -d. Ele já vai criar a rede e o volume automaticamente, além dos dois containers. Tudo isso foi descrito no Docker Compose. Ao rodar o mesmo comando novamente, ele vai rodar os container criados, sem precisar recriar tudo de novo do zero.
Para configurar o Adminer, podemos entrar no link http://localhost:8090 e lá podemos configurar nosso banco com o sitema de banco, servidor, usuário, senha e banco de dados, tudo isso foi configurado no nosso container. No caso fica assim:
Sistema: MySQL/MariaDB.
Servidor: mysql-curso.
Usuário: root.
Senha: 123.
Base de Dados: (Deixe vazio).
PS: Espere ele carregar o servidor completamente antes de tentar logar.
Podemos listar as redes criadas digitando docker network ls, e os volumes com docker volume ls.
No nosso exemplo anterior, o Docker Compose tinha a senha do banco de dados de forma literal, mas podemos usar variáveis de ambiente para mascarar
dados sensíveis.
O arquivo deverá se chamar .env
, e deverá estar na mesma pasta do Docker Compose, e ser escrito dessa forma (tudo com #
é comentário):
# Configurações do MySQL
# As variáveis podem ter qualquer nome:
DB_SENHA=123
DB_USER=root
DB_NOME=nomedobanco
# Configuração do Adminer:
PORTA_ADMINER=8090
E aí, pra usar as variáveis, basta colocar elas entre ${}, dessa forma, no Docker Compose:
services:
# Vamos deixar só o banco:
meubanco:
image: mysql:5.7
container_name: mysql-curso
environment:
MYSQL_ROOT_PASSWORD: ${DB_SENHA}
MYSQL_DATABASE: ${DB_NOME}
volumes:
- dados-mysql:/var/lib/mysql/
minhabase:
image: adminer
container_name: minhabase
ports:
- "${PORTA_ADMINER}:8080"
depends_on:
- meubanco
volumes:
dados-mysql:
Agora, exclua os containers anteriores manualmente, e as imagens deles também:
docker rm minhabase
docker rm mysql-curso
docker rmi adminer
docker rmi mysql:5.7
E aí você digita docker compose config para verificar a configuração dos valores que estão nas variáveis. Depois é só rodar o Docker Compose com docker compose up -d. Ele vai baixar as imagens novamente, criar os containers, redes e tudo mais que ele precisar.
Aí é só entrar no http://localhost:8090 novamente, e digitar os mesmos dados anteriormente no Adminer.
PS: O arquivo .env não deve ser compartilhado entre outras pessoas, e nem deve subir para repositórios como o GitHub. Nesse caso podemos colocar um arquivo pra explicar as instruções, que pode ser um arquivo de texto assim:
# Salve o arquivo como .env e coloque o nome de usuário e senha:
# Configurações do MySQL
DB_SENHA=COLOQUE_SUA_SENHA
DB_USER=COLOQUE_SEU_NOME_DE_USUARIO
DB_NOME=nomedobanco
# Configuração do Adminer:
PORTA_ADMINER=8090