Strapi è un headless CMS basato su Node.js che consente di modellare contenuti (content types), gestire utenti e ruoli, e pubblicare API REST e GraphQL. In questo articolo vedrai un flusso completo e riproducibile per far girare Strapi in container insieme a un database (PostgreSQL) tramite Docker Compose, e per consumare le API da un'applicazione web Node.js. L'obiettivo è ottenere:
- Un ambiente di sviluppo locale consistente su qualunque macchina.
- Persistenza dei dati tramite volumi Docker.
- Configurazione pulita tramite variabili d'ambiente.
- Un esempio di applicazione Node.js che interroga Strapi.
- Una base solida per passare in produzione (immagini buildate, hardening e reverse proxy).
Prerequisiti
- Docker e Docker Compose installati.
- Node.js (utile per l'app client, e opzionale se vuoi fare tutto solo in container).
- Conoscenze base di HTTP, API e variabili d'ambiente.
Scenario e struttura del progetto
Creiamo una struttura di cartelle semplice con due servizi:
- strapi: il CMS, con codice applicativo montato in volume in sviluppo.
- db: PostgreSQL come database per Strapi.
- web (opzionale ma consigliato): una piccola app Node.js (Express) che consuma Strapi.
Esempio di struttura:
mkdir -p my-stack/{strapi,web}
cd my-stack
Creazione del progetto Strapi
Puoi creare il progetto Strapi in due modi:
- Crearlo sul tuo host con Node.js e poi containerizzarlo.
- Crearlo direttamente in un container temporaneo (utile se non vuoi installare Node.js localmente).
Opzione A: crea Strapi sul tuo host
cd strapi
npx create-strapi-app@latest . --quickstart
Con --quickstart Strapi usa SQLite per partire subito. Noi lo riconfigureremo per PostgreSQL via Docker Compose e variabili d'ambiente.
Opzione B: crea Strapi in un container temporaneo
Se preferisci evitare installazioni locali:
cd strapi
docker run --rm -it -v "$PWD":/app -w /app node:20-bullseye bash -lc "npx create-strapi-app@latest . --quickstart"
Al termine avrai una cartella strapi con i file del progetto.
Configurazione di Strapi per PostgreSQL
Strapi legge la configurazione del database da variabili d'ambiente. Nelle versioni recenti la configurazione tipica si trova in config/database.js. Se il file non esiste, crealo.
// strapi/config/database.js
module.exports = ({ env }) => ({
connection: {
client: "postgres",
connection: {
host: env("DATABASE_HOST", "localhost"),
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 },
},
});
Nota: in produzione potresti voler gestire SSL e CA certificate in modo esplicito (ad esempio con ssl: { rejectUnauthorized: true, ca: ... }), ma per un setup locale con container è normale disattivare SSL.
Variabili d'ambiente: .env
Crea un file .env nella root del progetto (my-stack/.env) per centralizzare la configurazione di Compose. Separare questi valori riduce la duplicazione e facilita ambienti diversi (dev, staging, prod).
# my-stack/.env
# PostgreSQL
POSTGRES_DB=strapi
POSTGRES_USER=strapi
POSTGRES_PASSWORD=strapi_password
# Strapi
STRAPI_HOST=0.0.0.0
STRAPI_PORT=1337
APP_KEYS=someAppKey1,someAppKey2
API_TOKEN_SALT=someApiTokenSalt
ADMIN_JWT_SECRET=someAdminJwtSecret
JWT_SECRET=someJwtSecret
Per sicurezza:
- Non versionare
.env(aggiungilo a.gitignore). - In produzione usa segreti reali e generati in modo sicuro.
Dockerfile per Strapi
In sviluppo, spesso non serve un Dockerfile complesso perché puoi usare l'immagine Node ufficiale e montare il codice in volume. Tuttavia un Dockerfile è utile per la produzione e per avere un punto unico di build. Creiamo un Dockerfile semplice (multi-stage) nella cartella strapi.
# strapi/Dockerfile
FROM node:20-bullseye AS deps
WORKDIR /app
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
RUN if [ -f package-lock.json ]; then npm ci; \
elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm i --frozen-lockfile; \
else npm i; fi
FROM node:20-bullseye AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# In produzione: build dell'admin e compilazione
RUN npm run build
FROM node:20-bullseye AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app ./
EXPOSE 1337
CMD ["npm", "run", "start"]
In sviluppo useremo invece npm run develop per hot reload. Il Dockerfile sopra è pensato per un avvio ottimizzato in produzione.
docker-compose.yml: Strapi + PostgreSQL
Ora creiamo il file docker-compose.yml nella root del progetto (my-stack/docker-compose.yml). Useremo volumi per persistere il database e (in sviluppo) montare il codice Strapi e l'app web.
# my-stack/docker-compose.yml
services:
db:
image: postgres:16
container_name: my_stack_db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
ports:
- "5432:5432"
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: my_stack_strapi
environment:
NODE_ENV: development
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: ${APP_KEYS}
API_TOKEN_SALT: ${API_TOKEN_SALT}
ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
JWT_SECRET: ${JWT_SECRET}
volumes:
- ./strapi:/app
- /app/node_modules
ports:
- "1337:1337"
depends_on:
db:
condition: service_healthy
command: ["npm", "run", "develop"]
volumes:
db_data:
Punti importanti:
- depends_on con healthcheck: evita che Strapi parta prima che Postgres sia pronto.
- volumi:
db_datamantiene i dati; il bind mount./strapi:/appabilita il live editing in sviluppo. - /app/node_modules come volume anonimo: impedisce che la cartella
node_modulesvenga sovrascritta dai file dell'host.
Avvio dello stack
Dalla root del progetto:
docker compose up --build
Quando Strapi è pronto:
- Admin panel:
http://localhost:1337/admin - API base:
http://localhost:1337/api
Al primo avvio ti verrà chiesto di creare l'utente amministratore dal pannello admin.
Creazione di un Content Type e pubblicazione API
Dal pannello admin:
- Vai su Content-Type Builder.
- Crea un tipo, ad esempio Post, con campi come
title(string),content(rich text),slug(UID) epublishedAt(data). - Salva e riavvia (Strapi in develop solitamente riavvia automaticamente).
Per rendere accessibili le API in lettura pubblica:
- Vai in Settings → Users & Permissions → Roles.
- Apri il ruolo Public.
- Abilita i permessi per
findefindOnesul content type Post. - Salva.
Ora puoi testare (se hai creato almeno un contenuto):
curl -s http://localhost:1337/api/posts | jq
Se non hai jq installato, rimuovilo e guarda l'output JSON grezzo.
Token API e accesso autenticato
Per API protette o integrazioni server-to-server è consigliabile usare gli API Tokens di Strapi (dal pannello admin, sezione Settings o API Tokens a seconda della versione). Un token ti permette di chiamare endpoint senza passare da login utente, con permessi associati.
Esempio di chiamata con token:
curl -H "Authorization: Bearer YOUR_STRAPI_API_TOKEN" \
http://localhost:1337/api/posts
App web Node.js che consuma Strapi (Express)
Ora aggiungiamo una piccola app Express nella cartella web che legge i post da Strapi e li rende come HTML. Questo esempio è volutamente minimale: in un progetto reale potresti usare Next.js, Nuxt, SvelteKit o qualunque frontend.
Inizializzazione del progetto web
cd web
npm init -y
npm i express node-fetch
npm i -D nodemon
Aggiungi uno script di sviluppo in web/package.json:
{
"scripts": {
"dev": "nodemon server.js",
"start": "node server.js"
}
}
Server Express
Crea web/server.js:
// web/server.js
import express from "express";
import fetch from "node-fetch";
const app = express();
const port = process.env.PORT || 3000;
const STRAPI_URL = process.env.STRAPI_URL || "http://strapi:1337";
const STRAPI_TOKEN = process.env.STRAPI_TOKEN || "";
function strapiHeaders() {
const headers = { "Accept": "application/json" };
if (STRAPI_TOKEN) headers["Authorization"] = `Bearer ${STRAPI_TOKEN}`;
return headers;
}
app.get("/", async (req, res) => {
try {
const url = new URL("/api/posts", STRAPI_URL);
url.searchParams.set("populate", "*");
url.searchParams.set("sort", "publishedAt:desc");
const r = await fetch(url.toString(), { headers: strapiHeaders() });
if (!r.ok) {
const text = await r.text();
return res.status(502).send(`Errore da Strapi: ${r.status} ${text}`);
}
const data = await r.json();
const items = (data.data || []).map((item) => {
const attrs = item.attributes || {};
const title = attrs.title || "(senza titolo)";
const content = attrs.content || "";
return `<li><strong>${escapeHtml(title)}</strong><br />${escapeHtml(content)}</li>`;
});
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.send(`<!doctype html>
<html lang="it">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Posts</title></head>
<body>
<h1>Posts da Strapi</h1>
<ul>${items.join("")}</ul>
</body>
</html>`);
} catch (e) {
res.status(500).send(`Errore: ${e?.message || e}`);
}
});
app.listen(port, () => {
console.log(`Web app su http://localhost:${port}`);
});
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """)
.replaceAll("'", "'");
}
Nota: il file usa ESM (import). Per abilitarlo, aggiungi "type": "module" in web/package.json.
Dockerfile per la web app
Crea web/Dockerfile:
# web/Dockerfile
FROM node:20-bullseye
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci || npm i
COPY . .
EXPOSE 3000
CMD ["npm", "run", "start"]
Aggiunta del servizio web a Docker Compose
Aggiorna docker-compose.yml aggiungendo:
web:
build:
context: ./web
dockerfile: Dockerfile
container_name: my_stack_web
environment:
PORT: 3000
STRAPI_URL: http://strapi:1337
# STRAPI_TOKEN: "..." # opzionale se usi API token
volumes:
- ./web:/app
- /app/node_modules
ports:
- "3000:3000"
depends_on:
- strapi
command: ["npm", "run", "dev"]
Ora puoi avviare tutto e aprire:
- Web app:
http://localhost:3000 - Strapi admin:
http://localhost:1337/admin
Gestione dei dati: volumi, backup e reset
Con il volume db_data i dati di PostgreSQL restano anche se ricrei i container. Alcuni comandi utili:
# fermare i servizi
docker compose down
# fermare e rimuovere anche i volumi (attenzione: perdi i dati)
docker compose down -v
# vedere i volumi
docker volume ls
Backup rapido del database da container:
docker exec -t my_stack_db pg_dump -U strapi -d strapi > backup.sql
Ripristino:
cat backup.sql | docker exec -i my_stack_db psql -U strapi -d strapi
Dev vs produzione: cosa cambia davvero
In sviluppo l'obiettivo è iterare velocemente, quindi:
- Bind mount del codice (
./strapi:/app) - Comando
npm run develop - Possibile esporre Postgres sulla porta 5432
In produzione invece conviene:
- Costruire un'immagine Strapi con
npm run builde avvio connpm run start. - Non montare il codice in volume.
- Non esporre Postgres pubblicamente (niente mapping porta 5432 verso host).
- Gestire segreti e variabili in modo sicuro (secrets, vault, o strumenti del provider).
- Mettere un reverse proxy davanti (Nginx/Traefik) con HTTPS.
Un esempio di override per produzione (file docker-compose.prod.yml) potrebbe impostare NODE_ENV=production, rimuovere i volumi di codice e cambiare i comandi. In questo modo puoi fare:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
Hardening minimo consigliato
- Segreti: usa valori robusti per
APP_KEYS,JWT_SECRET,ADMIN_JWT_SECRET,API_TOKEN_SALT. - Permessi: concedi al ruolo Public solo ciò che serve. Valuta autenticazione per tutto il resto.
- CORS: configura origini consentite per evitare accessi indesiderati dai browser.
- Rate limiting: se esponi API pubbliche, proteggi con limiti (reverse proxy o middleware).
- Uploads: per file e immagini, in produzione valuta storage esterno (S3 compatibile) invece del filesystem del container.
Debug e problemi comuni
Strapi non parte e segnala errori di connessione al DB
- Verifica che
DATABASE_HOSTsiadb(nome servizio Compose), nonlocalhost. - Controlla i log:
docker compose logs -f strapiedocker compose logs -f db. - Assicurati che l'healthcheck di Postgres sia ok.
L'admin panel non carica correttamente
- In alcune configurazioni serve ricostruire:
docker compose up --build. - Se hai cambiato variabili critiche, elimina cache/build dell'admin (dipende dalla versione) e ricostruisci.
Permessi API: ricevo 403 o risposte vuote
- Controlla i permessi del ruolo Public o Authenticated.
- Se usi draft/publish, verifica che i contenuti siano pubblicati.
Conclusione
Hai costruito uno stack completo con Strapi in Docker e PostgreSQL via Docker Compose, con persistenza dei dati, configurazione via variabili d'ambiente e un esempio di applicazione Node.js che consuma le API. Da qui puoi evolvere in modo naturale:
- aggiungendo GraphQL e query più ricche,
- introducendo un frontend moderno (Next.js) per SSR/ISR,
- spostando file upload su storage esterno,
- preparando un setup di produzione con reverse proxy e HTTPS.
Se mantieni separati configurazione, build e runtime, Docker Compose resta uno strumento efficace sia per sviluppo che per ambienti di staging e test integrati.