Serve the CMS admin panel under /admin on the public domain
Replace the separate CMS subdomain with path-based routing in Caddy: /admin and /uploads go to Strapi, everything else to the frontend. Strapi 5 nests its whole admin panel (UI and API) under /admin, so this one prefix is enough and never collides with the frontend's own /api routes. Drops CMS_DOMAIN and the unused STRAPI_URL env var on the cms service; PUBLIC_STRAPI_URL now points at the same origin as the site. Authentication is unchanged: Strapi's own admin login still gates the panel, this only changes how it's reached.
This commit is contained in:
+2
-2
@@ -2,7 +2,6 @@
|
||||
|
||||
# --- Domains (Caddy) ---
|
||||
PUBLIC_DOMAIN=blog.localhost
|
||||
CMS_DOMAIN=cms.blog.localhost
|
||||
ACME_EMAIL=admin@example.com
|
||||
|
||||
# --- Database ---
|
||||
@@ -25,4 +24,5 @@ STRAPI_URL=http://cms:1337
|
||||
# Public base URL of the website, used for canonical URLs and Open Graph.
|
||||
PUBLIC_SITE_URL=https://blog.localhost
|
||||
# Public base URL of Strapi, used to build absolute media URLs in the browser.
|
||||
PUBLIC_STRAPI_URL=https://cms.blog.localhost
|
||||
# Same origin as PUBLIC_SITE_URL: Caddy proxies /admin and /uploads to Strapi.
|
||||
PUBLIC_STRAPI_URL=https://blog.localhost
|
||||
|
||||
@@ -16,17 +16,25 @@ sitemap e `robots.txt` dinamici, ricerca, e i test (nessun framework ancora conf
|
||||
## Architettura
|
||||
|
||||
```text
|
||||
Browser → Caddy ─┬─ dominio pubblico → Nuxt 4 (SSR) → REST Strapi
|
||||
└─ sottodominio CMS → Strapi 5 → PostgreSQL
|
||||
Browser → Caddy ─┬─ /admin, /uploads → Strapi 5 → PostgreSQL
|
||||
└─ tutto il resto → Nuxt 4 (SSR) → REST Strapi
|
||||
```
|
||||
|
||||
Caddy instrada per **path**, non per sottodominio: `PUBLIC_DOMAIN` serve sia il sito che,
|
||||
sotto `/admin` e `/uploads`, il pannello Strapi (Strapi 5 annida l'intera admin UI e la sua
|
||||
API sotto `/admin`, senza toccare `/api`). Un solo dominio, un solo certificato TLS.
|
||||
|
||||
- Strapi è la **sola** fonte di verità editoriale. Niente altro backend (no Express/Nest/Fastify):
|
||||
se serve logica server, sta in Nitro (`frontend/server/`) o in un controller Strapi.
|
||||
- I visitatori pubblici non si autenticano mai. Solo editor/admin usano l'auth Strapi.
|
||||
- **Il browser non parla mai con Strapi.** Le pagine chiamano gli endpoint Nitro in
|
||||
`frontend/server/api/`, che sono l'unico posto dove si costruiscono query Strapi. Così
|
||||
`NUXT_STRAPI_URL` resta l'indirizzo interno Docker, niente CORS e niente token nel client.
|
||||
Se aggiungi una vista, aggiungi l'endpoint lì e tipizza il ritorno in `shared/types/blog.ts`.
|
||||
- **Il browser dei visitatori pubblici non parla mai con Strapi.** Le pagine chiamano gli
|
||||
endpoint Nitro in `frontend/server/api/`, che sono l'unico posto dove si costruiscono
|
||||
query Strapi. Così `NUXT_STRAPI_URL` resta l'indirizzo interno Docker, niente CORS e
|
||||
niente token nel client. Se aggiungi una vista, aggiungi l'endpoint lì e tipizza il
|
||||
ritorno in `shared/types/blog.ts`. Fanno eccezione, per costruzione: l'admin panel
|
||||
(`/admin`, uso editor/admin autenticato) e le immagini cover, che il browser carica
|
||||
direttamente da `PUBLIC_STRAPI_URL` (`/uploads/...`, sola lettura, nessun'autenticazione
|
||||
richiesta né concessa).
|
||||
- Il Markdown dell'articolo è convertito in HTML **nell'endpoint**, non nel componente: il
|
||||
contenuto è già nell'HTML SSR e `marked` resta fuori dal bundle client.
|
||||
- L'**interfaccia** è tradotta in en/it/es/fr con `@nuxtjs/i18n`: stringhe in
|
||||
|
||||
@@ -4,8 +4,8 @@ Blog platform: a public website built with Nuxt, and a private Strapi CMS where
|
||||
articles are written. Everything runs behind Caddy via Docker Compose.
|
||||
|
||||
```text
|
||||
Browser → Caddy ─┬─ public domain → Nuxt (website)
|
||||
└─ cms subdomain → Strapi (CMS) → PostgreSQL
|
||||
Browser → Caddy ─┬─ /admin, /uploads → Strapi (CMS) → PostgreSQL
|
||||
└─ everything else → Nuxt (website)
|
||||
```
|
||||
|
||||
There are no front-end accounts: sign-up is disabled and only administrators write
|
||||
@@ -46,7 +46,8 @@ The first build takes a few minutes. `docker-compose.dev.yml` publishes the port
|
||||
`127.0.0.1` and leaves Caddy out; it must always be passed explicitly, so it can never be
|
||||
picked up by accident in production.
|
||||
|
||||
**4. Create the administrator account** at http://localhost:1337/admin. This is the first
|
||||
**4. Create the administrator account** at http://localhost:1337/admin (dev bypasses Caddy,
|
||||
so the CMS is reached directly on its port). This is the first
|
||||
run, so the form creates the account — pick your own credentials.
|
||||
|
||||
**5. Write something.** In the admin panel: create a **Category**, then an **Article**
|
||||
@@ -72,16 +73,15 @@ override it with `NUXT_STRAPI_URL` if needed. Strapi reads its own `cms/.env`.
|
||||
|
||||
## Production
|
||||
|
||||
**1. Point the DNS at the server.** Two `A` records (and `AAAA` if you have IPv6) on the
|
||||
**1. Point the DNS at the server.** One `A` record (and `AAAA` if you have IPv6) on the
|
||||
public IP of the machine:
|
||||
|
||||
| Record | Purpose |
|
||||
|---|---|
|
||||
| `example.com` | the website |
|
||||
| `cms.example.com` | the Strapi admin panel |
|
||||
| `example.com` | the website and, at `/admin`, the Strapi admin panel |
|
||||
|
||||
Wait for the records to resolve before starting the stack — Caddy requests the
|
||||
certificates on the first boot and a failed challenge means a retry delay.
|
||||
Wait for the record to resolve before starting the stack — Caddy requests the
|
||||
certificate on the first boot and a failed challenge means a retry delay.
|
||||
|
||||
**2. Open the firewall** for ports `80` and `443` only. Port `80` is required: Caddy uses
|
||||
it for the ACME challenge and to redirect to HTTPS. PostgreSQL, Strapi and Nuxt are only
|
||||
@@ -92,10 +92,9 @@ reachable inside the Docker network — do not publish their ports.
|
||||
| Variable | Value |
|
||||
|---|---|
|
||||
| `PUBLIC_DOMAIN` | `example.com` |
|
||||
| `CMS_DOMAIN` | `cms.example.com` |
|
||||
| `ACME_EMAIL` | a mailbox you read — Let's Encrypt sends expiry warnings there |
|
||||
| `PUBLIC_SITE_URL` | `https://example.com` |
|
||||
| `PUBLIC_STRAPI_URL` | `https://cms.example.com` |
|
||||
| `PUBLIC_STRAPI_URL` | `https://example.com` — same origin, Caddy proxies `/admin` and `/uploads` to Strapi |
|
||||
| `STRAPI_URL` | leave it as `http://cms:1337` — internal address, never public |
|
||||
|
||||
Then generate **fresh** secrets on that machine with the same loop as in development —
|
||||
@@ -112,11 +111,11 @@ already ignored.
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
This time Caddy is included: it serves the website on `PUBLIC_DOMAIN`, the CMS on
|
||||
`CMS_DOMAIN`, obtains and renews the TLS certificates on its own, and adds HSTS and the
|
||||
other security headers.
|
||||
This time Caddy is included: it serves both the website and, under `/admin` and
|
||||
`/uploads`, the CMS on `PUBLIC_DOMAIN`, obtains and renews the TLS certificate on its own,
|
||||
and adds HSTS and the other security headers.
|
||||
|
||||
**5. Create the administrator account** at `https://cms.example.com/admin`, immediately,
|
||||
**5. Create the administrator account** at `https://example.com/admin`, immediately,
|
||||
before anyone else finds the URL — the first visitor to that form is the one who gets the
|
||||
account. Then publish as in development.
|
||||
|
||||
|
||||
+15
-9
@@ -15,15 +15,21 @@
|
||||
{$PUBLIC_DOMAIN} {
|
||||
import security_headers
|
||||
encode zstd gzip
|
||||
reverse_proxy frontend:3000
|
||||
}
|
||||
|
||||
{$CMS_DOMAIN} {
|
||||
import security_headers
|
||||
encode zstd gzip
|
||||
# Uploaded media can be large; Strapi's own limit still applies.
|
||||
request_body {
|
||||
max_size 100MB
|
||||
# /admin and /uploads go to Strapi; everything else to the frontend.
|
||||
# Strapi 5 nests the whole admin panel (UI + its own API) under /admin,
|
||||
# so this single prefix is enough - it never touches /api, which stays
|
||||
# reserved for the frontend's own Nitro endpoints.
|
||||
@cms path /admin* /uploads*
|
||||
handle @cms {
|
||||
# Uploaded media can be large; Strapi's own limit still applies.
|
||||
request_body {
|
||||
max_size 100MB
|
||||
}
|
||||
reverse_proxy cms:1337
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy frontend:3000
|
||||
}
|
||||
reverse_proxy cms:1337
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ services:
|
||||
cms:
|
||||
ports:
|
||||
- "127.0.0.1:1337:1337"
|
||||
environment:
|
||||
STRAPI_URL: http://localhost:1337
|
||||
|
||||
frontend:
|
||||
ports:
|
||||
|
||||
@@ -36,7 +36,6 @@ services:
|
||||
TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
STRAPI_URL: https://${CMS_DOMAIN}
|
||||
volumes:
|
||||
- cms-uploads:/app/public/uploads
|
||||
|
||||
@@ -71,7 +70,6 @@ services:
|
||||
- 8.8.8.8
|
||||
environment:
|
||||
PUBLIC_DOMAIN: ${PUBLIC_DOMAIN}
|
||||
CMS_DOMAIN: ${CMS_DOMAIN}
|
||||
ACME_EMAIL: ${ACME_EMAIL}
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
|
||||
Reference in New Issue
Block a user