Preparació per a producció

Optimitzacions i passos necessaris per desplegar Laravel a producció: cache, assets, permisos, variables d'entorn i checklist completa.

Preparar una aplicació Laravel per a producció és molt més que canviar APP_ENV a production i pujar el codi. És un procés sistemàtic que cobreix configuració, optimització, seguretat i infraestructura. Cada pas que t'estalvies durant la preparació és un problema potencial que apareixerà en el pitjor moment: quan l'aplicació ja està servint usuaris reals. Un error de configuració pot exposar dades sensibles, una falta d'optimització pot fer que l'aplicació sigui lenta sota càrrega, i uns permisos incorrectes poden obrir forats de seguretat.

Aquesta guia recorre tots els passos necessaris, des de la configuració de les variables d'entorn fins a la verificació final del desplegament. Segueix-la com un checklist i adapta-la a les necessitats específiques del teu projecte.

Configuració de l'entorn#

El fitxer .env és el cor de la configuració de Laravel. En producció, cada variable ha d'estar configurada amb cura perquè un valor incorrecte pot tenir conseqüències greus. El fitxer .env mai s'ha de cometre al repositori Git (ja està al .gitignore per defecte), i cada entorn (local, staging, producció) ha de tenir el seu propi fitxer amb els valors apropiats.

Variables essencials#

Les variables més crítiques per a producció són les que controlen el comportament general de l'aplicació:

APP_NAME="La Meva Aplicació"
APP_ENV=production
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_DEBUG=false
APP_URL=https://lamevaaplicacio.com

APP_DEBUG=false és absolutament crític en producció. Amb APP_DEBUG=true, Laravel mostra traces d'error completes que inclouen variables d'entorn, credencials de base de dades, claus d'API i tot el contingut del fitxer .env. Un atacant que provoqui un error (per exemple, accedint a una URL inexistent amb un paràmetre malformat) obtindria accés complet a totes les credencials de l'aplicació. Mai, sota cap circumstància, activis el mode debug en producció.

La variable APP_KEY és la clau de xifrat que Laravel utilitza per encriptar cookies, sessions, tokens CSRF i qualsevol dada que passi per la funció encrypt(). Ha de ser una cadena de 32 bytes codificada en base64, generada amb php artisan key:generate. Si canvies aquesta clau en una aplicació en funcionament, totes les sessions actives s'invalidaran i les dades encriptades anteriorment seran il·legibles. Genera-la un cop i no la canviïs mai a menys que hi hagi una raó de seguretat per fer-ho.

APP_URL ha d'incloure el protocol (https://) i el domini exacte sense barra final. Laravel utilitza aquesta variable per generar URLs absolutes amb url() i route(), per als enllaços dels correus electrònics i per a la generació del sitemap. Un valor incorrecte pot provocar enllaços trencats a tota l'aplicació.

Base de dades, cache, sessions i cues#

En producció, els drivers de cache, sessions i cues han de ser serveis dedicats, no els drivers de fitxer que venen per defecte. Redis és la opció recomanada per a tots tres perquè és extremadament ràpid, suporta operacions atòmiques i pot gestionar milers de connexions simultànies:

# Base de dades
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=la_meva_app
DB_USERNAME=app_user
DB_PASSWORD=contrasenya-segura-i-llarga
 
# Cache, sessions i cues amb Redis
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
 
# Connexió Redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Si no pots utilitzar Redis, el driver database és una alternativa acceptable per a sessions i cues, però el rendiment serà inferior. El driver file per a la cache és acceptable per a aplicacions petites amb poc tràfic, però sota càrrega, les operacions de fitxer es converteixen en un coll d'ampolla.

Per a la base de dades, utilitza sempre un usuari específic per a l'aplicació amb els permisos mínims necessaris. No utilitzis mai l'usuari root en producció. Crea un usuari que tingui permisos de lectura, escriptura i modificació d'estructura (per a les migracions) només sobre la base de dades de l'aplicació.

