Strapi è un headless CMS: gestisci contenuti e modelli (content types) tramite un pannello di amministrazione e li consumi via API (REST o GraphQL) dal frontend o dal backend della tua applicazione. In questa guida costruiremo uno stack completo in Docker con:
- Strapi come backend CMS
- PostgreSQL come database per Strapi
- Un servizio Python (FastAPI) che consuma l’API di Strapi e la espone al tuo frontend o ad altri sistemi
- Docker Compose per orchestrare i servizi in locale (e una base solida per staging/produzione)
L’obiettivo è farti vedere un setup realistico: variabili d’ambiente, volumi persistenti, networking tra container, autenticazione, gestione media e un pattern pulito per integrare Strapi con Python.
Architettura e flusso dei dati
Schema logico:
- Strapi legge/scrive su PostgreSQL (contenuti, utenti, ruoli, configurazioni)
- Strapi espone API su
http://strapi:1337(nel network Docker) e suhttp://localhost:1337(sulla tua macchina) - Il servizio Python interroga Strapi via HTTP usando il nome del servizio Docker (
strapi) - Il frontend (non incluso qui) può parlare col Python service oppure direttamente con Strapi, a seconda del caso d’uso
Quando usare Python come “BFF” (Backend For Frontend) invece che chiamare Strapi direttamente dal frontend?
- Vuoi aggregare dati da Strapi e da altre fonti (pagamenti, CRM, microservizi)
- Vuoi applicare business logic, caching, rate limit, controlli aggiuntivi
- Vuoi evitare di esporre token/chiavi al browser
Prerequisiti
- Docker e Docker Compose installati
- Conoscenza base di YAML e dei concetti container/network/volume
- Python 3.11+ consigliato (il container userà Python 3.12)
Struttura del progetto
Una possibile struttura (monorepo) è questa:
.
├── docker-compose.yml
├── .env
├── strapi/
│ ├── Dockerfile
│ └── app/ # verrà popolata da Strapi (volume bind o named volume)
└── python-api/
├── Dockerfile
├── requirements.txt
└── app/
├── main.py
└── settings.py
Nota: Strapi può essere inizializzato al primo avvio e salvato su volume. In alternativa puoi creare l’app Strapi
una volta (es. con npx create-strapi-app) e poi containerizzarla. Qui scegliamo un approccio semplice e “Docker-first”:
Strapi vive nella cartella strapi/app persistita su volume.
Variabili d’ambiente
Crea un file .env nella root del progetto. Serve a centralizzare segreti e configurazioni che Docker Compose inietterà nei container.
Esempio:
# Database (Postgres)
POSTGRES_DB=strapi
POSTGRES_USER=strapi
POSTGRES_PASSWORD=strapi_password
POSTGRES_PORT=5432
# Strapi
STRAPI_HOST=0.0.0.0
STRAPI_PORT=1337
STRAPI_APP_KEYS=key1,key2,key3,key4
STRAPI_API_TOKEN_SALT=some_api_token_salt
STRAPI_ADMIN_JWT_SECRET=some_admin_jwt_secret
STRAPI_JWT_SECRET=some_jwt_secret
# Python service
PYTHON_PORT=8000
# URL Strapi visto dal container Python (hostname = nome servizio Docker)
STRAPI_INTERNAL_URL=http://strapi:1337
In produzione dovresti generare segreti robusti (app keys e secret) e gestirli con un secrets manager.
Per lo sviluppo locale va bene un file .env (da non committare se contiene segreti reali).
Docker Compose: Postgres + Strapi + Python
Crea docker-compose.yml nella root. Questo file definisce i servizi, le porte, le dipendenze e i volumi.
services:
db:
image: postgres:16-alpine
container_name: strapi_db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "${POSTGRES_PORT}:5432"
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 20
strapi:
build:
context: ./strapi
dockerfile: Dockerfile
container_name: strapi
environment:
HOST: ${STRAPI_HOST}
PORT: ${STRAPI_PORT}
DATABASE_CLIENT: postgres
DATABASE_HOST: db
DATABASE_PORT: 5432
DATABASE_NAME: ${POSTGRES_DB}
DATABASE_USERNAME: ${POSTGRES_USER}
DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
APP_KEYS: ${STRAPI_APP_KEYS}
API_TOKEN_SALT: ${STRAPI_API_TOKEN_SALT}
ADMIN_JWT_SECRET: ${STRAPI_ADMIN_JWT_SECRET}
JWT_SECRET: ${STRAPI_JWT_SECRET}
NODE_ENV: development
ports:
- "${STRAPI_PORT}:1337"
volumes:
- strapi_app:/opt/app
depends_on:
db:
condition: service_healthy
python-api:
build:
context: ./python-api
dockerfile: Dockerfile
container_name: python_api
environment:
STRAPI_BASE_URL: ${STRAPI_INTERNAL_URL}
ports:
- "${PYTHON_PORT}:8000"
depends_on:
- strapi
volumes:
db_data:
strapi_app:
Punti chiave:
dbusa un named volumedb_dataper persistere i dati.strapidipende dadbe aspetta l’healthcheck di Postgres.strapipersiste la sua app (config, API, plugin, upload) instrapi_appdentro/opt/app.python-apiparla a Strapi usandohttp://strapi:1337(nome servizio sul network interno).
Dockerfile per Strapi
Crea strapi/Dockerfile. Usiamo Node LTS e installiamo/avviamo Strapi nella directory /opt/app.
Se la directory è vuota al primo avvio, inizializziamo un progetto Strapi; poi avviamo in modalità sviluppo.
FROM node:20-alpine
WORKDIR /opt/app
# Dipendenze utili per build di moduli nativi (se servono)
RUN apk add --no-cache python3 make g++
# Script di bootstrap: crea l'app se non esiste e avvia Strapi
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
EXPOSE 1337
ENTRYPOINT ["docker-entrypoint.sh"]
Ora crea strapi/docker-entrypoint.sh.
Questo script controlla se esiste un progetto Strapi (file package.json).
Se manca, crea l’app e installa il driver Postgres.
#!/bin/sh
set -e
if [ ! -f "/opt/app/package.json" ]; then
echo "Inizializzo un nuovo progetto Strapi..."
# Strapi richiede una cartella vuota. /opt/app è un volume, quindi va bene.
npx --yes create-strapi-app@latest /opt/app --quickstart --no-run
cd /opt/app
echo "Configuro database Postgres..."
npm install pg
# Nota: la configurazione DB verrà letta da env in config/database.js (vedi sezione successiva)
else
cd /opt/app
fi
echo "Installazione dipendenze (se necessarie)..."
npm install
echo "Avvio Strapi..."
npm run develop -- --host 0.0.0.0 --port 1337
Per usare Postgres, Strapi deve avere una configurazione database che legga le variabili d’ambiente.
Creiamo/aggiustiamo /opt/app/config/database.js dopo la creazione dell’app.
Dato che l’app viene generata al runtime, la strada più semplice è montare un override tramite volume bind oppure
creare un piccolo passo che scrive il file se non esiste. Qui mostriamo la versione del file:
'use strict';
module.exports = ({ env }) => ({
connection: {
client: 'postgres',
connection: {
host: env('DATABASE_HOST', 'db'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapi'),
password: env('DATABASE_PASSWORD', 'strapi_password'),
ssl: env.bool('DATABASE_SSL', false) && { rejectUnauthorized: false },
},
pool: { min: 0, max: 10 },
},
});
Come inserirlo nell’app generata? Hai due opzioni pratiche:
-
Bind mount: crea una cartella
strapi/overrides/config/database.jse montala su/opt/app/config/database.jsdal compose. -
Bootstrap script: in
docker-entrypoint.sh, dopo la creazione del progetto, scrivi questo file nella posizione corretta con un heredoc.
Per mantenere l’esempio lineare, aggiungi questa sezione allo script, subito dopo npm install pg:
cat > /opt/app/config/database.js <<'EOF'
'use strict';
module.exports = ({ env }) => ({
connection: {
client: 'postgres',
connection: {
host: env('DATABASE_HOST', 'db'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapi'),
password: env('DATABASE_PASSWORD', 'strapi_password'),
ssl: env.bool('DATABASE_SSL', false) && { rejectUnauthorized: false },
},
pool: { min: 0, max: 10 },
},
});
EOF
Avvio dello stack
Dalla root:
docker compose up --build
Al primo avvio, Strapi richiederà di creare un utente admin tramite la UI.
Apri http://localhost:1337/admin e completa il setup.
Creare un Content Type in Strapi
Esempio: creiamo Post con campi:
title(Text)slug(UID basato su title)content(Rich text)publishedAt(gestito da Strapi se usi Draft & Publish)
Da Strapi Admin:
- Content-type Builder → Create new collection type →
Post - Aggiungi i campi e salva
- Vai su Settings → Roles → Public
- Abilita le permission
findefindOnesuPost(solo per demo; in produzione valuta bene)
A questo punto puoi creare alcuni post in Content Manager.
Consumare l’API REST di Strapi
Strapi espone endpoint REST convenzionali:
GET /api/postslistaGET /api/posts/:iddettaglio- Parametri utili:
filters,sort,pagination,populate
Esempio di query con filtri, ordinamento e paginazione:
curl "http://localhost:1337/api/posts?sort=publishedAt:desc&pagination[page]=1&pagination[pageSize]=10"
Se usi relazioni/media, spesso vuoi populate:
curl "http://localhost:1337/api/posts?populate=*"
Token API e sicurezza
Per chiamate server-to-server è consigliato usare un API token. In Strapi Admin: Settings → API Tokens → Create new token. Scegli “Read-only” se ti basta leggere contenuti.
Poi chiami l’API con header Authorization: Bearer <token>.
Questo è particolarmente utile se vuoi mantenere disabilitate le permission “Public”.
curl -H "Authorization: Bearer YOUR_TOKEN" "http://localhost:1337/api/posts"
Servizio Python (FastAPI) che integra Strapi
Creiamo un microservizio Python che:
- legge la variabile
STRAPI_BASE_URL - chiama Strapi per ottenere i post
- espone endpoint propri, utili al frontend
python-api/requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.30.6
httpx==0.27.2
pydantic-settings==2.5.2
python-api/Dockerfile
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
python-api/app/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=None)
strapi_base_url: str = "http://strapi:1337"
strapi_api_token: str | None = None
settings = Settings()
python-api/app/main.py
from fastapi import FastAPI, HTTPException
import httpx
from .settings import settings
app = FastAPI(title="Python BFF for Strapi")
def _strapi_headers() -> dict[str, str]:
headers: dict[str, str] = {"Accept": "application/json"}
if settings.strapi_api_token:
headers["Authorization"] = f"Bearer {settings.strapi_api_token}"
return headers
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/posts")
async def list_posts(page: int = 1, page_size: int = 10):
url = f"{settings.strapi_base_url}/api/posts"
params = {
"sort": "publishedAt:desc",
"pagination[page]": page,
"pagination[pageSize]": page_size,
}
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(url, params=params, headers=_strapi_headers())
if r.status_code >= 400:
raise HTTPException(status_code=502, detail={"strapi_status": r.status_code, "body": r.text})
data = r.json()
# Qui puoi rimappare/normalizzare l'output per il frontend
return data
@app.get("/posts/{post_id}")
async def get_post(post_id: int):
url = f"{settings.strapi_base_url}/api/posts/{post_id}"
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(url, headers=_strapi_headers())
if r.status_code == 404:
raise HTTPException(status_code=404, detail="Post non trovato")
if r.status_code >= 400:
raise HTTPException(status_code=502, detail={"strapi_status": r.status_code, "body": r.text})
return r.json()
Se vuoi usare un API token anche dal container Python, aggiungi a .env:
STRAPI_API_TOKEN=your_token_here
E nel docker-compose.yml, passa l’env al servizio Python:
python-api:
environment:
STRAPI_BASE_URL: ${STRAPI_INTERNAL_URL}
STRAPI_API_TOKEN: ${STRAPI_API_TOKEN}
Poi aggiorna Settings in modo che legga anche STRAPI_API_TOKEN (già previsto con strapi_api_token).
Test rapido end-to-end
Con lo stack avviato:
- Strapi admin:
http://localhost:1337/admin - API Strapi:
http://localhost:1337/api/posts - Python API:
http://localhost:8000/posts
Prova:
curl "http://localhost:8000/health"
curl "http://localhost:8000/posts?page=1&page_size=5"
Popolare relazioni e media (populate) e mapping nel BFF
Strapi per impostazione predefinita non include relazioni e media completi: devi usare populate.
Nel BFF puoi astrarre questa complessità e restituire un payload più comodo.
Esempio: includere tutto e poi normalizzare:
params = {
"populate": "*",
"sort": "publishedAt:desc",
"pagination[page]": page,
"pagination[pageSize]": page_size,
}
Poi puoi convertire i record dal formato Strapi (data, attributes) a uno schema più lineare
prima di mandarlo al frontend.
Webhooks: far reagire Python ai cambiamenti in Strapi
Se vuoi che il servizio Python reagisca a “publish”/“update” (es. invalidare cache, inviare notifiche, rigenerare pagine), Strapi supporta webhooks.
- In Strapi: Settings → Webhooks → Create new webhook
- Inserisci l’URL del servizio Python, ad esempio
http://python-api:8000/strapi-webhook(interno a Docker) - Seleziona eventi:
entry.create,entry.update,entry.publish, ecc.
Endpoint FastAPI di esempio:
from fastapi import Request
@app.post("/strapi-webhook")
async def strapi_webhook(req: Request):
payload = await req.json()
# Verifica firma, secret condiviso, o almeno una whitelist IP in produzione
# Esegui azioni: purge cache, rebuild, notify, ecc.
return {"received": True}
Persistenza degli upload e storage
In locale puoi usare lo storage su filesystem di Strapi (che finisce nel volume strapi_app).
In produzione spesso conviene uno storage esterno (S3 compatibile, Cloud storage), per scalare orizzontalmente.
Se rimani su filesystem, assicurati che il volume includa la cartella public/uploads.
Con il volume /opt/app stai già persistendo tutto il progetto, inclusi gli upload.
Modalità sviluppo vs produzione
La modalità sviluppo (npm run develop) è comoda: hot reload e log più verbosi.
In produzione di solito:
- Imposti
NODE_ENV=production - Costruisci l’admin panel una volta (
npm run build) - Avvii con
npm start
Esempio di Dockerfile “production-ish” (idea, non obbligatorio per questa guida):
FROM node:20-alpine AS build
WORKDIR /opt/app
COPY . .
RUN npm ci
RUN npm run build
FROM node:20-alpine
WORKDIR /opt/app
COPY --from=build /opt/app /opt/app
ENV NODE_ENV=production
EXPOSE 1337
CMD ["npm", "start"]
Nel caso in cui generi l’app a runtime (come in questa guida), il pattern multi-stage è meno immediato: per produzione è preferibile avere l’app Strapi “versionata” nel repo (o in un artifact) e costruire un’immagine deterministica.
Consigli pratici
- Versiona i content types: evita di affidarti solo allo stato del volume. In team, crea e committa l’app Strapi (API, components, config).
- Non esporre Strapi pubblicamente senza protezione: davanti metti un reverse proxy, HTTPS, rate limit e hardening; limita i permessi “Public”.
- Separa gli ambienti: .env diverso per dev/stage/prod, segreti fuori dal codice.
- Osservabilità: log strutturati e metriche per Strapi e Python.
- Backup: backup regolari di Postgres e, se usi filesystem, anche dei media.
Riepilogo
Hai costruito uno stack Docker Compose con Strapi e Postgres e un servizio Python che consuma le API di Strapi. Da qui puoi:
- aggiungere un frontend (Next.js, Nuxt, React, ecc.)
- spostare lo storage dei media su S3
- aggiungere caching nel Python service (Redis) e invalidazione via webhooks
- fare deploy su un orchestratore (Docker Swarm, Kubernetes) o su una piattaforma PaaS
Il punto chiave è mantenere Strapi come “fonte verità” per i contenuti e usare Python dove serve orchestrazione, logica e integrazione.