You start Strapi behind a domain (a staging subdomain, a Cloudflare tunnel, an ngrok URL, a Coolify or Dokploy deployment), open the admin panel, and get a blank page with this:
Blocked request. This host ("cms.example.com") is not allowed.
To allow this host, add "cms.example.com" to server.allowedHosts in vite.config.js.The error message tells you exactly what to do and almost nothing about where to do it. There is no vite.config.js at the root of a Strapi project, which is where most people look first.
Why this happens
Strapi 5's admin panel is a React single-page application, and Vite is the default bundler that builds it. When you run strapi develop, Vite runs a dev server to serve the admin panel with hot module replacement.
Vite validates the Host header on every request to that dev server. This is a security control, not a bug: it protects developers against DNS-rebinding attacks, where a malicious page in your browser resolves an attacker-controlled domain to 127.0.0.1 and talks to your local dev server. Vite tightened this from Vite 5 onwards, which is why the error started appearing for people who had been running the same setup for months.
By default Vite allows localhost and loopback addresses. Any other hostname, including your own staging domain, gets rejected.
Check your version first. Strapi's own development Vite config now sets
allowedHosts: truein the base config it builds before your overrides are applied (resolve-development-config, fixing #23491). On a current Strapi 5 release you should not hit this error at all. If you are seeing it, upgrading Strapi may be the whole fix, and a better one than the config below. The rest of this post applies to versions that predate that change, and to cases where you want an explicit allowlist rather than Strapi's permissive default.
The critical detail: this only affects strapi develop. In production you run strapi build followed by strapi start, which serves a static, pre-built admin bundle with no Vite dev server involved. If you are hitting this error on a public domain, that is a signal worth reading, and we come back to it at the end.
The fix
Create ./src/admin/vite.config.ts if it does not exist. Strapi looks for vite.config.js, .mjs, .ts, or .mts in ./src/admin/, and project templates ship an example file in that folder that merges a custom alias, which is a good starting point to replace:
import { mergeConfig, type UserConfig } from 'vite';
export default (config: UserConfig) => {
// Important: always return the modified config
return mergeConfig(config, {
server: {
allowedHosts: ['cms.example.com', '.preview.example.com'],
},
});
};JavaScript projects use the same shape:
const { mergeConfig } = require('vite');
module.exports = (config) => {
return mergeConfig(config, {
server: {
allowedHosts: ['cms.example.com', '.preview.example.com'],
},
});
};Then restart strapi develop. Note that Strapi passes you the base config it built and expects you to return a modified copy. If you return a fresh object instead of the result of mergeConfig, you will drop Strapi's own Vite setup and the admin panel will fail to build.
A few rules for the values:
- A string array is the recommended shape.
['cms.example.com']allows exactly that host. - A leading dot allows all subdomains:
.example.commatchescms.example.comandpreview.example.com. 'all'disables host checking entirely. Vite also acceptstruefor this, which is what most forum answers reach for.
About allowedHosts: true
It works, and it is the answer you will find copy-pasted everywhere. Understand what you are turning off: any website your browser visits can now make requests to your dev server, and your dev server has an authenticated admin session.
On a laptop on your own network, that risk is small. On a dev instance reachable from the public internet, it is not, and neither is running strapi develop there at all, which brings us back to the real question.
If you hit this on a deployed environment
Running strapi develop on a server that has a domain name is the underlying problem, not the host check. Development mode:
- runs the Vite dev server and rebuilds the admin panel on every file change,
- enables the Content-Type Builder, which writes
schema.jsonfiles to disk that your deployment will overwrite on the next release, - is significantly slower and more memory-hungry than a production build.
If you need a Strapi instance that is always reachable at https://dev.example.com, deploy it the same way you deploy production:
NODE_ENV=production npm run build
NODE_ENV=production npm run startContent-type changes then happen locally and travel through source control, which is how Strapi is designed to work.
Tunnels and rewritten ports
If you reach the admin panel through a tunnel that terminates TLS and rewrites ports (ngrok, Cloudflare Tunnel, a VS Code port forward), allowing the host may fix the initial load but leave hot reload broken. The HMR websocket still tries to connect to the internal port. The Strapi docs suggest pairing allowedHosts with Vite's hmr.clientPort:
import { mergeConfig, type UserConfig } from 'vite';
export default (config: UserConfig) => {
return mergeConfig(config, {
server: {
allowedHosts: ['.ngrok-free.app'],
hmr: {
clientPort: 443,
},
},
});
};This one is contested, so test it before you commit to it. Recent Strapi versions deliberately bind HMR to Strapi's own
http.Serverso the websocket reuses the app's port, and the code comments say that adding a separate listener plusclientPortis what breaks setups behind a proxy that only exposes the Strapi port. The Strapi source assertsclientPortis undefined; the docs page still recommends setting it. If HMR works withoutclientPorton your version, leave it out.
Quick checklist
- File goes in
./src/admin/vite.config.ts, not the project root. - Always return
mergeConfig(config, { ... }). - Prefer an explicit host list over
true. - Restart
strapi develop. This file is not hot-reloaded. - If the host is a real deployed domain, build and start in production mode instead.




