Come usare Strapi in un'applicazione web con Node.js usando Docker e Docker Compose

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:

  1. Crearlo sul tuo host con Node.js e poi containerizzarlo.
  2. 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_data mantiene i dati; il bind mount ./strapi:/app abilita il live editing in sviluppo.
  • /app/node_modules come volume anonimo: impedisce che la cartella node_modules venga 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:

  1. Vai su Content-Type Builder.
  2. Crea un tipo, ad esempio Post, con campi come title (string), content (rich text), slug (UID) e publishedAt (data).
  3. Salva e riavvia (Strapi in develop solitamente riavvia automaticamente).

Per rendere accessibili le API in lettura pubblica:

  1. Vai in SettingsUsers & PermissionsRoles.
  2. Apri il ruolo Public.
  3. Abilita i permessi per find e findOne sul content type Post.
  4. 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("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll("\"", "&quot;")
    .replaceAll("'", "&#39;");
}

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 build e avvio con npm 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_HOST sia db (nome servizio Compose), non localhost.
  • Controlla i log: docker compose logs -f strapi e docker 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.

Torna su