Um Ambiente de Desenvolvimento PHP com Docker
O Problema
Há um tempo atrás, eu estava programando uma aplicação com PHP, que na época, estava chegando à sua versão 8.1. No entanto, eu prosseguia me limitando a usar a versão 7.4, pois era a que estava disponível nos repositórios da minha Distro Linux - o Void Linux - que não chega a ser popular como variantes do Ubuntu, Debian, ou Fedora.
Cansado desse problema, pensei em formas de resolvê-lo: compilar do zero seria gambiarrento, uma vez que eu também queria manter a versão nova em paralelo à presente, de forma isolada; e foi pensando em isolamento que cheguei ao Docker. Neste artigo vou descrever como configurei um ambiente para o desenvolvimento de uma aplicação minha.
Docker
Uma explicação detalhada do Docker está fora do escopo deste artigo, mas resumidamente, ele implementa uma forma de virtualização em que criam-se ambientes isolados - os contêineres, com sua própia estrutura de diretórios e conjuntos de libraries e binários, utilizando o kernel do sistema nativo. Isso quer dizer que o Docker não tenta simular um sistema por completo, mas apenas uma parte dele necessária para a criação do contâiner. Um mesmo contâiner pode ser utilizado por diferentes distros linux, por exemplo, mas não funcionaria da mesma maneira no Windows, devido à diferença de kernel.
Apesar desse detalhe, ganhamos em performance, pois contêineres são muito mais leves e rápidos que máquinas virtuais, como eram usadas no Vagrant, justamente por dispensarem emulações que na maioria dos casos são desnecessárias.
Iniciando
Para começar, vamos precisar de um servidor web e uma maneira de rodar PHP junto a ele. Poderiamos utilizar
o Apache com seu módulo de PHP, porém, resolvi optar pelo Nginx, passando a execução de código via FastCGI
para o PHP-FPM, pois trata-se de uma solução mais leve e rápida. São dois serviços separados, e usaremos um
contêiner para cada, pois é no geral uma boa prática no Docker. Para facilitar o gerenciamento de múltiplos
contêineres, existe a ferramenta Docker Compose, que automatiza boa parte dos comandos ao utilizar um arquivo
de configuração em yaml, titulada docker-compose.yml
.
version: '3'
services:
web:
image: nginx:alpine
ports: 80:80
Começamos ditando a versão do Docker Compose e configurando o contêiner responsável pelo nosso servidor, o Nginx.
Afirmamos que iremos utilizar a imagem - basicamente uma camada de libraries e programas - baseada no Alpine Linux,
e vamos expor o port 80 do contêiner para o port 80 do nosso localhost. Ao rodarmos o comando docker-compose up
no
diretório onde deixamos a configuração, e acessarmos http://localhost:80
no navegador de escolha, já nos deparamos com a
página index do Nginx:
Porém, queremos usar nosso própio index, e ainda, já que estou usando o padrao MVC, configurar o index para ser o ponto único de entrada da nossa aplicação. Como proceder?
Configurando o Nginx
O Nginx pode ser configurado por um ou mais arquivos compostos por um conjuntos de diretrizes e blocos. Blocos contém
diretrizes ou outros blocos, e alguns deles são o events
e o http
. Como o Nginx permite que múltiplos domínios sejam servidos (chamados de Virtual Hosts), e para
cada domínio precisamos configurar seu respectivo bloco server
, que é declarado de forma separada do arquivo principal, o /etc/nginx/nginx.conf
,
onde configuramos diretrizes e blocos universais, que se aplicam a todos os domínios servidos. Os arquivos referentes a cada
domínio ficam, ou no diretorio /etc/nginx/conf.d/
, e seguindo o formato <nome-do-domínio>.conf
, ou no /etc/nginx/sites-enabled/
e seguindo <nome-do-domínio>
. Aqui vamos optar pela primeira alternativa, e criar um arquivo chamado localhost.conf
. Note
que não precisamos criar o nginx.conf
, a menos que precisemos de configurações muito específicas. A seguir, vamos inserir no arquivo:
# Isto é um comentario
server {
listen 80 default_server; # Use o Port 80, e torne este o bloco server padrão
root /app/public; # A root do servidor será o diretório public da nossa aplicação
server_name localhost; # Associamos o bloco ao nome localhost
index index.php; # Configure o arquivo index
location / {
# Se o uri não levar a um arquivo ou diretório, sirva o index.php com os parâmetros GET
try_files $uri $uri/ /index.php$is_args$query_string
}
}
Note que, como nosso servidor só terá um único virtual host, tornamos esta a configuração padrão.
Para levarmos esse arquivo de configuração ao nosso contêiner, vamos usar um volume, de modo a compartilhar o arquivo entre o nosso sistema e o contêiner. No docker-compose.yml, acrescentamos:
...
volumes:
- ./docker/nginx:/etc/nginx/conf.d
No caso, guardei o arquivo dentro da pasta nginx
, que por sua vez fica numa pasta chamada docker. Os arquivos dentro dessa pasta
serão compartilhados com o diretório conf.d
do contêiner. Iniciando o contêiner com o docker-compose e acessando localhost/index.php
pelo navegador não vemos nada além de uma tela em branco. Por que será?
Executando PHP
Esquecemos que o Nginx por si só não é capaz de executar código; precisamos de um serviço separado para isso, utilizando uma implementação
do FastCGI para a respectiva linguagem. No caso do PHP, usamos o PHP-FPM. Poderiamos subi-lo no mesmo contêiner em que executamos o Nginx,
usando o supervisord, mas prezando pela simplicidade, vamos executa-lo em um contêiner separado, o que é, no geral, uma boa prática no Docker.
No fim do docker-compose.yml
, acrescentamos:
...
php: # Nome do Serviço
image: php:8.0-fpm-alpine
volumes:
- ./app:/app
Na seção volumes
, compartilhamos o conteúdo do diretório root de nossa aplicação com uma um diretório app
dentro do contêiner, afim
de que o PHP-FPM tenha os arquivos a serem executados. Também será necessário fazer configurações adicionais no Nginx para pedir o PHP-FPM
para executar o código solicitado.
server {
listen 80 default_server; # Use o Port 80, e torne este o bloco server padrão
root /app/public; # A root do servidor será o diretório public da nossa aplicação
server_name localhost; # Associamos o bloco ao nome localhost
index index.php; # Configure o arquivo index
location / {
# Se o uri não levar a um arquivo ou diretório, sirva o index.php com os parâmetros GET
try_files $uri $uri/ /index.php$is_args$query_string
}
# Se o URI terminar com .php, o arquivo referido será passado para o FastCGI
location ~ \.php$ {
# O Request é passado para o serviço do PHP-FPM, que estará escutando no Port 9000
fastcgi_pass php:9000;
# Indicamos o nome do arquivo do script
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
# Passe os parâmetros indicados para o FastCGI
include fastcgi_params;
}
}
E está feito. Subindo os contêiners com o docker-compose, temos nossa aplicação funcionante - só que não.
Bancos de Dados
Se sua aplicação depende do uso de banco de dados - como é na maior parte dos casos - então ela claramente apresentará comportamento defeituoso, uma vez que não fizemos nenhuma configuração relacionado ao banco e a persistência de seus dados. Aqui usarei SQLite, pois julgo o suficiente pra minha aplicação, mas em alguma atualização deste artigo talvez eu acrescente detalhes sobre o setup com PostgreSQL.
Lidando com o SQLite
Primeiramente, precisamos de uma forma de persistir o arquivo do banco sqlite. Faremos isso por meio de um volume gerenciado pelo própio docker,
que chamaremos de database
. Anexamos então, ao docker-compose.yml
:
...
volumes:
database: {}
Apenas indicamos que usaremos um volume chamado database
, sem configurações adicionais, que será automaticamnete criado pelo
docker-compose caso não exista. Ainda será necessário fazer uns ajustes na configurações do serviço php
:
php: # Nome do Contêiner
image: php:8.0-fpm-alpine
volumes:
- ./app:/app
- database:/app/var/data
Dessa forma, os arquivos do diretório /app/var/data/
serão compartilhados com o volume, e vice-versa, assim persistindo ao
longo do tempo. Porém ainda falta o mais importante: criar o própio banco de dados:
docker-compose run -u www-data php touch /app/var/data/database.sqlite
Como o usuário www-data, criamos o arquivo do banco sqlite. E, com sorte, tudo deve estar finalmente funcionante.