Docker Compose passo a passo para quem vem de VMs

Docker Compose passo a passo com PHP e MariaDB para quem vem de VMs. Teste volume, rede e falhas, e evite conflito de faixa IP com o host.

No começo da carreira, operei aplicações PHP em EC2 com Nginx e PHP-FPM direto no host, com o banco no RDS. Quando o Docker entrou nesse cenário, o que mais me pegou não foi o Dockerfile, foi a rede: a rede criada pelo Docker entrou em conflito com a rede local da EC2, na faixa 172.30, e o container simplesmente não subia.

Para quem vem de VMs, a dúvida central é onde cada coisa roda: qual endereço usar, qual porta acessar e onde ficam os dados. Este passo a passo responde isso na prática. Você sobe uma aplicação PHP com MariaDB usando Docker Compose, recria os containers e para o banco de propósito para ver o que acontece.

Antes de começar Para acompanhar, você precisa de Docker e Compose funcionando e um terminal Bash no Linux ou WSL. O exemplo usa PHP simples, sem instalar Laravel. Testado na distro WSL Ubuntu-26.04; Docker Desktop e macOS não foram testados.

Início rápido: suba aplicação e banco

1. Confira as ferramentas e crie a pasta

docker version
docker compose version
mkdir compose-vms-lab
cd compose-vms-lab

O primeiro comando precisa mostrar cliente e servidor. Se aparecer erro de conexão com o daemon, resolva isso antes de seguir. O segundo precisa reconhecer docker compose, com espaço. Para instalar, use as instruções oficiais de Docker Engine e do plugin Compose.

A pasta vai conter compose.yaml, Dockerfile, index.php e .env. Crie os três primeiros com seu editor. O .env será gerado no passo seguinte.

2. Gere a senha do laboratório

umask 077
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
printf '.env\n*.sql\n' > .gitignore
printf '.env\n*.sql\n.git\n' > .dockerignore

Faça isso uma única vez, na pasta nova. O comando depende de openssl. Não compartilhe nem versione o .env. O .dockerignore também impede que esse arquivo entre no contexto enviado ao build.

Não regenere a senha depois de criar o banco As variáveis de inicialização do MariaDB configuram o banco quando o diretório de dados está vazio. Trocar apenas o .env depois disso não altera automaticamente a senha já gravada no banco. Você pode acabar com a aplicação usando uma senha e o banco esperando outra.

3. Crie a aplicação PHP

Salve como index.php. A aplicação abre uma conexão, cria uma tabela didática e incrementa um contador a cada acesso. Esse comportamento permite observar se o dado sobrevive à recriação dos containers.

<?php
header('Content-Type: text/plain; charset=utf-8');
try {
    $dsn = 'mysql:host=' . getenv('DB_HOST') . ';dbname=' . getenv('DB_DATABASE') . ';charset=utf8mb4';
    $db = new PDO($dsn, getenv('DB_USERNAME'), getenv('DB_PASSWORD'), [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_TIMEOUT => 3,
    ]);
    $db->exec('CREATE TABLE IF NOT EXISTS visitas (id INT PRIMARY KEY, total INT NOT NULL)');
    $db->exec('INSERT INTO visitas (id, total) VALUES (1, 1) ON DUPLICATE KEY UPDATE total = total + 1');
    $total = $db->query('SELECT total FROM visitas WHERE id = 1')->fetchColumn();
    echo "Aplicacao PHP conectada ao banco.\nVisitas: $total\n";
} catch (PDOException $e) {
    http_response_code(503);
    error_log('Banco indisponivel; SQLSTATE: ' . $e->getCode());
    echo "Banco indisponivel. Consulte os logs do laboratorio.\n";
}

O usuário do banco pode criar essa tabela para simplificar o exercício. Em uma aplicação de produção, permissões e migrações merecem outro desenho. A resposta de falha é genérica; o navegador não recebe a mensagem completa da exceção.

4. Crie a imagem da aplicação

Salve como Dockerfile:

FROM php:8.4-cli
RUN docker-php-ext-install pdo_mysql
WORKDIR /app
COPY index.php /app/index.php
USER www-data
CMD ["php", "-S", "0.0.0.0:8000", "-t", "/app"]

Esse arquivo parte da imagem oficial PHP, instala o driver de conexão com MySQL/MariaDB e copia o programa. O processo da aplicação usa www-data. A documentação da imagem PHP explica o instalador de extensões utilizado aqui. Para imagens de produção menores, veja build multistage no Docker.

