Instalar PHP, MySQL, Apache e phpMyAdmin direto no computador funciona até o dia em que você precisa de duas versões diferentes do PHP, ou até um colega entrar no projeto e passar a tarde configurando tudo de novo. Com Docker Compose, PHP e MySQL sobem juntos com um único comando, com as mesmas versões para todo o time — e somem sem deixar rastro quando você não precisa mais deles.
Neste tutorial vamos montar um ambiente completo com PHP 8.4 + Apache, MySQL 8.4 e phpMyAdmin, explicando cada linha do compose.yaml. Ao final, você terá uma página PHP lendo dados do banco e o conhecimento para adaptar o mesmo modelo a Laravel, WordPress ou qualquer outro projeto PHP.
O que é o Docker Compose
O Docker Compose é uma ferramenta para definir e executar aplicações com vários containers. Em vez de digitar longos comandos docker run para cada serviço, você descreve tudo em um arquivo YAML — imagens, portas, volumes, variáveis e dependências — e controla o conjunto com comandos simples como docker compose up e docker compose down.
Se você ainda não conhece imagens, containers e volumes, comece pelo tutorial Docker para iniciantes. Este guia parte desses conceitos.
Nas versões atuais, o Compose vem integrado ao Docker como um plugin e é chamado com docker compose (com espaço). O antigo docker-compose (com hífen) foi descontinuado.
A arquitetura que vamos montar
| Serviço | Imagem | Porta no seu computador | Função |
|---|---|---|---|
app |
Construída a partir de php:8.4-apache |
8080 | Executa o código PHP |
db |
mysql:8.4 |
Não exposta | Banco de dados |
phpmyadmin |
phpmyadmin:5 |
8081 | Interface web para o MySQL |
Os três containers ficam em uma rede interna criada pelo Compose. Dentro dela, cada serviço é encontrado pelo nome: o PHP se conecta ao banco usando o host db, e não localhost. Esse é o detalhe que mais confunde quem está começando, então guarde-o.
Passo 1: estrutura de pastas
Crie a seguinte estrutura:

compose.yamldescreve os serviços.Dockerfilepersonaliza a imagem do PHP..envguarda senhas e nomes do banco.docker/mysql/init.sqlcria a tabela e os dados iniciais.src/contém o código da aplicação.
Passo 2: o arquivo compose.yaml
Este é o coração do tutorial. Crie o compose.yaml na raiz:

Agora, serviço por serviço.
Serviço app (PHP + Apache)
build: .— em vez de usar uma imagem pronta, o Compose constrói uma a partir doDockerfileda pasta atual.ports: "8080:80"— publica a porta 80 do Apache na porta 8080 do seu computador.volumes: ./src:/var/www/html— um bind mount: a pastasrcdo seu computador aparece dentro do container. Você edita o código no seu editor e o resultado aparece ao recarregar o navegador, sem rebuild.environment— as credenciais do banco chegam ao PHP como variáveis de ambiente. Os valores${DB_NAME}e similares vêm do arquivo.env.depends_oncomcondition: service_healthy— o PHP só inicia depois que o MySQL estiver realmente pronto para conexões, e não apenas com o container criado.
Serviço db (MySQL)
image: mysql:8.4— versão LTS do MySQL. Fixar a versão evita surpresas quando a imagemlatestmudar.environment— a imagem oficial do MySQL lê essas variáveis na primeira inicialização para criar a senha do root, o banco e o usuário da aplicação.dados-mysql:/var/lib/mysql— um volume nomeado onde o MySQL grava os dados. Sem ele, tudo seria apagado ao remover o container.init.sqlem/docker-entrypoint-initdb.d/— scripts nessa pasta rodam automaticamente apenas na primeira vez, quando o volume está vazio.healthcheck— a cada 5 segundos, o Compose executamysqladmin ping. Quando responde, o serviço fica healthy e libera oapp.
Repare que o db não publica porta. O PHP e o phpMyAdmin o acessam pela rede interna, e o banco não fica exposto. Se quiser conectar um cliente como DBeaver ou MySQL Workbench, adicione ports: - "3306:3306".
Serviço phpmyadmin
Uma interface web para administrar o banco. O PMA_HOST: db aponta para o serviço do MySQL. Acesse em http://localhost:8081 com o usuário e a senha definidos no .env.
Passo 3: o Dockerfile e o arquivo .env
A imagem oficial php:8.4-apache não vem com a extensão de MySQL habilitada. O Dockerfile resolve isso com o utilitário docker-php-ext-install, que já vem na imagem:

Precisa de outras extensões? Adicione na mesma linha: RUN docker-php-ext-install pdo_mysql mysqli bcmath. Para extensões que dependem de bibliotecas do sistema, como gd e zip, é necessário instalar pacotes com apt-get antes.
Em seguida, crie o .env na raiz (o Compose lê esse arquivo automaticamente):
DB_NAME=loja
DB_USER=app
DB_PASSWORD=senha_app_123
DB_ROOT_PASSWORD=senha_root_456
Adicione o .env ao .gitignore e versione um .env.example sem as senhas reais. O motivo é explicado em Variáveis de ambiente e arquivo .env.
Passo 4: dados iniciais e o código PHP
O script docker/mysql/init.sql cria uma tabela de produtos com três registros:
CREATE TABLE IF NOT EXISTS produtos (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
preco DECIMAL(10,2) NOT NULL
);
INSERT INTO produtos (nome, preco) VALUES
('Teclado mecânico', 349.90),
('Mouse sem fio', 129.00),
('Monitor 27"', 1499.00);
E o src/index.php conecta ao banco com PDO e lista os produtos:

