Come usare Strapi in un'applicazione web con Python usando Docker e Docker Compose

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 su http://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:

  • db usa un named volume db_data per persistere i dati.
  • strapi dipende da db e aspetta l’healthcheck di Postgres.
  • strapi persiste la sua app (config, API, plugin, upload) in strapi_app dentro /opt/app.
  • python-api parla a Strapi usando http://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:

  1. Bind mount: crea una cartella strapi/overrides/config/database.js e montala su /opt/app/config/database.js dal compose.
  2. 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:

  1. Content-type Builder → Create new collection type → Post
  2. Aggiungi i campi e salva
  3. Vai su SettingsRolesPublic
  4. Abilita le permission find e findOne su Post (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/posts lista
  • GET /api/posts/:id dettaglio
  • 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: SettingsAPI 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.

  1. In Strapi: SettingsWebhooks → Create new webhook
  2. Inserisci l’URL del servizio Python, ad esempio http://python-api:8000/strapi-webhook (interno a Docker)
  3. 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.

Torna su