Configuració de logs#

En producció, no vols que els logs s'acumulin en un sol fitxer gegant que acabi ocupant tot l'espai del disc. El driver daily rota els fitxers automàticament creant un fitxer per dia i eliminant els antics. El nivell warning filtra els missatges menys importants (debug, info) perquè en producció no necessites registrar cada consulta SQL o cada petició HTTP:

LOG_CHANNEL=stack
LOG_STACK=daily,sentry
LOG_LEVEL=warning
LOG_DEPRECATIONS_CHANNEL=null

El canal stack permet combinar múltiples canals de log. En l'exemple, els logs es guarden en fitxers diaris localment i també s'envien a Sentry (o un servei similar) per a monitorització centralitzada. Sentry agrupa els errors per tipus, mostra la freqüència i envia alertes quan apareixen errors nous, cosa que el fa indispensable per a aplicacions en producció.

// config/logging.php
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'sentry'],
        'ignore_exceptions' => false,
    ],
 
    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => env('LOG_LEVEL', 'warning'),
        'days' => 14, // Mantenir logs dels últims 14 dies
        'replace_placeholders' => true,
    ],
 
    'sentry' => [
        'driver' => 'sentry',
        'level' => env('LOG_LEVEL', 'warning'),
        'bubble' => true,
    ],
],

Comandes d'optimització#

Laravel ofereix un conjunt de comandes que preparen l'aplicació per funcionar de la manera més eficient possible. Cadascuna d'elles pre-processa un aspecte diferent de l'aplicació perquè en producció no hagi de fer-ho en cada petició.

Cache de configuració#

La comanda config:cache llegeix tots els fitxers de configuració del directori config/, els fusiona en un sol fitxer PHP i el guarda a bootstrap/cache/config.php. En lloc de llegir i parsejar desenes de fitxers de configuració en cada petició, Laravel carrega un sol fitxer. L'impacte en el rendiment és significatiu, especialment en aplicacions amb molts paquets que afegeixen els seus propis fitxers de configuració:

php artisan config:cache

