Linux Development

Servidor de deploy (Laravel)

ver o script

Provisiona o servidor, cria a estrutura do projeto e gera o script de deploy zero-downtime.

bash <(curl -s https://raw.githubusercontent.com/edsuuu/Linux-Devlopment/main/scripts/deploy/setup.sh)

Ao iniciar, o script pergunta o modo — navegue com as setas ↑ ↓ e confirme com Enter:

1) Instalação completa

Instala o servidor (tabela abaixo) e em seguida cria o projeto. Use num servidor novo.

2) Apenas o projeto

Pula a instalação e só cria a estrutura e o script de deploy. Use quando o servidor já está pronto.

O que a instalação completa instala

ItemDetalhes
PHPVersão disponível no apt do sistema, com as extensões cli fpm mbstring xml curl mysql sqlite3 zip gd bcmath intl
ComposerInstalador oficial, em /usr/local/bin/composer
Node.jsVia nodesource — menu com 26, 24 (padrão), 22 e 20
nginxInstalado, habilitado no boot e configurado pro projeto se você informar o domínio
pnpmHabilitado via corepack (vem com o Node)
apache2Desativado e removido se aparecer como dependência do PHP — aqui o site é servido por nginx

Idempotente: o que já existe é pulado, então rodar de novo não quebra nada.

Estrutura de pastas no servidor

Como o projeto fica em /var/www/projects depois do setup e de alguns deploys:

meuprojeto
├── current -> releases/2026-08-04-130511
├── releases
│   ├── 2026-08-04-101503
│   ├── 2026-08-04-121022
│   └── 2026-08-04-130511
│       ├── .env
│       ├── storage -> /var/www/projects/meuprojeto/shared/storage
│       └── public
│           └── storage -> /var/www/projects/meuprojeto/shared/storage/app/public
└── shared
    ├── .env
    └── storage
        ├── app
        │   └── public
        ├── framework
        │   ├── cache
        │   ├── sessions
        │   ├── testing
        │   └── views
        └── logs
  • current — symlink pra release ativa. Aponte o root do nginx pra current/public.
  • releases — uma pasta por deploy, nomeada pela data. Só as 3 mais novas ficam.
  • shared — o que sobrevive entre releases: o .env e o storage.

O .env é copiado, não é symlink

Repare na árvore: o storage tem seta (é symlink pro shared/), o .env não — cada release recebe uma cópia própria do shared/.env, feita no momento do deploy.

Na prática: editar o shared/.env não muda a release que está no ar — o valor novo só entra no próximo deploy. Pra valer agora, edite também o current/.env e rode artisan optimize.

nginx do projeto

O setup pergunta o domínio: informando um, ele grava o arquivo abaixo em /etc/nginx/sites-available/<projeto>, habilita o site, testa com nginx -t e recarrega. Deixando vazio, use este modelo à mão:

server {
    listen 80;
    listen [::]:80;
    server_name seudominio.com.br;

    root /var/www/projects/meuprojeto/current/public;
    index index.php;
    charset utf-8;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    client_max_body_size 32m;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

O $realpath_root no lugar do $document_root é o que faz o PHP receber o caminho real da release (releases/2026-…) em vez do caminho pelo symlink — sem isso o OPcache mistura releases.

nginx com HTTPS

Emita o certificado primeiro (veja Certificado SSL) — sem os arquivos .pem no lugar, o nginx -t falha. Com o certificado emitido, troque o conteúdo do arquivo do projeto por este: o primeiro server manda todo HTTP daquele domínio pro HTTPS, o segundo serve o site.

server {
    listen 80;
    listen [::]:80;
    server_name seudominio.com.br;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name seudominio.com.br;

    ssl_certificate /etc/letsencrypt/live/seudominio.com.br/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/seudominio.com.br/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/seudominio.com.br/chain.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;

    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    root /var/www/projects/meuprojeto/current/public;
    index index.php;
    charset utf-8;

    client_max_body_size 32m;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Valide e recarregue:

sudo nginx -t
sudo systemctl reload nginx

Confira o socket do fastcgi_pass e o domínio nos três lugares antes de recarregar. Config genérica de HTTPS, fora da estrutura de deploy: Nginx HTTPS.

Se o servidor tiver mais de uma versão de PHP

Versões convivem sem problema — o que quebra é o CLI e o FPM apontarem para versões diferentes. O artisan e o composer rodam pelo PHP da linha de comando; o site roda pelo socket do FPM no fastcgi_pass. Se o CLI for 8.5 e só o FPM 8.3 estiver no ar, apontar pro socket 8.5 dá 502.

O setup checa isso: usa o socket da versão do CLI se ele existir, senão cai no socket do FPM que está rodando e avisa da diferença. O deploy faz o mesmo no reload. Pra conferir à mão: php -v e ls /run/php/.

O setup grava o script do projeto em scripts/, a partir do diretório onde você o rodou. A cada release:

./scripts/deploy-<projeto>.sh

O que o deploy faz

  • 01Clone raso do branch numa release datada em releases/
  • 02No primeiro deploy, cria o shared/.env a partir do .env.example e roda o key:generate
  • 03Copia o shared/.env pra dentro da release — o .env nunca é symlink — e liga o storage por symlink pro shared/
  • 04composer install --no-dev e assets pelo lockfile (pnpm ou npm ci)
  • 05migrate --force + artisan optimize
  • 06Troca atômica do symlink current, mantém as 3 releases mais novas
  • 07Recarrega o php-fpm pra limpar o OPcache e o realpath cache

Por que recarregar o php-fpm

O PHP guarda em cache o bytecode (OPcache) e o caminho real de cada arquivo (realpath cache, 120s por padrão). Como o current é um symlink, sem recarregar o FPM o site continua servindo o código da release anterior por alguns minutos depois do deploy.

O deploy roda systemctl reload php<versão>-fpm logo após trocar o symlink, o que reinicia os workers e zera os dois caches sem derrubar requisição.

pnpm

Fixe "packageManager": "pnpm@10.12.1" no package.json — sem isso o corepack baixa o pnpm mais novo, que pode exigir Node mais novo que o do servidor.