Strapi con Laravel in Docker: guida completa con Docker Compose

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 ci e npm run build
  • avviare Strapi con npm run start (non develop)
  • 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 strapi sul filesystem host e la compatibilità con i volumi.
  • Errore DB connection: assicurati che DATABASE_HOST punti 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 php e abilita APP_DEBUG=true in locale.
  • Nginx 404 su route Laravel: assicurati che try_files punti a /index.php?$query_string e che la root sia public.
  • Timeout chiamate Strapi: aumenta timeout nel client Laravel o ottimizza query Strapi (filtri, paginazione, populate solo dove serve).

18) Checklist finale

  1. Avvia tutto: docker compose up -d --build
  2. Laravel: install, key, migrate
  3. Strapi: apri http://localhost:1337/admin, crea admin, crea content-types e permessi/token
  4. Laravel: imposta STRAPI_URL e STRAPI_TOKEN, implementa client e controller
  5. Verifica: http://localhost:8080 (Laravel) e http://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.

Torna su