Come usare Strapi in un'applicazione web con Java Spring Boot usando Docker e Docker Compose

Strapi è un CMS headless che espone contenuti tramite API REST (e, opzionalmente, GraphQL). In un'architettura moderna, Strapi può gestire la parte editoriale e di content management, mentre un'applicazione Java Spring Boot può consumare quelle API per costruire un backend più “business oriented” (validazioni, regole di dominio, integrazioni con altri sistemi), o per fornire un'unica API verso frontend e client.

In questo articolo realizzeremo un ambiente completo con Docker Compose: Strapi + database PostgreSQL + Spring Boot. L'obiettivo è avere un setup riproducibile in sviluppo e facilmente estendibile in produzione.

Architettura di riferimento

  • Strapi: gestisce contenuti e pannello admin, persiste su PostgreSQL.
  • PostgreSQL: database per Strapi (evitiamo SQLite perché poco adatto a scenari Docker e produzione).
  • Spring Boot: espone un'API propria e/o aggrega e normalizza i dati provenienti da Strapi.

Prerequisiti

  • Docker e Docker Compose installati.
  • JDK 17+ (se vuoi eseguire Spring Boot anche fuori da Docker).
  • Node.js 18+ (solo se vuoi creare o modificare Strapi localmente senza container).

Struttura del progetto

Una struttura minima, adatta a un repository unico, può essere:

.
├─ docker-compose.yml
├─ .env
├─ strapi/
│  ├─ Dockerfile
│  ├─ package.json
│  ├─ config/
│  ├─ src/
│  └─ ...
└─ spring-boot/
   ├─ Dockerfile
   ├─ pom.xml
   └─ src/
      └─ ...

Puoi anche tenere Strapi in un repository separato. Qui lo includiamo nella stessa repo per semplificare lo sviluppo coordinato.

1) Creare il progetto Strapi

Se non hai ancora un progetto Strapi, puoi crearlo una tantum (fuori da Docker) e poi contenerizzarlo. Da root del repository:

npx create-strapi-app@latest strapi --quickstart

Il comando --quickstart usa SQLite e avvia subito l'admin. Per usarlo con PostgreSQL in Docker, modificheremo la configurazione e useremo variabili d'ambiente. Se preferisci, durante la creazione puoi selezionare PostgreSQL come DB.

Configurazione database Strapi via variabili d'ambiente

In Strapi v4, la configurazione del DB si trova in config/database.js. Un esempio robusto per PostgreSQL con variabili d'ambiente:

module.exports = ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: {
      host: env('DATABASE_HOST', 'postgres'),
      port: env.int('DATABASE_PORT', 5432),
      database: env('DATABASE_NAME', 'strapi'),
      user: env('DATABASE_USERNAME', 'strapi'),
      password: env('DATABASE_PASSWORD', 'strapi'),
      ssl: env.bool('DATABASE_SSL', false),
    },
    pool: {
      min: 0,
      max: 10,
    },
  },
});

Abilitare CORS per consumare l'API

In sviluppo, spesso Spring Boot gira su un host/porta differente. Configura CORS in Strapi (ad esempio in config/middlewares.js). Esempio permissivo per sviluppo:

module.exports = [
  'strapi::errors',
  'strapi::security',
  {
    name: 'strapi::cors',
    config: {
      enabled: true,
      origin: ['http://localhost:8080', 'http://localhost:3000'],
      headers: '*',
    },
  },
  'strapi::poweredBy',
  'strapi::logger',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
];

In produzione, restringi origin, evita wildcard e verifica anche le impostazioni di strapi::security.

2) Dockerfile per Strapi

In molte situazioni puoi usare direttamente l'immagine ufficiale su Docker Hub. Tuttavia, avere un Dockerfile dedicato è utile per installare dipendenze, buildare e controllare la versione Node. Ecco un Dockerfile semplice (multi-stage) per Strapi:

# strapi/Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build dell'admin (utile in produzione). In sviluppo puoi ometterlo.
RUN npm run build

FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app ./
EXPOSE 1337
CMD ["npm", "run", "start"]

Se vuoi un'esperienza di sviluppo con hot reload, puoi usare npm run develop e montare il volume. Più avanti vedrai come gestirlo in Compose.