Quan la configuració està cacheada, la funció env() només funciona dins dels fitxers de configuració (config/*.php). Si crides env() directament des d'un controlador, middleware, model o qualsevol altre lloc, retornarà null. Això és perquè un cop la configuració està cacheada, Laravel no carrega el fitxer .env en absolut. Assegura't que totes les crides a env() estan dins dels fitxers de configuració i que la resta del codi utilitza config() per accedir als valors.

Per exemple, en lloc de fer env('STRIPE_KEY') dins d'un servei, defineix el valor al fitxer de configuració:

// config/services.php
'stripe' => [
    'key' => env('STRIPE_KEY'),       // env() aquí SÍ funciona
    'secret' => env('STRIPE_SECRET'), // perquè és un fitxer config/
],
// app/Services/PaymentService.php
class PaymentService
{
    public function __construct()
    {
        // Correcte: utilitza config()
        $key = config('services.stripe.key');
 
        // INCORRECTE: retornarà null amb config:cache
        // $key = env('STRIPE_KEY');
    }
}

Cache de rutes#

La comanda route:cache serialitza totes les definicions de rutes en un format compilat que Laravel pot carregar molt més ràpidament que parsejar els fitxers de rutes. Això és especialment notable en aplicacions grans amb centenars de rutes, on la millora de rendiment pot ser de diversos mil·lisegons per petició:

php artisan route:cache

La cache de rutes funciona amb totes les rutes basades en controladors. Si tens rutes que utilitzen closures directament (funcions anònimes), la cache de rutes fallarà perquè PHP no pot serialitzar closures. En producció, totes les rutes han d'apuntar a mètodes de controladors:

// Funciona amb route:cache
Route::get('/usuaris', [UserController::class, 'index']);
 
// NO funciona amb route:cache (closure)
Route::get('/usuaris', function () {
    return User::all();
});

Cache de vistes#

La comanda view:cache pre-compila totes les plantilles Blade a PHP. Normalment, Laravel compila cada plantilla Blade la primera vegada que es renderitza i la guarda en cache. Amb view:cache, aquesta compilació es fa per endavant per a totes les vistes, eliminant el petit retard de la primera renderització:

php artisan view:cache

Cache d'events#

La comanda event:cache cacheja la descoberta automàtica d'events i listeners. Si l'aplicació utilitza la descoberta automàtica d'events (que és el comportament per defecte a Laravel), el framework escaneja els directoris de listeners en cada petició per trobar les associacions entre events i listeners. La cache elimina aquest escaneig:

php artisan event:cache

La comanda optimize#

La comanda optimize executa totes les caches anteriors d'un sol cop. És la manera més pràctica d'optimitzar l'aplicació durant el desplegament:

# Equival a executar config:cache, route:cache, view:cache i event:cache
php artisan optimize

Per netejar totes les caches (útil durant el desenvolupament o quan necessites forçar una recàrrega completa):

# Neteja totes les caches d'optimització
php artisan optimize:clear

Optimització de Composer#

Composer té les seves pròpies optimitzacions que són importants en producció. L'opció --no-dev elimina totes les dependències de desenvolupament (PHPUnit, Faker, Telescope, etc.), reduint l'espai en disc i la superfície d'atac. L'opció --optimize-autoloader genera un classmap optimitzat que permet a PHP trobar les classes directament en lloc de buscar-les per directoris:

composer install --no-dev --optimize-autoloader

Per a una optimització encara més agressiva, pots utilitzar el classmap autoritari, que indica a Composer que NOMÉS busqui classes al classmap generat, sense fer fallback a la càrrega per directoris. Això és lleugerament més ràpid, però requereix que totes les classes estiguin correctament registrades:

composer install --no-dev --optimize-autoloader --classmap-authoritative

No executis composer install amb --no-dev al teu entorn local. Eliminaria les dependències de desenvolupament com PHPUnit, Laravel Dusk o Telescope. L'opció --no-dev és exclusivament per a entorns de producció i staging.

Compilació d'assets#

Si l'aplicació utilitza Vite (el bundler d'assets per defecte de Laravel), necessites compilar els assets CSS i JavaScript per a producció. La compilació de producció minifica el codi, elimina el codi mort (tree-shaking), optimitza les imatges i genera noms de fitxer amb hash per al cache busting:

npm run build

El cache busting és el mecanisme que garanteix que els navegadors carreguin la versió correcta dels assets. Vite genera noms de fitxer amb un hash únic basat en el contingut (per exemple, app-Bf3a2c1e.js). Quan el contingut canvia, el hash canvia i el navegador descarrega la nova versió en lloc d'utilitzar la versió cacheada. Laravel gestiona això automàticament amb la directiva @vite:

{{-- Al teu layout Blade --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
 
{{-- En producció, genera algo com: --}}
<link rel="stylesheet" href="/build/assets/app-Bf3a2c1e.css">
<script type="module" src="/build/assets/app-Da4b5f2e.js"></script>

El directori public/build que genera Vite sí que s'ha de cometre al repositori o generar-se durant el desplegament. La majoria d'equips opten per generar-lo durant el desplegament per mantenir el repositori net.

Permisos de fitxers#

Laravel necessita permís d'escriptura en dos directoris: storage/ (per a logs, cache de fitxers, sessions de fitxer i fitxers pujats) i bootstrap/cache/ (per a les caches d'optimització). La resta de l'aplicació hauria de ser de només lectura per al servidor web.

# Donar permisos d'escriptura al servidor web
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache

El permís 775 significa que el propietari i el grup poden llegir, escriure i executar, mentre que la resta d'usuaris només poden llegir i executar. Això és adequat quan l'usuari del servidor web (normalment www-data a Ubuntu/Debian) és el propietari o pertany al grup dels directoris.

No utilitzis mai 777 (lectura, escriptura i execució per a tothom) en producció. El permís 777 significa que qualsevol usuari del sistema pot modificar i executar fitxers en aquests directoris, cosa que és un risc de seguretat greu. Si necessites depurar problemes de permisos, comprova quin usuari executa el procés PHP i assegura't que pertany al grup correcte:

# Comprovar quin usuari executa PHP-FPM
ps aux | grep php-fpm
 
# Comprovar els permisos actuals
ls -la storage/
ls -la bootstrap/cache/
 
# Si el desplegament es fa amb un usuari diferent del servidor web,
# afegeix l'usuari de desplegament al grup www-data
sudo usermod -aG www-data deploy-user

Una alternativa més elegant és utilitzar ACLs (Access Control Lists), que permeten definir permisos per a múltiples usuaris sense canviar el propietari ni el grup:

# Donar permisos a www-data i a l'usuari de desplegament
sudo setfacl -Rm u:www-data:rwX,u:deploy:rwX storage bootstrap/cache
sudo setfacl -dRm u:www-data:rwX,u:deploy:rwX storage bootstrap/cache

Configuració de PHP per a producció#

La configuració de PHP (php.ini) té un impacte directe en el rendiment i la seguretat de l'aplicació. A continuació, les configuracions més importants per a producció:

OPcache#

OPcache és una extensió de PHP que emmagatzema en memòria el bytecode compilat dels fitxers PHP. Sense OPcache, PHP ha de llegir, parsejar i compilar cada fitxer PHP en cada petició. Amb OPcache, el bytecode compilat es reutilitza directament des de la memòria, cosa que redueix dràsticament el temps d'execució:

; php.ini - Configuració OPcache per a producció
 
; Activar OPcache
opcache.enable=1
 
; Memòria assignada per al cache (en MB)
; 256 MB és adequat per a la majoria d'aplicacions Laravel
opcache.memory_consumption=256
 
; Nombre màxim de fitxers a cachear
; Una aplicació Laravel amb dependències pot tenir 10.000+ fitxers
opcache.max_accelerated_files=20000
 
; No comprovar si els fitxers han canviat (en producció no canvien entre desplegaments)
; Això elimina la crida stat() per cada fitxer en cada petició
opcache.validate_timestamps=0
 
; Guardar els comentaris (necessari per a anotacions i reflexió)
opcache.save_comments=1
 
; Prefetch del framework al iniciar
opcache.preload=/var/www/app/vendor/laravel/framework/src/preload.php
opcache.preload_user=www-data
 
; Percentatge de memòria malgastada abans de reiniciar (per fragmentació)
opcache.max_wasted_percentage=10

Amb opcache.validate_timestamps=0, OPcache no detectarà canvis als fitxers PHP. Després de cada desplegament, has de reiniciar PHP-FPM per forçar OPcache a recarregar els fitxers: sudo systemctl reload php8.3-fpm. Si no ho fas, l'aplicació seguirà executant el codi antic.

Altres configuracions de PHP#

; Memòria màxima per procés (128-256 MB és adequat per a la majoria d'aplicacions)
memory_limit=256M
 
; Mida màxima de pujada de fitxers
upload_max_filesize=64M
post_max_size=64M
 
; Temps màxim d'execució d'un script (en segons)
; 30 segons és suficient per a peticions HTTP normals
max_execution_time=30
 
; Temps màxim d'entrada (parsing de dades POST)
max_input_time=60
 
; Desactivar funcions perilloses
disable_functions=exec,passthru,shell_exec,system,proc_open,popen
 
; No mostrar la versió de PHP a les capçaleres HTTP
expose_php=Off
 
; Zona horària
date.timezone=Europe/Andorra
 
; Codificació per defecte
default_charset=UTF-8
 
; Mida del buffer de realpath (millora el rendiment d'include/require)
realpath_cache_size=4096K
realpath_cache_ttl=600

Desactivar funcions com exec i shell_exec amb disable_functions pot causar problemes si l'aplicació o algun paquet les necessita. Per exemple, alguns paquets de generació de PDF (com Browsershot) o de processament d'imatges (com Spatie Media Library amb conversions) necessiten proc_open. Comprova les dependències de l'aplicació abans de desactivar funcions.

Configuració de Nginx#

Nginx és el servidor web recomanat per a aplicacions Laravel en producció. La configuració següent inclou les optimitzacions essencials: compressió gzip, cache d'assets estàtics, capçaleres de seguretat i la redirecció correcta per al router de Laravel:

server {
    listen 80;
    server_name lamevaaplicacio.com www.lamevaaplicacio.com;
 
    # Redirigir tot el tràfic HTTP a HTTPS
    return 301 https://lamevaaplicacio.com$request_uri;
}
 
server {
    listen 443 ssl http2;
    server_name lamevaaplicacio.com;
 
    # Redirigir www a no-www
    if ($host = www.lamevaaplicacio.com) {
        return 301 https://lamevaaplicacio.com$request_uri;
    }
 
    # Directori arrel
    root /var/www/app/public;
    index index.php;
 
    # SSL (configurat per Certbot o Forge)
    ssl_certificate /etc/letsencrypt/live/lamevaaplicacio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/lamevaaplicacio.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
 
    # Capçaleres de seguretat
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 
    # Mida màxima de pujada (ha de coincidir amb upload_max_filesize de PHP)
    client_max_body_size 64M;
 
    # Compressió gzip
    gzip on;
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_proxied any;
    gzip_vary on;
    gzip_types
        application/javascript
        application/json
        application/xml
        application/rss+xml
        text/css
        text/javascript
        text/plain
        text/xml
        image/svg+xml;
 
    # Cache d'assets estàtics
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
 
    # Laravel: provar fitxer estàtic primer, després enviar a index.php
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
 
        # Timeouts
        fastcgi_connect_timeout 60;
        fastcgi_send_timeout 60;
        fastcgi_read_timeout 60;
    }
 
    # Bloquejar accés a fitxers sensibles
    location ~ /\.(?!well-known) {
        deny all;
    }
 
    location ~ /\.env {
        deny all;
    }
 
    # Desactivar logs per a favicon i robots
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
 
    # Pàgina d'error personalitzada
    error_page 404 /index.php;
}

Els punts clau d'aquesta configuració son:

  • Redirecció HTTP a HTTPS: Tot el tràfic no xifrat es redirigeix a HTTPS amb un codi 301 (redirecció permanent).
  • Compressió gzip: Redueix significativament la mida de les respostes HTML, CSS, JavaScript i JSON. Un nivell de compressió de 5 ofereix un bon equilibri entre compressió i ús de CPU.
  • Cache d'assets estàtics: Els fitxers CSS, JS, imatges i fonts es cacheen durant 1 any al navegador. La capçalera immutable indica que el contingut no canviarà mai per a la mateixa URL (gràcies al cache busting de Vite).
  • Capçaleres de seguretat: X-Frame-Options evita que la pàgina s'incrusti en un iframe (protecció contra clickjacking), X-Content-Type-Options evita que el navegador interpreti fitxers amb un MIME type diferent del declarat, i HSTS força l'ús d'HTTPS durant un any.
  • Bloqueig de fitxers sensibles: Les ubicacions que comencen amb . (com .env, .git) es bloquegen excepte .well-known (necessari per a Let's Encrypt).

L'script de desplegament#

Un desplegament típic segueix una seqüència d'operacions ben definida. Cada pas té un propòsit específic i l'ordre importa. A continuació, un script de desplegament complet amb explicacions:

#!/bin/bash
set -e  # Aturar l'execució si qualsevol comanda falla
 
# Directori de l'aplicació
APP_DIR="/var/www/app"
cd $APP_DIR
 
# 1. Activar el mode de manteniment
# Mostra una pàgina de manteniment als usuaris mentre es desplega
php artisan down --retry=60 --refresh=15
 
# 2. Obtenir el codi més recent
git pull origin main
 
# 3. Instal·lar dependències PHP (sense dependències de desenvolupament)
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
 
# 4. Instal·lar dependències JavaScript i compilar assets
npm ci
npm run build
 
# 5. Executar les migracions de base de dades
# --force és necessari en producció (Laravel demana confirmació per defecte)
php artisan migrate --force
 
# 6. Netejar i reconstruir totes les caches d'optimització
php artisan optimize:clear
php artisan optimize
 
# 7. Reiniciar els workers de cues perquè carreguin el codi nou
php artisan queue:restart
 
# 8. Recarregar PHP-FPM per netejar OPcache
sudo systemctl reload php8.3-fpm
 
# 9. Desactivar el mode de manteniment
php artisan up

Alguns detalls importants sobre aquest script:

  • set -e fa que l'script s'aturi si qualsevol comanda falla. Sense això, una migració fallida no impediria que php artisan up desactivés el mode de manteniment, deixant l'aplicació en un estat inconsistent.
  • npm ci en lloc de npm install: ci instal·la exactament les versions del package-lock.json sense modificar-lo, cosa que garanteix reproducibilitat.
  • --force a migrate: en producció, Laravel demana confirmació interactiva per defecte per evitar migracions accidentals. El flag --force evita aquesta confirmació.
  • queue:restart envia un senyal als workers perquè acabin el job actual i es reiniciïn. No és instantani: cada worker es reinicia quan acaba el job que estigui processant.
  • La recàrrega de PHP-FPM (reload, no restart) és una operació elegant que no interromp les peticions en curs. Les connexions existents acaben amb el procés antic i les noves connexions utilitzen el procés nou amb OPcache actualitzat.

Desplegament amb zero downtime#

L'script anterior té un problema: mentre s'executa, l'aplicació està en mode de manteniment. Si el desplegament triga 30 segons (instal·lar dependències, compilar assets, executar migracions), els usuaris veuran una pàgina de manteniment durant 30 segons. Per a aplicacions amb molt tràfic, això és inacceptable.

El desplegament amb zero downtime resol aquest problema amb una estructura de directoris basada en symlinks. La idea és senzilla: en lloc de modificar el codi en directe, es prepara una nova versió en un directori separat i, un cop tot està llest, s'intercanvia un symlink atòmicament. L'operació de canvi de symlink és instantània (microsegons), cosa que elimina completament el temps d'inactivitat.

L'estructura de directoris és la següent:

/var/www/app/
├── current -> releases/20250115120000  # Symlink a la versió activa
├── releases/
│   ├── 20250114100000/                 # Versió anterior
│   ├── 20250115120000/                 # Versió actual
│   └── 20250116090000/                 # Nova versió (en preparació)
├── shared/
│   ├── .env                            # Configuració compartida
│   └── storage/                        # Storage compartit entre versions

El directori current és un symlink que apunta a la versió activa. Cada desplegament crea un directori nou dins de releases/, hi prepara tot el codi, i finalment canvia el symlink current per apuntar a la nova versió. El directori shared/ conté els fitxers que han de persistir entre desplegaments (l'arxiu .env, el directori storage/ amb logs, fitxers pujats, etc.).

#!/bin/bash
set -e
 
RELEASE_DIR="/var/www/app/releases/$(date +%Y%m%d%H%M%S)"
SHARED_DIR="/var/www/app/shared"
CURRENT_LINK="/var/www/app/current"
 
# 1. Crear directori per a la nova versió
mkdir -p $RELEASE_DIR
 
# 2. Clonar el codi
git clone --depth 1 --branch main git@github.com:usuari/app.git $RELEASE_DIR
 
# 3. Enllaçar fitxers compartits
ln -sf $SHARED_DIR/.env $RELEASE_DIR/.env
rm -rf $RELEASE_DIR/storage
ln -sf $SHARED_DIR/storage $RELEASE_DIR/storage
 
# 4. Instal·lar dependències
cd $RELEASE_DIR
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
npm ci && npm run build
 
# 5. Migracions i optimització
php artisan migrate --force
php artisan optimize
 
# 6. Canviar el symlink (operació atòmica)
ln -sfn $RELEASE_DIR $CURRENT_LINK
 
# 7. Reiniciar serveis
php artisan queue:restart
sudo systemctl reload php8.3-fpm
 
# 8. Netejar versions antigues (mantenir les últimes 5)
cd /var/www/app/releases
ls -dt */ | tail -n +6 | xargs rm -rf

Eines com Laravel Envoyer (servei oficial de Laravel per a desplegaments amb zero downtime) i Deployer (eina open-source) implementen aquesta estratègia automàticament amb funcionalitats addicionals com rollbacks, notificacions i health checks.

Health checks#

Després de cada desplegament, és important verificar que l'aplicació funciona correctament. Un health check bàsic comprova que l'aplicació respon, que la base de dades és accessible i que els serveis externs estan connectats:

// routes/web.php
Route::get('/health', function () {
    $checks = [];
 
    // Comprovar la base de dades
    try {
        DB::connection()->getPdo();
        $checks['database'] = 'ok';
    } catch (\Exception $e) {
        $checks['database'] = 'error: ' . $e->getMessage();
    }
 
    // Comprovar Redis
    try {
        Cache::store('redis')->put('health-check', true, 10);
        $checks['redis'] = 'ok';
    } catch (\Exception $e) {
        $checks['redis'] = 'error: ' . $e->getMessage();
    }
 
    // Comprovar l'storage
    try {
        Storage::put('health-check.txt', 'ok');
        Storage::delete('health-check.txt');
        $checks['storage'] = 'ok';
    } catch (\Exception $e) {
        $checks['storage'] = 'error: ' . $e->getMessage();
    }
 
    $allOk = collect($checks)->every(fn ($status) => $status === 'ok');
 
    return response()->json([
        'status' => $allOk ? 'healthy' : 'unhealthy',
        'checks' => $checks,
        'timestamp' => now()->toIso8601String(),
    ], $allOk ? 200 : 503);
});

Després del desplegament, pots verificar l'estat amb una crida HTTP:

# Al final de l'script de desplegament
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://lamevaaplicacio.com/health)
 
if [ "$RESPONSE" != "200" ]; then
    echo "ALERTA: Health check ha fallat amb codi $RESPONSE"
    # Aquí podries fer rollback automàticament
    exit 1
fi
 
echo "Desplegament completat correctament"

Checklist completa de producció#

Abans de cada desplegament, repassa aquesta checklist per assegurar-te que no oblides cap pas crític:

  1. APP_ENV=production al fitxer .env
  2. APP_DEBUG=false per no exposar informació sensible
  3. APP_URL configurat amb el domini correcte i HTTPS
  4. APP_KEY generat i configurat correctament
  5. Base de dades configurada amb un usuari específic (no root)
  6. Cache, sessions i cues configurades amb Redis (o un driver adequat per a producció)
  7. Logs configurats amb el driver daily i nivell warning
  8. composer install --no-dev --optimize-autoloader per eliminar dependències de desenvolupament
  9. npm run build per compilar i minificar els assets
  10. php artisan optimize per cachear configuració, rutes, vistes i events
  11. Permisos de storage/ i bootstrap/cache/ correctes (775, propietat de www-data)
  12. OPcache activat i configurat
  13. Nginx configurat amb HTTPS, gzip, cache d'estàtics i capçaleres de seguretat
  14. Certificat SSL instal·lat i renovat automàticament
  15. Cron configurat per al scheduler (* * * * * php artisan schedule:run)
  16. Workers de cues gestionats per Supervisor amb reinici automàtic
  17. Backups de base de dades configurats i verificats
  18. Monitorització activa (Sentry, Pulse o similar)
  19. Health check accessible i verificat
  20. Firewall configurat per bloquejar ports innecessaris