Pontos importantes do código:
- O DSN usa
getenv('DB_HOST'), que valedb— o nome do serviço no Compose. PDO::ERRMODE_EXCEPTIONfaz o PDO lançar exceções em vez de falhar em silêncio.htmlspecialchars()protege a página contra XSS ao exibir dados do banco.charset=utf8mb4garante acentos e emojis corretos.
Passo 5: validando e subindo o ambiente
Antes de subir, valide o arquivo. O docker compose config mostra a configuração final, com as variáveis do .env já substituídas — se houver erro de indentação no YAML, ele aponta aqui:

Agora suba tudo:
docker compose up -d --build
upcria e inicia os containers.-droda em segundo plano.--buildforça a construção da imagem doapp(use sempre que alterar oDockerfile).

Na primeira execução o Docker baixa as imagens, então pode levar alguns minutos. Observe a ordem: o db fica Healthy antes do app iniciar, graças ao depends_on com healthcheck.
Abra http://localhost:8080:

Seu ambiente Docker Compose com PHP e MySQL está funcionando. Edite o index.php, salve e recarregue a página: a mudança aparece na hora, graças ao bind mount.
Passo 6: comandos do dia a dia com Docker Compose
docker compose ps # status dos serviços
docker compose logs -f app # logs do PHP/Apache em tempo real
docker compose exec app bash # terminal dentro do container PHP
docker compose exec db mysql -u app -p loja # cliente MySQL
docker compose restart app # reinicia um serviço
docker compose stop # para tudo (mantém containers)
docker compose down # remove containers e rede (mantém o volume)
docker compose down -v # remove também o volume (APAGA o banco)

Atenção à diferença entre down e down -v: o primeiro preserva os dados do banco no volume; o segundo apaga tudo. Use o -v quando quiser recomeçar do zero — por exemplo, para rodar o init.sql novamente.
Adaptando para Laravel e outros frameworks
O mesmo modelo serve para projetos maiores com poucos ajustes:
- Laravel: aponte o
DocumentRootdo Apache para a pastapublice preencha no.envdo LaravelDB_CONNECTION=mysql,DB_HOST=dbe as credenciais. Rode comandos comdocker compose exec app php artisan migrate. Veja o básico do framework em Laravel da instalação ao Hello World. - Composer dentro do container: adicione ao
Dockerfilea linhaCOPY --from=composer:2 /usr/bin/composer /usr/bin/composer. - Nginx + PHP-FPM: troque a imagem
php:8.4-apacheporphp:8.4-fpme adicione um serviçonginx. É a combinação mais usada em produção. - Redis, filas e e-mail: adicione serviços como
redis:7emailpitao mesmo arquivo.
Erros comuns com Docker Compose, PHP e MySQL
“SQLSTATE[HY000] [2002] Connection refused” — o PHP tentou conectar antes de o MySQL estar pronto, ou você usou localhost como host. Use DB_HOST=db e o depends_on com healthcheck.
“could not find driver” — a extensão pdo_mysql não foi instalada. Confira o Dockerfile e rode docker compose up -d --build.
“Access denied for user” — você alterou as senhas no .env depois que o volume já existia. As variáveis do MySQL só valem na primeira inicialização. Rode docker compose down -v (isso apaga os dados) e suba novamente.
O init.sql não executou — ele só roda com o volume vazio. Mesma solução: down -v e up.
“port is already allocated” — outro serviço usa a porta 8080 ou 8081. Troque o lado esquerdo do mapeamento, por exemplo "8090:80".
Erro de indentação no YAML — o YAML usa espaços (nunca tabs) e a indentação importa. O docker compose config mostra a linha do problema.
Perguntas frequentes
Qual a diferença entre docker-compose.yml e compose.yaml?
Os dois funcionam. compose.yaml é o nome preferido na especificação atual; docker-compose.yml continua aceito por compatibilidade. A chave version: no topo do arquivo também é obsoleta e pode ser removida.
Posso usar Docker Compose em produção?
Pode, especialmente em servidores únicos e projetos pequenos ou médios. Nesse caso, remova os bind mounts de código, use imagens com o código copiado no build, não exponha o banco e guarde senhas com mais cuidado. Para vários servidores e alta disponibilidade, considere orquestradores como Kubernetes.
Os dados do MySQL ficam salvos se eu desligar o computador?
Sim. Os dados ficam no volume dados-mysql, que sobrevive a docker compose stop, down e reinicializações. Só são apagados com docker compose down -v ou docker volume rm.
Como trocar a versão do PHP?
Altere a linha FROM do Dockerfile (por exemplo, php:8.3-apache) e rode docker compose up -d --build. Em segundos você testa o projeto em outra versão, sem instalar nada no computador.
Conclusão
Você montou um ambiente com Docker Compose, PHP e MySQL do zero: entendeu cada linha do compose.yaml, personalizou a imagem do PHP, inicializou o banco com um script SQL, conectou via PDO usando variáveis de ambiente e aprendeu os comandos para operar o ambiente no dia a dia. A partir de agora, qualquer pessoa do seu time sobe o projeto com docker compose up.
Com o ambiente pronto, que tal fazer sua aplicação PHP conversar com o mundo? No tutorial Como enviar SMS com PHP usando a SDK da APIBrasil você adiciona envio de SMS ao projeto em poucas linhas, e a mesma SDK da APIBrasil oferece consultas de CEP, CNPJ, placas e WhatsApp.
![]()