3) Creare il progetto Spring Boot

Il progetto Spring Boot consumerà Strapi usando un client HTTP. L'approccio consigliato oggi è Spring WebClient (reactive) anche in applicazioni non reattive, perché è flessibile e moderno.

Dipendenze Maven tipiche:

<!-- spring-boot/pom.xml -->
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
</dependencies>

Configurazione applicativa

Configuriamo URL Strapi e token. In Strapi puoi creare un API Token dal pannello Admin: Settings → API Tokens. Evita di usare credenziali admin per chiamare le API da backend.

# spring-boot/src/main/resources/application.yml
server:
  port: 8080

strapi:
  base-url: ${STRAPI_BASE_URL:http://strapi:1337}
  api-token: ${STRAPI_API_TOKEN:}

WebClient configurato con token

package com.example.demo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class StrapiClientConfig {

  @Bean
  public WebClient strapiWebClient(
      @Value("${strapi.base-url}") String baseUrl,
      @Value("${strapi.api-token}") String apiToken
  ) {
    var builder = WebClient.builder()
        .baseUrl(baseUrl)
        .exchangeStrategies(ExchangeStrategies.builder()
            .codecs(cfg -> cfg.defaultCodecs().maxInMemorySize(4 * 1024 * 1024))
            .build()
        );

    if (apiToken != null && !apiToken.isBlank()) {
      builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiToken);
    }
    return builder.build();
  }
}

4) Modellare le risposte di Strapi

Strapi v4 tende a restituire payload annidati (ad esempio data, attributes, e metadati). È utile creare DTO che rappresentino ciò che ti serve, oppure usare mappe generiche e trasformare a livello di servizio.

Supponiamo di avere in Strapi una Collection Type post con campi title, slug, content. Una risposta tipica (REST) assomiglia a:

{
  "data": [
    {
      "id": 1,
      "attributes": {
        "title": "Hello",
        "slug": "hello",
        "content": "Testo...",
        "createdAt": "2026-01-10T12:00:00.000Z",
        "updatedAt": "2026-01-10T12:00:00.000Z",
        "publishedAt": "2026-01-10T12:00:00.000Z"
      }
    }
  ],
  "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 } }
}

DTO minimali:

package com.example.demo.strapi.dto;

import java.util.List;

public record StrapiListResponse(List> data, Object meta) {}

public record StrapiSingleResponse(StrapiData data, Object meta) {}

public record StrapiData(Long id, T attributes) {}
package com.example.demo.strapi.dto;

public record PostAttributes(
    String title,
    String slug,
    String content,
    String createdAt,
    String updatedAt,
    String publishedAt
) {}

In produzione conviene usare tipi data/ora (es. OffsetDateTime) e configurare Jackson di conseguenza. Qui usiamo stringhe per mantenere l'esempio compatto.

5) Servizio Spring Boot che legge i contenuti da Strapi

Esempio: recuperare la lista dei post e un singolo post per slug. Strapi supporta filtri via query string (es. filters[slug][$eq]=hello) e paginazione.

package com.example.demo.strapi;

import com.example.demo.strapi.dto.PostAttributes;
import com.example.demo.strapi.dto.StrapiListResponse;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class PostService {

  private final WebClient strapi;

  private static final ParameterizedTypeReference> POST_LIST =
      new ParameterizedTypeReference<>() {};

  public PostService(WebClient strapiWebClient) {
    this.strapi = strapiWebClient;
  }

  public Mono> listPosts(int page, int pageSize) {
    return strapi.get()
        .uri(uriBuilder -> uriBuilder
            .path("/api/posts")
            .queryParam("pagination[page]", page)
            .queryParam("pagination[pageSize]", pageSize)
            .queryParam("sort", "publishedAt:desc")
            .build()
        )
        .retrieve()
        .bodyToMono(POST_LIST);
  }

  public Mono> findBySlug(String slug) {
    return strapi.get()
        .uri(uriBuilder -> uriBuilder
            .path("/api/posts")
            .queryParam("filters[slug][$eq]", slug)
            .queryParam("pagination[pageSize]", 1)
            .build()
        )
        .retrieve()
        .bodyToMono(POST_LIST);
  }
}

