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
- Avvia tutto:
docker compose up --build - Apri Strapi admin:
http://localhost:1337/admine completa la creazione del primo utente admin. - Crea un Collection Type (es.
post) e aggiungi qualche contenuto. - Imposta permessi per le API pubbliche se necessario: Settings → Roles → Public e abilita le azioni
find/findOne. - 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
postgressia healthy (healthcheck ok). - Controlla
DATABASE_HOST(in Compose deve essere il nome del servizio:postgres). - Assicurati che username/password coincidano tra
postgrese 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.