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:
2026-08-25 19:49:37 +02:00
parent cb29c68d75
commit e927396505
6 changed files with 44 additions and 35 deletions
+13 -14
View File
@@ -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.