6) Controller Spring Boot come facciata verso Strapi

A volte vuoi che il frontend non chiami Strapi direttamente. Motivi tipici: token server-side, rate limit centralizzato, caching, logging, trasformazioni del payload, policy di sicurezza.

package com.example.demo.api;

import com.example.demo.strapi.PostService;
import com.example.demo.strapi.dto.PostAttributes;
import com.example.demo.strapi.dto.StrapiListResponse;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@Validated
@RestController
@RequestMapping("/v1/posts")
public class PostController {

  private final PostService service;

  public PostController(PostService service) {
    this.service = service;
  }

  @GetMapping
  public Mono> list(
      @RequestParam(defaultValue = "1") @Min(1) int page,
      @RequestParam(defaultValue = "10") @Min(1) @Max(100) int pageSize
  ) {
    return service.listPosts(page, pageSize);
  }

  @GetMapping("/{slug}")
  public Mono> bySlug(@PathVariable String slug) {
    return service.findBySlug(slug);
  }
}

7) Dockerfile per Spring Boot

Un Dockerfile multi-stage compila il jar e poi lo esegue in una base più leggera. Ecco un esempio con Maven Wrapper:

# spring-boot/Dockerfile
FROM eclipse-temurin:17-jdk AS build
WORKDIR /workspace
COPY . .
RUN ./mvnw -q -DskipTests package

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /workspace/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]

8) Docker Compose: Strapi + PostgreSQL + Spring Boot

Ora componiamo i servizi. Usiamo volumi per persistenza del DB e, opzionalmente, per persistenza di upload Strapi. Inseriamo anche un healthcheck per PostgreSQL e dipendenze tra servizi.

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${DATABASE_NAME:-strapi}
      POSTGRES_USER: ${DATABASE_USERNAME:-strapi}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-strapi}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 5s
      timeout: 5s
      retries: 20
    ports:
      - "5432:5432"

  strapi:
    build:
      context: ./strapi
      dockerfile: Dockerfile
    environment:
      NODE_ENV: ${STRAPI_NODE_ENV:-development}
      HOST: 0.0.0.0
      PORT: 1337

      DATABASE_HOST: postgres
      DATABASE_PORT: 5432
      DATABASE_NAME: ${DATABASE_NAME:-strapi}
      DATABASE_USERNAME: ${DATABASE_USERNAME:-strapi}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD:-strapi}
      DATABASE_SSL: "false"

      APP_KEYS: ${STRAPI_APP_KEYS}
      API_TOKEN_SALT: ${STRAPI_API_TOKEN_SALT}
      ADMIN_JWT_SECRET: ${STRAPI_ADMIN_JWT_SECRET}
      JWT_SECRET: ${STRAPI_JWT_SECRET}
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "1337:1337"
    volumes:
      - ./strapi:/app
      - /app/node_modules
      - strapiuploads:/app/public/uploads
    command: ["npm", "run", "develop"]

  spring:
    build:
      context: ./spring-boot
      dockerfile: Dockerfile
    environment:
      STRAPI_BASE_URL: http://strapi:1337
      STRAPI_API_TOKEN: ${STRAPI_API_TOKEN}
    depends_on:
      - strapi
    ports:
      - "8080:8080"

volumes:
  pgdata:
  strapiuploads:

File .env

Le chiavi e i segreti di Strapi non dovrebbero essere hardcoded. Docker Compose carica automaticamente .env. Esempio:

# .env
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=strapi

# Genera valori random robusti (almeno 32+ caratteri), qui placeholder:
STRAPI_APP_KEYS=key1,key2,key3,key4
STRAPI_API_TOKEN_SALT=changeme_api_token_salt
STRAPI_ADMIN_JWT_SECRET=changeme_admin_jwt_secret
STRAPI_JWT_SECRET=changeme_jwt_secret

# Token creato dal pannello admin di Strapi (Settings -> API Tokens)
STRAPI_API_TOKEN=changeme_strapi_api_token

STRAPI_NODE_ENV=development

In sviluppo puoi rigenerare facilmente segreti e token. In produzione usa un secret manager o variabili d'ambiente sicure, e non committare mai .env con valori reali.

