Questa guida mostra come integrare Strapi (headless CMS) in un’applicazione web basata su Laravel, orchestrando tutto con Docker e Docker Compose. L’obiettivo è ottenere un ambiente riproducibile in locale e facilmente portabile in staging/produzione, con:
- Laravel (PHP-FPM) + Nginx
- Strapi (Node.js) + database dedicato
- Database per Laravel
- Rete Docker condivisa tra servizi
- Persistenza tramite volumi
Nel seguito useremo PostgreSQL (ottimo per entrambi), ma la struttura è simile con MySQL/MariaDB. I comandi sono pensati per Linux/macOS; su Windows (WSL2) la logica è la stessa.
1) Struttura del progetto
Una struttura consigliata è tenere Laravel e Strapi come due progetti affiancati, dentro un unico repository “monorepo” oppure in cartelle separate. Qui useremo un repository unico con questa struttura:
my-app/
├─ docker/
│ ├─ nginx/
│ │ └─ default.conf
│ └─ php/
│ ├─ Dockerfile
│ └─ php.ini
├─ laravel/ # progetto Laravel
├─ strapi/ # progetto Strapi
├─ docker-compose.yml
└─ .env.example
Questo approccio semplifica la rete interna e l’accesso ai servizi: Laravel potrà chiamare Strapi tramite il nome del servizio Docker (es. http://strapi:1337).
2) Creare Laravel e Strapi (se non esistono già)
Laravel
Se devi inizializzare Laravel:
mkdir -p my-app && cd my-app
composer create-project laravel/laravel laravel
Strapi
Strapi può essere inizializzato via CLI. Se vuoi farlo “fuori” da Docker una sola volta (poi lo userai nel container), puoi eseguire:
npx create-strapi-app@latest strapi --quickstart
Oppure puoi creare la cartella e demandare l’installazione al container (approccio più “container-first”), come vedremo più avanti.
3) Docker Compose: servizi e rete
Creiamo docker-compose.yml alla radice. Questo file definisce:
- nginx: reverse proxy per Laravel
- php: PHP-FPM con estensioni per Laravel
- laravel_db: database per Laravel
- strapi: Node.js con Strapi
- strapi_db: database per Strapi
services:
nginx:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./laravel:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
networks:
- appnet
php:
build:
context: ./docker/php
volumes:
- ./laravel:/var/www/html
environment:
# utile per debugging/config, ma la config vera sta in laravel/.env
PHP_MEMORY_LIMIT: 512M
depends_on:
- laravel_db
networks:
- appnet
laravel_db:
image: postgres:16-alpine
environment:
POSTGRES_DB: laravel
POSTGRES_USER: laravel
POSTGRES_PASSWORD: laravel
volumes:
- laravel_db_data:/var/lib/postgresql/data
ports:
- "54321:5432" # opzionale: accesso dal host
networks:
- appnet
strapi:
image: node:20-alpine
working_dir: /srv/app
command: sh -c "if [ ! -f package.json ]; then npx create-strapi-app@latest . --no-run; fi && npm run develop"
environment:
NODE_ENV: development
# Database Strapi
DATABASE_CLIENT: postgres
DATABASE_HOST: strapi_db
DATABASE_PORT: 5432
DATABASE_NAME: strapi
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: strapi
# Strapi
HOST: 0.0.0.0
PORT: 1337
# Chiavi: in produzione usane di robuste e non versionate
APP_KEYS: "replace-me-1,replace-me-2"
API_TOKEN_SALT: "replace-me"
ADMIN_JWT_SECRET: "replace-me"
JWT_SECRET: "replace-me"
volumes:
- ./strapi:/srv/app
- strapi_node_modules:/srv/app/node_modules
ports:
- "1337:1337"
depends_on:
- strapi_db
networks:
- appnet
strapi_db:
image: postgres:16-alpine
environment:
POSTGRES_DB: strapi
POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi
volumes:
- strapi_db_data:/var/lib/postgresql/data
ports:
- "54322:5432" # opzionale: accesso dal host
networks:
- appnet
volumes:
laravel_db_data:
strapi_db_data:
strapi_node_modules:
networks:
appnet:
Nota importante su Strapi: la riga di command inizializza il progetto solo se non trova package.json, poi avvia npm run develop. È comodo in locale; in produzione conviene buildare un’immagine dedicata e avviare in modalità start con build precompilata.
4) Nginx per Laravel
Crea docker/nginx/default.conf per servire Laravel tramite PHP-FPM:
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass php:9000;
}
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$ {
expires 7d;
access_log off;
}
}
Così l’app sarà disponibile su http://localhost:8080.
5) Dockerfile PHP per Laravel
Crea docker/php/Dockerfile con estensioni tipiche per Laravel e PostgreSQL:
FROM php:8.3-fpm-alpine
# Dipendenze di sistema
RUN apk add --no-cache \
bash \
git \
unzip \
icu-dev \
oniguruma-dev \
libzip-dev \
postgresql-dev
# Estensioni PHP
RUN docker-php-ext-install \
pdo \
pdo_pgsql \
intl \
mbstring \
zip
# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# php.ini custom (opzionale)
COPY php.ini /usr/local/etc/php/conf.d/99-custom.ini
E un docker/php/php.ini minimo (opzionale):
memory_limit=512M
upload_max_filesize=32M
post_max_size=32M
max_execution_time=120
6) Configurare Laravel per il DB in Docker
Nel file laravel/.env imposta la connessione al database usando il nome del servizio Docker laravel_db:
APP_NAME="My App"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8080
DB_CONNECTION=pgsql
DB_HOST=laravel_db
DB_PORT=5432
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=laravel
Poi avvia i container e inizializza Laravel:
docker compose up -d --build
# Genera key e migrazioni
docker compose exec php composer install
docker compose exec php php artisan key:generate
docker compose exec php php artisan migrate
7) Configurare Strapi per PostgreSQL
Nel nostro Compose abbiamo passato a Strapi le variabili d’ambiente per il database. Strapi, alla prima installazione, genererà i file necessari e userà queste variabili.
Apri Strapi in locale su http://localhost:1337/admin e crea l’utente admin. Se è il primo avvio, potrebbe metterci un po’ a installare dipendenze e fare bootstrap.
8) Modellare contenuti in Strapi
Esempio: crea una collection type Post con campi:
title(Text)slug(UID, basato su title)content(Rich text o Long text)publishedAt(DateTime, opzionale)
In Settings → Roles → Public, abilita i permessi di lettura se vuoi endpoint pubblici (ad esempio find e findOne per posts). In alternativa, tieni tutto protetto e usa API token lato server (consigliato quando Laravel fa da “backend” pubblico).
9) Autenticazione: API token e sicurezza
Per consumare Strapi da Laravel in modo sicuro, è comune usare un API Token di Strapi (Settings → API Tokens). Questo token va tenuto solo lato server (mai nel frontend).
In Strapi, crea un token con permessi adeguati (Read-only per contenuti pubblici, oppure Full access per integrazioni interne). Poi, in Laravel, salvalo in .env:
STRAPI_URL=http://strapi:1337
STRAPI_TOKEN=YOUR_STRAPI_API_TOKEN
Nota sullo URL: dentro Docker, Laravel parla con Strapi via rete interna usando http://strapi:1337. Se invece stai facendo chiamate dall’host (es. Postman sul tuo PC), userai http://localhost:1337.
10) Chiamare Strapi da Laravel (HTTP Client)
Laravel include un ottimo HTTP client basato su Guzzle (Http::). Crea un semplice service per incapsulare le chiamate a Strapi.
Esempio: laravel/app/Services/StrapiClient.php
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class StrapiClient
{
public function __construct(
private readonly string $baseUrl,
private readonly ?string $token,
) {}
public static function make(): self
{
return new self(
baseUrl: rtrim(config('services.strapi.url'), '/'),
token: config('services.strapi.token'),
);
}
private function http()
{
$req = Http::baseUrl($this->baseUrl)
->acceptJson()
->timeout(10);
if ($this->token) {
$req = $req->withToken($this->token);
}
return $req;
}
public function listPosts(int $page = 1, int $pageSize = 10): array
{
$response = $this->http()->get('/api/posts', [
'pagination[page]' => $page,
'pagination[pageSize]' => $pageSize,
'sort' => 'publishedAt:desc',
]);
$response->throw();
return $response->json();
}
public function getPostBySlug(string $slug): array
{
$response = $this->http()->get('/api/posts', [
'filters[slug][$eq]' => $slug,
'pagination[pageSize]' => 1,
]);
$response->throw();
return $response->json();
}
}
Configura laravel/config/services.php aggiungendo Strapi:
<?php
return [
// ...
'strapi' => [
'url' => env('STRAPI_URL', 'http://strapi:1337'),
'token' => env('STRAPI_TOKEN'),
],
];
Ora crea un controller che espone i contenuti nel tuo sito Laravel:
<?php
namespace App\Http\Controllers;
use App\Services\StrapiClient;
use Illuminate\Http\Request;
class BlogController extends Controller
{
public function index(Request $request)
{
$page = (int) $request->query('page', 1);
$data = StrapiClient::make()->listPosts(page: $page, pageSize: 10);
return view('blog.index', [
'posts' => $data['data'] ?? [],
'meta' => $data['meta'] ?? [],
]);
}
public function show(string $slug)
{
$data = StrapiClient::make()->getPostBySlug($slug);
$post = $data['data'][0] ?? null;
abort_if(!$post, 404);
return view('blog.show', [
'post' => $post,
]);
}
}
E le rotte in laravel/routes/web.php:
use App\Http\Controllers\BlogController;
Route::get('/blog', [BlogController::class, 'index']);
Route::get('/blog/{slug}', [BlogController::class, 'show']);
11) Rendering in Blade e gestione dei dati Strapi
Strapi restituisce tipicamente una struttura con data, attributes, ecc. Un esempio minimo in Blade:
<!-- laravel/resources/views/blog/index.blade.php -->
<h1>Blog</h1>
<ul>
@foreach ($posts as $post)
@php($attr = $post['attributes'] ?? [])
<li>
<a href="{{ url('/blog/' . ($attr['slug'] ?? '')) }}">
{{ $attr['title'] ?? 'Senza titolo' }}
</a>
</li>
@endforeach
</ul>
Per contenuti rich text, Strapi può restituire HTML o strutture rich text a seconda del tipo di campo/plugin. Se ricevi HTML, assicurati di sanificarlo o di fidarti della sorgente (tipicamente sì, se editor interno). In Blade:
<!-- laravel/resources/views/blog/show.blade.php -->
@php($attr = $post['attributes'] ?? [])
<h1>{{ $attr['title'] ?? '' }}</h1>
{!! $attr['content'] ?? '' !!}
Se non vuoi stampare HTML “raw”, valuta una pipeline di sanitizzazione (es. HTMLPurifier) e/o l’uso di un formato come Markdown.
12) CORS e chiamate dal browser
Due scenari tipici:
- Laravel chiama Strapi lato server: nessun problema CORS, perché il browser parla solo con Laravel.
- Frontend (browser) chiama Strapi direttamente: serve configurare CORS in Strapi.
Se vuoi chiamare Strapi dal browser, in Strapi abilita/limita CORS (in base alla versione e al progetto, la configurazione può stare in config/middlewares.js o simili). L’idea è autorizzare solo i domini necessari (es. http://localhost:8080 in locale) e i metodi richiesti.
In generale, in architetture Laravel+Strapi è più sicuro far passare tutto da Laravel (che applica auth, rate limiting, caching) e usare Strapi come CMS privato.
13) Cache e performance
Per evitare di colpire Strapi ad ogni richiesta, aggiungi cache lato Laravel. Un esempio semplice:
use Illuminate\Support\Facades\Cache;
use App\Services\StrapiClient;
$data = Cache::remember('strapi.posts.page.1', now()->addMinutes(5), function () {
return StrapiClient::make()->listPosts(page: 1, pageSize: 10);
});
Per invalidare la cache automaticamente quando cambiano i contenuti, puoi:
- usare webhooks di Strapi verso un endpoint Laravel che fa purge della cache
- usare cache breve (TTL) se ti basta aggiornamento entro pochi minuti
- implementare un sistema di “tag” e versionamento cache
14) Webhook da Strapi a Laravel (invalidazione cache)
Strapi supporta webhooks per eventi (create/update/delete/publish). Crea un endpoint Laravel dedicato, protetto da un segreto condiviso.
In Laravel:
Route::post('/webhooks/strapi', function (\Illuminate\Http\Request $request) {
abort_unless(
hash_equals(config('services.strapi.webhook_secret'), $request->header('X-Webhook-Secret', '')),
403
);
// esempio: pulizia cache mirata
\Illuminate\Support\Facades\Cache::flush();
return response()->json(['ok' => true]);
});
Nel tuo .env:
STRAPI_WEBHOOK_SECRET=super-secret
In Strapi, configura il webhook verso http://nginx/webhooks/strapi (se la chiamata avviene dentro la rete Docker) oppure http://host.docker.internal:8080/webhooks/strapi (a seconda di OS e networking). In molti casi, usare il nome del servizio nginx o php dentro la rete Docker è la scelta più semplice.
15) Gestione file e media
Strapi gestisce upload e media library. In locale puoi usare lo storage su filesystem nel container, ma in produzione conviene usare un provider esterno (S3 compatibile, Cloudinary, ecc.) per avere scalabilità e persistenza affidabile.
Se resti su filesystem, assicurati di:
- montare un volume persistente per la cartella degli upload di Strapi
- gestire backup
- servire i file in modo efficiente (reverse proxy o CDN)
Esempio di volume per uploads (varia in base alla versione/struttura Strapi):
strapi:
volumes:
- ./strapi:/srv/app
- strapi_uploads:/srv/app/public/uploads
volumes:
strapi_uploads:
16) Build e avvio in produzione
In produzione è preferibile:
- buildare un’immagine PHP con dipendenze già installate (composer install in build)
- buildare un’immagine Strapi con
npm cienpm run build - avviare Strapi con
npm run start(nondevelop) - usare variabili e segreti gestiti dal runtime (Docker secrets, vault, ecc.)
- mettere Nginx o un reverse proxy davanti (TLS, compression, security headers)
Esempio (concettuale) di Dockerfile per Strapi in produzione:
FROM node:20-alpine
WORKDIR /srv/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
ENV NODE_ENV=production
EXPOSE 1337
CMD ["npm", "run", "start"]
Per Laravel, puoi usare un’immagine PHP che includa dipendenze, opcache e config ottimizzata, e gestire la cache config/routes in fase di deploy:
php artisan config:cache
php artisan route:cache
php artisan view:cache
17) Troubleshooting frequente
- Strapi non parte per permessi o file lock: verifica permessi della cartella
strapisul filesystem host e la compatibilità con i volumi. - Errore DB connection: assicurati che
DATABASE_HOSTpunti al nome del servizio (es.strapi_db) e che la porta sia corretta (5432 nel network interno). - Laravel 500 o pagina bianca: controlla log con
docker compose logs -f phpe abilitaAPP_DEBUG=truein locale. - Nginx 404 su route Laravel: assicurati che
try_filespunti a/index.php?$query_stringe che la root siapublic. - Timeout chiamate Strapi: aumenta timeout nel client Laravel o ottimizza query Strapi (filtri, paginazione, populate solo dove serve).
18) Checklist finale
- Avvia tutto:
docker compose up -d --build - Laravel: install, key, migrate
- Strapi: apri
http://localhost:1337/admin, crea admin, crea content-types e permessi/token - Laravel: imposta
STRAPI_URLeSTRAPI_TOKEN, implementa client e controller - Verifica:
http://localhost:8080(Laravel) ehttp://localhost:1337(Strapi)
Con questa base hai un’architettura pulita: Strapi gestisce contenuti e backoffice, Laravel governa l’esperienza web, la sicurezza e le integrazioni. Da qui puoi estendere con autenticazione utenti (Laravel Sanctum/Passport), preview dei contenuti, webhooks avanzati, e pipeline CI/CD per build e deploy delle immagini.