Questo articolo mostra un flusso completo e ripetibile per integrare Strapi (headless CMS) in una applicazione web con backend in Go, orchestrando tutto con Docker e Docker Compose. L’obiettivo è ottenere:
- Un’istanza Strapi in container, con database persistente (PostgreSQL).
- Un servizio Go in container che legge i contenuti da Strapi tramite REST API.
- Un setup locale consistente per sviluppo e test, facilmente estendibile a staging/produzione.
Prerequisiti
- Docker e Docker Compose installati.
- Conoscenze base di Go e HTTP/JSON.
- Una macchina con almeno 4 GB di RAM disponibili per Docker (consigliato 6–8 GB).
Architettura di riferimento
Nel setup che costruiremo:
- strapi: container Strapi esposto su
http://localhost:1337. - db: PostgreSQL con volume per persistenza.
- api: servizio Go esposto su
http://localhost:8080.
Il servizio Go non “monta” Strapi dentro al proprio processo: lo consuma come un servizio HTTP esterno. Questo rende l’integrazione pulita e mantiene la separazione tra gestione contenuti (Strapi) e logica applicativa (Go).
Struttura del progetto
Una struttura minimale (puoi adattarla alle tue preferenze):
.
├── compose.yaml
├── strapi
│ ├── Dockerfile
│ └── .env
└── api
├── Dockerfile
├── go.mod
├── go.sum
├── main.go
└── .env
1) Containerizzare Strapi
File strapi/Dockerfile
Qui creiamo una build basata su Node LTS e installiamo le dipendenze del progetto Strapi. In sviluppo è comune montare il codice come volume; in produzione useresti un’immagine più “chiusa” e ottimizzata.
FROM node:20-bookworm-slim
WORKDIR /app
# Dipendenze necessarie a Strapi e ad alcuni provider (es. sharp)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential python3 pkg-config libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev \
&& rm -rf /var/lib/apt/lists/*
# Copiamo solo package* per sfruttare la cache
COPY package.json package-lock.json* ./
RUN npm ci
# Copiamo il resto
COPY . .
EXPOSE 1337
CMD ["npm", "run", "develop"]
Nota: se inizializzi Strapi con npx create-strapi-app, avrai già package.json ecc.
Se preferisci generare Strapi direttamente nel container, è possibile, ma per un progetto reale conviene versionare
il codice Strapi nel repository.
File strapi/.env
Variabili tipiche per collegare Strapi a PostgreSQL e impostare chiavi applicative. In locale puoi usare valori di esempio; in ambienti reali usa segreti robusti.
# Strapi
HOST=0.0.0.0
PORT=1337
APP_KEYS=change-me-1,change-me-2,change-me-3,change-me-4
API_TOKEN_SALT=change-me-token-salt
ADMIN_JWT_SECRET=change-me-admin-jwt
JWT_SECRET=change-me-jwt
# Database (PostgreSQL)
DATABASE_CLIENT=postgres
DATABASE_HOST=db
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=strapi
DATABASE_SSL=false
2) Servizio Go: consumare Strapi via REST
Il servizio Go esporrà un endpoint, ad esempio /posts, che interroga Strapi (collection type “Post”)
e restituisce una vista “pulita” al client. Questo pattern è utile quando vuoi:
- Un layer di cache, business rules o aggregazioni non gestite da Strapi.
- Uniformare la risposta per il frontend o per altri consumatori.
- Gestire autenticazione/permessi in modo centralizzato.
File api/go.mod
module example.com/strapi-go-docker
go 1.22
File api/main.go
Questo esempio usa la REST API di Strapi. Strapi restituisce strutture JSON con data,
attributes ecc. La forma esatta dipende dalla versione e dai tipi creati; qui gestiamo un caso tipico:
una collezione “posts” con campi title, slug, content, e publishedAt.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"time"
)
type Config struct {
ListenAddr string
StrapiURL string
StrapiToken string
HTTPTimeout time.Duration
}
func loadConfig() Config {
timeout := 8 * time.Second
if v := os.Getenv("HTTP_TIMEOUT"); v != "" {
if d, err := time.ParseDuration(v); err == nil {
timeout = d
}
}
return Config{
ListenAddr: getenvDefault("LISTEN_ADDR", ":8080"),
StrapiURL: getenvDefault("STRAPI_URL", "http://strapi:1337"),
StrapiToken: os.Getenv("STRAPI_API_TOKEN"),
HTTPTimeout: timeout,
}
}
func getenvDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
type StrapiListResponse[T any] struct {
Data []struct {
ID int `json:"id"`
Attributes T `json:"attributes"`
} `json:"data"`
Meta any `json:"meta"`
}
type PostAttributes struct {
Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"`
PublishedAt time.Time `json:"publishedAt"`
}
type PostDTO struct {
ID int `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"`
PublishedAt time.Time `json:"publishedAt"`
}
func main() {
cfg := loadConfig()
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
posts, err := fetchPosts(r.Context(), cfg)
if err != nil {
log.Printf("fetch posts error: %v", err)
http.Error(w, "upstream error", http.StatusBadGateway)
return
}
writeJSON(w, posts)
})
srv := &http.Server{
Addr: cfg.ListenAddr,
Handler: withBasicMiddleware(mux),
ReadHeaderTimeout: 5 * time.Second,
}
log.Printf("Go API listening on %s", cfg.ListenAddr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
func withBasicMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r)
})
}
func writeJSON(w http.ResponseWriter, v any) {
enc := json.NewEncoder(w)
enc.SetEscapeHTML(true)
if err := enc.Encode(v); err != nil {
http.Error(w, "encode error", http.StatusInternalServerError)
}
}
func fetchPosts(ctx context.Context, cfg Config) ([]PostDTO, error) {
// Esempio: /api/posts?sort=publishedAt:desc&pagination[pageSize]=20
url := fmt.Sprintf("%s/api/posts?sort=publishedAt:desc&pagination[pageSize]=20", cfg.StrapiURL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
// Se usi Token API di Strapi: Authorization: Bearer
if cfg.StrapiToken != "" {
req.Header.Set("Authorization", "Bearer "+cfg.StrapiToken)
}
c := &http.Client{Timeout: cfg.HTTPTimeout}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("strapi returned %s", resp.Status)
}
var raw StrapiListResponse[PostAttributes]
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, err
}
out := make([]PostDTO, 0, len(raw.Data))
for _, item := range raw.Data {
out = append(out, PostDTO{
ID: item.ID,
Title: item.Attributes.Title,
Slug: item.Attributes.Slug,
Content: item.Attributes.Content,
PublishedAt: item.Attributes.PublishedAt,
})
}
return out, nil
}
File api/.env
Qui configuriamo il servizio Go con l’URL interno di Strapi. In Compose, i servizi si risolvono via DNS sul nome
del servizio (strapi), quindi http://strapi:1337 è corretto dal container Go.
LISTEN_ADDR=:8080
STRAPI_URL=http://strapi:1337
HTTP_TIMEOUT=8s
# Se usi un token creato su Strapi (Settings > API Tokens)
# STRAPI_API_TOKEN=...
File api/Dockerfile
Dockerfile multi-stage: compila il binario statico e lo copia in una immagine minimale.
FROM golang:1.22-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/api ./...
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=build /out/api /app/api
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app/api"]
3) Docker Compose: Strapi + Postgres + Go
Creiamo un file compose.yaml nella root. Questo orchestri i servizi, definisce volumi persistenti e
passa le variabili d’ambiente.
name: strapi-go
services:
db:
image: postgres:16
environment:
POSTGRES_DB: strapi
POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U strapi -d strapi"]
interval: 5s
timeout: 5s
retries: 20
strapi:
build:
context: ./strapi
env_file:
- ./strapi/.env
depends_on:
db:
condition: service_healthy
ports:
- "1337:1337"
volumes:
- ./strapi:/app
- /app/node_modules
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:1337/admin').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 30
api:
build:
context: ./api
env_file:
- ./api/.env
depends_on:
strapi:
condition: service_healthy
ports:
- "8080:8080"
restart: unless-stopped
volumes:
db_data:
Se preferisci non montare il codice Strapi (approccio più simile alla produzione), puoi rimuovere il volume
./strapi:/app e costruire un’immagine con l’app Strapi già dentro. In sviluppo però il bind mount
accelera molto il ciclo di feedback.
4) Inizializzare il progetto Strapi
Se non hai ancora una cartella Strapi pronta, puoi crearla una tantum. Un approccio pratico è generare Strapi localmente e poi usarlo con Compose:
# Nella root del progetto
npx create-strapi-app@latest strapi --quickstart
Poi modifica la configurazione del database per usare PostgreSQL, oppure imposta le variabili in strapi/.env
come mostrato prima. In Strapi, la configurazione DB dipende dalla versione; per Strapi v4 tipicamente si lavora
in config/database.js e si legge da process.env.
Esempio di strapi/config/database.js (Strapi v4) che usa le variabili d’ambiente:
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),
},
debug: false,
},
});
Se la tua versione di Strapi differisce (v5 o successiva), i file e le opzioni possono cambiare leggermente. Il principio rimane: “DB config da env” e “host/port in container”.
5) Avvio dell’ambiente
docker compose up --build
Dovresti ottenere:
- Admin Strapi su
http://localhost:1337/admin - API Go su
http://localhost:8080/posts
Al primo avvio Strapi chiederà di creare l’utente admin. Dopo l’onboarding, crea un
Collection Type chiamato ad esempio Post con i campi:
title(Text)slug(UID basato su title)content(Rich text o Text)publishedAt(gestito automaticamente se abiliti “Draft & Publish”)
6) Permessi e sicurezza: Public, API Token e ruoli
In Strapi, le API possono essere protette in più modi. Per sviluppo è comodo abilitare l’accesso pubblico in: Settings → Users & Permissions plugin → Roles → Public. In produzione invece è spesso meglio:
- Disabilitare o limitare il ruolo Public.
- Usare API Tokens (Settings → API Tokens) e inviarli dal backend Go.
- Gestire autenticazione utente con il plugin Users & Permissions e chiamare Strapi con JWT utente, se serve.
Nel codice Go, l’header Authorization: Bearer ... viene aggiunto se STRAPI_API_TOKEN
è presente. Questo è un buon punto di partenza per evitare che il frontend esponga token o segreti.
7) Popolare relazioni e media
Strapi per default non “popola” relazioni e media nella REST API; serve la query populate.
Esempio, se un Post ha un’immagine “cover” (media field) e un autore (relation):
# Esempio REST
GET /api/posts?populate[cover]=*&populate[author]=*
Dal lato Go, puoi ampliare i tipi JSON oppure decodificare in mappe generiche se vuoi flessibilità. Per progetti grandi conviene modellare i payload e isolare la logica di trasformazione in un package dedicato.
8) Caching e resilienza nel servizio Go
Una volta che Go fa da “facciata” tra client e Strapi, puoi aggiungere:
- Cache in memoria con TTL per ridurre chiamate a Strapi (utile per pagine pubbliche).
- Timeout e retry per gestire errori transitori.
- Circuit breaker se Strapi è instabile.
Anche senza introdurre librerie, un timeout sul client HTTP (già presente) evita request bloccate. Per la cache puoi iniziare con una struttura semplice e poi passare a Redis quando serve.
9) Migliorare lo sviluppo: hot reload e logs
Per Strapi, npm run develop già supporta un ciclo di sviluppo rapido.
Per Go puoi usare strumenti di hot reload (ad esempio air) in un container dedicato
o in locale, mantenendo Strapi e DB in Compose.
Esempio di workflow ibrido molto pratico:
- Avvia in Docker:
dbestrapi. - Esegui Go in locale (o con hot reload) puntando a
http://localhost:1337.
In Compose, puoi separare profili (es. profiles: ["dev"]) per avere varianti “solo infrastruttura” e
“tutto in container”.
10) Deployment: considerazioni essenziali
Per produzione, tipicamente conviene:
- Usare un database gestito o un volume affidabile (PostgreSQL con backup e retention).
- Costruire un’immagine Strapi “immutabile” (senza bind mount) e avviare con
npm start. - Impostare correttamente
NODE_ENV=productione le variabili di sicurezza (segreti reali). - Mettere un reverse proxy (Nginx/Traefik) con TLS davanti a Strapi e/o Go.
- Limitare l’admin Strapi a rete interna o VPN, se possibile.
In ambienti orchestrati (Kubernetes), Strapi e Go diventano deployment separati, con ConfigMap/Secret e un servizio DB esterno. Tuttavia, la logica e i concetti restano identici a Compose.
Checklist rapida di troubleshooting
| Problema | Possibile causa | Soluzione |
|---|---|---|
| Strapi non parte e va in loop | DB non raggiungibile o migrazioni fallite | Controlla log di db e strapi, verifica variabili DB, elimina volume solo se accetti perdita dati |
| Go riceve 403/401 da Strapi | Permessi Public disabilitati o token mancante/errato | Crea un API Token e imposta STRAPI_API_TOKEN, oppure abilita temporaneamente i permessi Public |
| Campi relazionali vuoti | Manca populate nella query |
Aggiungi populate per relazioni/media |
| Modifiche Strapi non si riflettono | Cache, build non ricostruita, volumi | In sviluppo usa bind mount; se necessario docker compose up --build e controlla /app/node_modules |
Conclusione
Con Strapi in container e un servizio Go che consuma i contenuti via REST, ottieni un’architettura modulare: il team contenuti lavora nell’admin Strapi, mentre l’applicazione Go applica logica, sicurezza e presentazione. Docker Compose rende l’ambiente riproducibile e riduce le differenze tra macchine di sviluppo.
Da qui puoi estendere il progetto con GraphQL (Strapi), caching Redis, code generation per i client, oppure con un frontend SSR/SPA che consuma l’API Go.