Eu tava mexendo num projeto de um cliente e encontrei alguns pontos de atenção no deploy. A aplicação Laravel inteira estava dentro da public_html. Funcionava, só que também deixava a pasta completa do projeto dentro da raiz pública do domínio.
E aí dá um B.O. grande de segurança.
Nesse caso, foi possível baixar pela internet tanto o database/database.sqlite quanto o storage/logs/laravel.log. Além disso, arquivos como .env.example, artisan, composer.json e composer.lock estavam acessíveis, e um info.php exibia informações do servidor.
A estrutura era mais ou menos essa:
public_html/ ← raiz pública do domínio
├── app/
├── bootstrap/
├── config/
├── database/
│ └── database.sqlite ← dava pra baixar
├── resources/
├── routes/
├── storage/
│ └── logs/laravel.log ← dava pra baixar também
├── vendor/
├── wp/ ← instalação WordPress
├── sideproject1/ ← outra aplicação Laravel inteira
├── public/ ← esta deveria ser a raiz pública do Laravel
├── .env ← dependia de bloqueio no .htaccess
├── .env.example
├── artisan
├── composer.json
├── composer.lock
└── info.php
Dá pra sair bloqueando arquivo por arquivo no .htaccess, mas a correção principal é tirar esses arquivos da área pública. O Laravel já tem uma pasta pra receber as requisições do navegador: public/.
Preparando a aplicação
O processo ainda é meio arcaico, principalmente nessas hospedagens compartilhadas. O ideal mesmo seria essa parada toda estar automatizada por pipeline, mas enquanto isso não existe dá pra organizar o deploy manual.
Primeiro, gere os assets localmente ou na CI. Nesse projeto, o comando era:
npm ci
npm run production
Essa parte varia conforme o projeto e a ferramenta de build. Use o script que existe no seu package.json.
Também precisamos de uma vendor limpa, só com as dependências de produção:
composer install --no-dev --optimize-autoloader
Dá pra manter uma cópia local do projeto só pra preparar essa vendor antes de enviar os arquivos. O ambiente usado pra gerar as dependências precisa ser compatível com o PHP e as extensões da hospedagem.
No .env de produção, confira:
APP_ENV=production
APP_DEBUG=false
Separando a aplicação da parte pública
A ideia é deixar a aplicação em uma pasta fora da public_html:
/home/usuario/
├── projeto/
│ ├── app/
│ ├── bootstrap/
│ ├── config/
│ ├── database/
│ ├── resources/
│ ├── routes/
│ ├── storage/
│ ├── vendor/
│ ├── .env
│ ├── artisan
│ ├── composer.json
│ └── public/
└── public_html -> /home/usuario/projeto/public
Se a hospedagem deixa configurar a raiz pública do domínio, ela pode apontar diretamente para /home/usuario/projeto/public. Aqui, a solução foi fazer a public_html apontar pra essa pasta com um link simbólico. Isso depende de a hospedagem permitir o link e de o servidor web conseguir segui-lo.
Antes de trocar essa estrutura, vale olhar o que ficou largado na raiz do projeto. Nesse projeto tinha bastante coisa que precisava continuar acessível pelo navegador:
robots.txt
sitemap.xml
politica-sobre-cookies.pdf
favicon.ico
css/
js/
images/
Esses arquivos vão pra public/. Já .env, app/, config/, database/, storage/, vendor/ e os outros arquivos internos continuam fora dela.
Essa separação é a parte mais importante da mudança. Se algum upload precisa ser público, exponha só a pasta destinada a esses arquivos, não a storage/ inteira com logs e outros dados internos.
Enviando os arquivos e trocando a entrada do site
Nesse tipo de hospedagem, o deploy acaba sendo uma mistura de FTP/SFTP com SSH. No meu caso, o acesso SSH da Locaweb precisava ser habilitado pelo painel antes de conectar:
ssh usuario@dominio.do.ftp
Os arquivos da aplicação passam a ser enviados para /home/usuario/projeto/.
Pra um projeto novo, com a aplicação no lugar e sem uma public_html existente, o link fica assim:
cd /home/usuario
ln -s /home/usuario/projeto/public public_html
A sintaxe é ln -s ALVO NOME_DO_LINK. Ou seja, public_html é o nome do link, e a pasta public/ do projeto é o destino.
Nesse projeto tinha uma diferença: a aplicação antiga já estava inteira dentro da public_html. Então a própria pasta existente virou a pasta do projeto:
cd /home/usuario
mv public_html projeto
ln -s /home/usuario/projeto/public public_html
Esse mv não é uma etapa obrigatória de todo deploy Laravel. Ele fez sentido nessa migração, com projeto ainda inexistente. Antes de fazer uma troca dessas em produção, tenha backup e considere a interrupção entre mover a pasta e criar o link.
No final, o caminho ficou:
public_html → projeto/public
O WordPress e o sideproject1 no meio disso tudo
O projeto também tinha uma instalação WordPress em /wp e outra aplicação Laravel em /sideproject1. Essas URLs precisavam continuar funcionando.
Foram criados dois links dentro da public/:
cd /home/usuario/projeto
ln -s ../wp public/wp
ln -s ../sideproject1/public public/sideproject1
No sideproject1, a aplicação inteira continua em sideproject1/, mas o domínio só enxerga sideproject1/public/. A URL permanece /sideproject1, sem expor junto app/, config/, .env, vendor/ e companhia.
O WordPress é diferente: o link de public/wp expõe a instalação em wp/. Ele não cria a mesma separação que a pasta public/ do Laravel oferece.
Nesse caso, o wp-config.php também foi movido para um nível acima da instalação. Eu não sabia dessa possibilidade até mexer nisso. Ainda assim, o WordPress continua precisando dos próprios cuidados com configuração, permissões e arquivos acessíveis.
A estrutura final ficou assim:
/home/usuario/
├── projeto/
│ ├── app/
│ ├── config/
│ ├── database/
│ ├── storage/
│ ├── vendor/
│ ├── wp/
│ ├── wp-config.php
│ ├── sideproject1/
│ │ ├── app/
│ │ ├── .env
│ │ └── public/
│ ├── .env
│ ├── artisan
│ └── public/
│ ├── css/
│ ├── images/
│ ├── js/
│ ├── wp -> ../wp
│ ├── sideproject1 -> ../sideproject1/public
│ ├── robots.txt
│ ├── sitemap.xml
│ ├── politica-sobre-cookies.pdf
│ ├── index.php
│ └── .htaccess
└── public_html -> /home/usuario/projeto/public
E o .htaccess?
Com a mudança, o .htaccess da entrada do Laravel passa a ser o que está em projeto/public/.htaccess.
Antes, a raiz do projeto era a própria public_html. Agora o Apache recebe as requisições pela pasta public/, então é ali que as regras de entrada do Laravel precisam estar.
Vale revisar as regras antigas durante a troca, principalmente se elas assumiam que o site era servido pela raiz do projeto.
Testando o que abre e o que parou de abrir
Depois da mudança, comecei pelas URLs que precisavam continuar funcionando:
/wp/
/sideproject1/
/admin/
/cliente/login
/anunciante/login
/robots.txt
/sitemap.xml
/politica-sobre-cookies.pdf
Também entram nessa conferência CSS, JavaScript, imagens, uploads, pagamentos e callbacks externos. A página inicial abrir não significa que o deploy inteiro deu certo.
E tem a outra metade do teste: tentar acessar o que não deveria estar disponível.
/.env
/.env.example
/artisan
/composer.json
/composer.lock
/database/database.sqlite
/storage/logs/laravel.log
/phpunit.xml
/info.php
O esperado é não conseguir mais baixar essa penca de arquivo nem abrir a página de diagnóstico. Confira o conteúdo da resposta também: só olhar o status HTTP não basta pra saber o que o servidor entregou.
Tirar os arquivos do ar resolve a exposição dali pra frente. Não desfaz os downloads que podem ter acontecido antes, então banco e logs expostos também precisam ser tratados como um incidente, com revisão dos dados e de possíveis credenciais comprometidas.
O deploy continua manual, mas agora a estrutura acompanha o que o Laravel espera: o navegador chega na public/, e os arquivos internos ficam fora da raiz pública. Era essa a mudança que faltava aqui.