9) Avvio e verifica

  1. Avvia tutto: docker compose up --build
  2. Apri Strapi admin: http://localhost:1337/admin e completa la creazione del primo utente admin.
  3. Crea un Collection Type (es. post) e aggiungi qualche contenuto.
  4. Imposta permessi per le API pubbliche se necessario: Settings → Roles → Public e abilita le azioni find/findOne.
  5. Verifica l'endpoint Spring Boot: http://localhost:8080/v1/posts

10) Strapi: permessi, token e sicurezza

Tre pattern comuni:

  • API pubblica (Public role): il frontend chiama Strapi direttamente senza token. Più semplice, ma richiede attenzione a CORS e rate limiting.
  • Backend proxy (Spring Boot): il frontend chiama Spring; Spring chiama Strapi con API token. Centralizza la sicurezza e riduce la superficie esposta.
  • Utenti autenticati Strapi: usi l'autenticazione di Strapi (Users & Permissions) e token JWT degli utenti, eventualmente integrandola con sistemi esterni.

Se Spring Boot è il tuo backend principale, spesso ha senso che Strapi sia raggiungibile solo dalla rete interna (non esposto su Internet), e che sia Spring Boot a esporre l'API verso il mondo.

11) Popolare relazioni e media

Strapi supporta il parametro populate per includere relazioni e media. Esempio: includere tutte le relazioni:

curl "http://localhost:1337/api/posts?populate=*"

Se hai relazioni complesse, preferisci una popolazione esplicita per evitare payload enormi. Esempio (popola solo cover e author):

curl "http://localhost:1337/api/posts?populate[cover]=*&populate[author]=*"

In Spring Boot, i DTO diventano più articolati; valuta se mappare in oggetti di dominio più semplici (es. PostView) da restituire al frontend.

12) Caching e resilienza lato Spring Boot

Se l'app chiama Strapi frequentemente, aggiungere caching o resilienza può migliorare performance e stabilità:

  • Caching in memoria o Redis per endpoint lettura (contenuti spesso non cambiano al secondo).
  • Timeout e retry controllati sui client HTTP verso Strapi.
  • Fallback (ad esempio restituire contenuti cache) se Strapi non è temporaneamente disponibile.

Esempio di timeout con WebClient:

import java.time.Duration;
import reactor.util.retry.Retry;

public Mono callWithPolicy(Mono mono) {
  return mono
      .timeout(Duration.ofSeconds(3))
      .retryWhen(Retry.backoff(2, Duration.ofMillis(200)));
}

13) Considerazioni per la produzione

  • NODE_ENV=production per Strapi e build dell'admin (se non usi un reverse proxy che serve asset statici separati).
  • Reverse proxy (Nginx/Traefik) per TLS, compressione, rate limit, routing su host diversi.
  • Persistenza per database e upload (volumi o storage esterno tipo S3).
  • Backup regolari del DB e degli upload.
  • Log e osservabilità: centralizza log e metriche (Actuator, log driver, APM).
  • Hardening: CORS restrittivo, segreti protetti, minimi privilegi.

14) Problemi comuni e soluzioni

Strapi non parte e segnala errori sul DB

  • Verifica che postgres sia healthy (healthcheck ok).
  • Controlla DATABASE_HOST (in Compose deve essere il nome del servizio: postgres).
  • Assicurati che username/password coincidano tra postgres e Strapi.

Spring Boot riceve 403 o 401 da Strapi

  • Se usi permessi Public, abilita le azioni in Roles.
  • Se usi API Token, assicurati che sia presente e valido e che il tipo di token abbia i permessi corretti.

CORS blocca le chiamate dal browser

  • Configura CORS su Strapi (o proxy in Spring Boot) con origini esplicite.
  • Se passi da Spring Boot, il browser chiama solo Spring Boot: spesso è la soluzione più pulita.

Conclusione

Con Docker Compose puoi mettere in piedi rapidamente un ambiente Strapi + PostgreSQL + Spring Boot ripetibile e portabile. Strapi eccelle nella gestione contenuti e nella velocità di prototipazione; Spring Boot è ideale per logiche di business, orchestrazione e integrazione. La combinazione ti permette di separare chiaramente content management e dominio applicativo, mantenendo un flusso di sviluppo semplice e scalabile.

Torna su