Servidor apenas para estudo O servidor embutido do PHP atende ao laboratório local. Não transforme esse comando em um deploy público. O próprio manual do PHP delimita seu uso para desenvolvimento e testes.

5. Descreva os dois serviços

Salve como compose.yaml:

name: devopsmind-compose-lab
services:
  app:
    build: .
    ports:
      - "127.0.0.1:8080:8000"
    environment:
      DB_HOST: db
      DB_DATABASE: laboratorio
      DB_USERNAME: aluno
      DB_PASSWORD: ${DB_PASSWORD:?Defina DB_PASSWORD no arquivo .env}
    depends_on:
      db:
        condition: service_healthy
  db:
    image: mariadb:11.4
    environment:
      MARIADB_DATABASE: laboratorio
      MARIADB_USER: aluno
      MARIADB_PASSWORD: ${DB_PASSWORD:?Defina DB_PASSWORD no arquivo .env}
      MARIADB_RANDOM_ROOT_PASSWORD: "1"
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 20s
volumes:
  db_data:

Leia o arquivo procurando responsabilidades. app é o serviço PHP. db é o banco. build: . manda construir a imagem da aplicação usando o diretório atual. O banco usa uma imagem pronta. db_data guarda os dados fora da camada gravável do container. O Compose também aceita o nome antigo docker-compose.yml; compose.yaml é o nome padrão atual, conforme a referência do arquivo Compose.

O banco não tem ports. A aplicação consegue conversar com ele pela rede do projeto sem publicar a porta no computador. A senha chega aos dois serviços por uma variável do ambiente. O usuário root recebe uma senha aleatória gerada pela imagem e não é usado pelo PHP.

Por que o banco está em container aqui O MariaDB roda em container para o laboratório ficar autocontido. No cenário de EC2 que descrevi na abertura, o banco ficava no RDS, fora da máquina. O raciocínio de rede é o mesmo nos dois casos: a aplicação fala com um nome, não com um IP fixo.

As tags php:8.4-cli e mariadb:11.4 fixam a linha de versão, mas recebem atualizações. Para algo além do laboratório, fixe a imagem por digest.

6. Suba e confira a resposta

docker compose config --quiet
docker compose up -d --build --wait --wait-timeout 180
docker compose ps
curl -i http://localhost:8080

config --quiet valida a configuração sem imprimir o arquivo interpolado com a senha. O build e o primeiro download dependem de internet. Se o tempo limite for atingido, confira ps e os logs antes de repetir os comandos.

O resultado esperado é HTTP 200 e um corpo neste formato:

Aplicacao PHP conectada ao banco.
Visitas: 1

Abra também http://localhost:8080 no navegador. O contador deve aumentar a cada requisição ao PHP, inclusive acessos adicionais feitos pelo navegador. Não espere um número fixo depois de abrir várias abas.

Resposta real da aplicação PHP conectada ao MariaDB, com contador de visitas igual a 1.
Captura real do laboratório em Ubuntu-26.04: aplicação PHP conectada ao banco após o primeiro acesso.

Confira se o dado sobrevive à recriação

Comparação do volume db_data antes e depois de recriar os containers sem apagar o volume.
Exemplo didático: o volume permanece, e o novo acesso incrementa o contador.

Anote o contador e execute:

docker compose down
docker compose up -d --wait --wait-timeout 180
curl http://localhost:8080

O contador deve continuar do valor anterior. Esse é o critério do exercício: o programa foi recriado, mas a tabela continuou no volume nomeado. Mantenha o mesmo diretório e o mesmo nome de projeto durante o teste.

Segundo a referência de docker compose down, remover o projeto com esse comando não remove os volumes nomeados por padrão. Acrescentar --volumes muda essa operação.

Volume não é backup Persistir dados entre containers não protege contra falha do disco, exclusão do volume ou um comando SQL errado. Backup do banco, restauração e retenção são tarefas próprias. Este laboratório não implementa essa proteção.

Quebre uma dependência e acompanhe o diagnóstico

Pare somente o banco:

docker compose stop db
docker compose ps -a
curl -i http://localhost:8080
docker compose logs --tail=20 app

stop não é down stop só para os containers, que continuam existindo. down remove containers e a rede do projeto, mas mantém os volumes nomeados, a menos que você passe --volumes.

O esperado é HTTP 503 com uma mensagem de banco indisponível. O processo PHP pode continuar em execução, mas isso não significa que a aplicação esteja funcional. Compare o estado dos dois serviços com a resposta HTTP. Se o DNS do ambiente estiver lento, a resposta de erro pode demorar alguns segundos.

Recupere o projeto e tente novamente:

docker compose up -d --wait --wait-timeout 180
curl -i http://localhost:8080

O esperado é voltar ao HTTP 200 com os dados anteriores. O healthcheck.sh vem da imagem MariaDB e testa a prontidão do banco, conforme a documentação oficial do healthcheck.

depends_on com service_healthy coordena a inicialização. Ele não transforma Compose em um sistema que resolve toda queda posterior de dependência. Leia as condições e limites na documentação de ordem de inicialização.

O que muda no raciocínio de quem opera VMs

Referência de infraestruturaComo pensar neste laboratório
VM com sistema operacional e serviços instaladosContainer executa o processo definido pela imagem, com isolamento; não é uma VM completa
Instalar PHP manualmente no servidorDockerfile descreve como montar a imagem com PHP e extensão
Iniciar app e banco separadamenteCompose gerencia os dois serviços declarados no projeto
Guardar tudo no disco da máquinaSeparar a imagem da aplicação dos dados persistentes do banco
Entrar por SSH para investigarComeçar por estado, logs e comandos pontuais com docker compose exec
Fixar IP do servidor na configuraçãoUsar o nome db para descobrir o serviço na rede Compose

Container não é servidor para ajustar na mão Um vício comum de quem vem de VM é entrar com docker compose exec para instalar um pacote ou editar um php.ini. Funciona até o container ser recriado, por um down seguido de up ou por um up --build com imagem nova: ele volta ao que a imagem define. Ajuste que precisa ficar vai para o Dockerfile ou para o compose.yaml.

Containers Linux compartilham o kernel do ambiente Linux que os executa. No Docker Desktop, esse ambiente pode estar dentro de uma VM. Containers e VMs podem coexistir. A distinção está no modelo de execução e isolamento, não em proibir uma tecnologia quando você adota a outra.

Se imagem, build e container ainda se misturam, volte à base em Docker e o papel da imagem no pipeline. O Compose usa essas peças; ele não substitui o Dockerfile.

Rede: de onde você está tentando conectar?

Navegador acessa a aplicação pela porta publicada; app encontra o banco pelo nome db.
O endereço muda conforme o ponto de partida da conexão.

Uma dificuldade que encontrei foi separar os IPs dos containers, os endereços da rede local e a própria rede Docker. Antes de trocar um IP na configuração, localize quem está iniciando a conexão.

OrigemDestino neste labEndereço e porta
Navegador no computador que executa o labAplicação publicadalocalhost:8080
Serviço appServiço dbdb:3306
PHP dentro de appO próprio container applocalhost aponta para o próprio ambiente do processo, não para db
Outro computador da LANAplicação do labNão há publicação intencional na interface da LAN nesta configuração

Em 127.0.0.1:8080:8000, a primeira parte seleciona a interface local do host, a segunda é a porta publicada e a terceira é a porta em que o PHP atende dentro do container. Já 0.0.0.0:8000, no comando PHP, faz o processo escutar nas interfaces do container; isso não equivale a publicar a aplicação na LAN. A referência de publicação de portas explica o mapeamento e as ressalvas, incluindo a possibilidade de acesso pela mesma rede local em versões do Engine anteriores à 28.

Entre os serviços, use db. Não copie o IP observado em uma inspeção para DB_HOST. O Compose fornece descoberta pelo nome do serviço, e o endereço do container pode mudar. A documentação de rede Compose detalha essa separação.

Faça a pergunta antes do comando O navegador está no host e usa a porta publicada. O PHP está dentro de app e usa o serviço db. Desenhar esses dois caminhos no papel já elimina boa parte da confusão com localhost.

Confira nome e conexão a partir da aplicação

docker compose exec app php -r 'echo gethostbyname("db"), PHP_EOL;'
docker compose exec app php -r '$s = @fsockopen("db", 3306, $errno, $erro, 3); echo $s ? "TCP OK\n" : "TCP FALHOU\n";'
docker compose port app 8000

O primeiro comando deve devolver o IP resolvido para db; se devolver apenas o texto db, a resolução falhou. O segundo verifica se consegue abrir a porta TCP. TCP OK não valida usuário, senha ou tabela, apenas essa conexão. O terceiro mostra a publicação da porta da aplicação.

Quando a rede do Docker colide com a rede do host

Esse foi o problema que vivi na EC2. O Docker cria redes bridge em faixas privadas, e cada projeto Compose ganha a sua. No meu caso, a rede do Docker conflitou com a rede local da instância, na faixa 172.30, e o sintoma foi o container falhando na subida. Para quem vem de VM, a pegadinha é que essas faixas parecem “internas do Docker”, mas dividem a tabela de rotas com o host.

A correção combinou três passos: ajustar o daemon.json para o Engine usar faixas que não conflitam com a rede da VPC, fixar a sub-rede do projeto no Compose e remover a rede conflitante. A remoção é necessária porque mudar o daemon não altera redes que já existem; ela vale para as próximas que forem criadas.

ip route
docker network inspect devopsmind-compose-lab_default --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

Compare a sub-rede do projeto com as rotas que o host já usa. Para evitar a colisão, defina faixas que não conflitam com a sua rede: para o Engine inteiro, com default-address-pools na configuração do daemon; para um projeto, com ipam na definição de redes do Compose.

Use a inspeção do host como uma camada do diagnóstico

No meu caso, foi o ip a que esclareceu a rede: as interfaces docker0 e br-... aparecem ali com seus blocos de IP, ao lado das interfaces da própria máquina. No Linux que hospeda o Engine, comece por:

ip a
docker network ls
docker network inspect devopsmind-compose-lab_default

O nome acima decorre do name definido no arquivo. Ele muda se você sobrescrever o nome do projeto. Consulte docker network ls em vez de assumir que toda rede Docker se chama docker0.

Para conhecer a rede deste projeto, prefira a inspeção do Docker a arquivos de rede da distribuição, e não edite o host para tentar corrigir DB_HOST.

Erros que valem investigar por camada

SintomaO que verificarPróxima ação
Docker não mostra servidor em docker versionEngine e contexto selecionadoResolver o acesso ao daemon antes do Compose
Porta 8080 ocupadaOutro processo ou projeto usando a portaTrocar só a porta do host, por exemplo para 127.0.0.1:8081:8000
app não encontra dbNome do serviço e rede compartilhadaTestar resolução dentro de app
TCP responde, mas PHP retorna 503Credenciais, nome do banco e inicializaçãoConferir logs de app; comparar variáveis sem divulgá-las
Senha mudou no .env e conexão parouVolume inicializado com outra senhaRestaurar a configuração anterior ou alterar a senha pelo banco
Banco demora a ficar saudávelPull, recursos e logs de inicializaçãoLer logs de db localmente e conferir o healthcheck
Container não sobe e a rede do projeto conflita com a rede do hostSobreposição entre a sub-rede Docker e as rotas do host (ip route)Definir faixas com default-address-pools ou ipam e remover a rede conflitante

No Windows, use Bash no WSL para os comandos deste artigo. Com Docker Desktop, a rede dos containers vive no ambiente administrado pelo Desktop; ip a no WSL ou no host não mostra necessariamente as mesmas interfaces do Engine. A documentação de rede do Desktop descreve essas diferenças.

Encerre o laboratório sabendo o que será removido

Para encerrar preservando o volume:

docker compose down

Somente se quiser apagar os dados didáticos e começar do zero:

docker compose down --volumes

Execute na pasta do laboratório, confira o projeto e não use comandos de limpeza global. As imagens podem continuar no cache local; isso é separado da vida dos containers e dos dados.

Minha leitura: onde o Compose cabe

Minha regra continua a mesma do post sobre Docker: Compose no ambiente local e em serviço de uma máquina só. Em EC2 de serviço, como um Zabbix com banco e aplicação juntos, ele funcionou bem e não deu trabalho no dia a dia. Quando a aplicação precisa escalar e se recuperar sozinha, o lugar é o Kubernetes.

Antes de seguir, faça três coisas sem consultar o YAML: diga qual endereço o PHP usa para o banco, mostre onde o dado persiste e encontre o primeiro comando para investigar uma falha. Quando isso fizer sentido, o laboratório de Kubernetes local com Kind passa a ser um próximo passo com contexto.

Compartilhe / Share
Fernando Müller Junior
Fernando Müller Junior

SRE Manager na Appmax. 16 anos de infraestrutura, de datacenter a cloud. Escrevo sobre o que opero em produção.

Artigos: 35

Receba as notícias por email / Receive news by email

Insira seu endereço de e-mail abaixo e assine nossa newsletter / Enter your email address below and subscribe to our newsletter

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *