Deploy a React SPA to CloudBase Hosting
In one sentence: Build your project with Vite + React to produce a
distfolder, then runtcb hosting deploy dist -e <env-id>to upload it to CloudBase static website Hosting in one shot — you get CDN acceleration, HTTPS, and custom domain support automatically; when using React Router withBrowserRouter, set the error page toindex.htmlin the Console to resolve refresh 404s.Estimated time: 15 minutes | Difficulty: Beginner
Applicable Scenarios
This recipe covers the most common React frontend deployment scenario: a pure SPA with no SSR and no server-side logic, where the build output is a set of static files. CloudBase Hosting is backed by COS + CDN and billed by storage and traffic — no container instance fees.
- Applicable: React 18+ + Vite or Create React App SPA
- Applicable: China-hosted deployment that requires ICP filing + HTTPS
- Applicable: cost-sensitive projects that do not need a server-side process
- Not applicable: SSR / SSG / Next.js server-side rendering — use CloudBase Run instead, see deploy-nextjs-to-cloudbase-run
- Not applicable: projects that need a BFF or backend API — use CloudBase Run or Cloud Functions
Hosting vs. CloudBase Run
One-line distinction: Hosting = static files + CDN distribution; CloudBase Run = containerized server-side process.
| Dimension | Hosting | CloudBase Run |
|---|---|---|
| Deployment artifact | Static files (HTML/CSS/JS/images) | Container image (Dockerfile) |
| Runtime | None (CDN serves directly from COS) | Node.js / Python / custom runtime process |
| Billing | Storage + traffic | Instance uptime + traffic |
| Best for | SPA / docs site / landing page | SSR / API / WebSocket / long connections |
A React SPA produces exactly two kinds of artifacts — one HTML file + one JS bundle + other static assets — all of which are cheapest to serve via Hosting.
Prerequisites
| Dependency | Version |
|---|---|
| Node.js | ≥ 18 (minimum requirement for Vite 5/6) |
| React | 18+ |
| Vite or CRA | Vite 5+ / Create React App 5+ |
react-router-dom | 6+ (if using routing) |
@cloudbase/cli | latest (v3.x at time of writing) |
| A CloudBase environment with Hosting enabled | — |
Install and log in to the CLI globally:
npm i -g @cloudbase/cli
tcb login
Step 1: Configure the React Project
Before deploying a SPA to Hosting, two settings must be confirmed, otherwise you will hit classic problems like refresh 404s and asset 404s in Step 4.
1.1 Vite base Configuration
vite.config.ts (or vite.config.js):
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: '/', // use '/' when deploying to the root of your domain
// base: '/my-app/', // use '/my-app/' if deploying to a sub-path (e.g. default domain /my-app/)
build: {
outDir: 'dist',
},
});
base controls the prefix added to asset references in the built HTML. If you deploy to the root of the default domain (https://<env-id>.tcloudbaseapp.com/), use /. If you deploy to a sub-path with tcb hosting deploy dist /my-app -e <env-id>, change it to /my-app/ — both leading and trailing slashes are required.
For Create React App projects, the equivalent field is homepage in package.json, with the same meaning (CRA build output goes to build/ instead of dist/; the rest of this recipe uses Vite as the example).
1.2 React Router basename
React Router 6+ supports two authoring styles. In both, basename must exactly match the base set in Vite.
Classic style (BrowserRouter):
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
function App() {
return (
<BrowserRouter basename="/">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Modern style (createBrowserRouter, recommended):
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
const router = createBrowserRouter(
[
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
],
{ basename: '/' },
);
function App() {
return <RouterProvider router={router} />;
}
export default App;
Both styles are equivalent, but createBrowserRouter supports the data API (loader / action) and is what React Router recommends for new projects.
A mismatch between basename and Vite's base produces the confusing symptom where the home page loads but navigating to /about results in every asset returning 404 — the same trap as in the Vue Hosting recipe.
If you want to avoid the complexity of history mode entirely, you can use createHashRouter (or HashRouter) instead. URLs will look like /#/about, and no server-side fallback configuration is needed. However, hash-based URLs are not SEO-friendly and are generally not recommended for production.
Step 2: Build dist
npm install
npm run build
After the build completes, a dist/ folder appears in the project root:
dist/
├── index.html
├── assets/
│ ├── index-a1b2c3d4.js
│ ├── index-e5f6g7h8.css
│ └── ...(various chunks and static assets)
└── favicon.ico
Open dist/index.html and verify: all <script> and <link> src/href values should begin with your configured base. If base is /my-app/, the HTML should reference /my-app/assets/index-xxxxx.js, not /assets/index-xxxxx.js. If the paths are wrong, fix vite.config.ts and rebuild.
Step 3: Deploy to CloudBase Hosting
The CloudBase CLI offers two deployment paths depending on your workflow.
Path A: tcb app deploy (recommended — automated build + upload)
tcb app deploy runs the full pipeline — install dependencies → build → upload artifacts → configure routing — in a single command, with the build happening in the cloud:
tcb app deploy --framework react -e <env-id>
The CLI reads name from package.json as the application name and infers the build command (npm run build) and output directory (./dist) from --framework react. On the first run it prompts you to confirm these parameters interactively, then writes them back to cloudbaserc.json. Subsequent deployments only need tcb app deploy.
Common parameters:
| Parameter | Description |
|---|---|
--framework | Framework type: vite / vue / react / next / nuxt / angular / static |
-e, --env-id <envId> | Target environment ID |
--build-command <cmd> | Custom build command, overrides the framework default |
--output-dir <dir> | Build output directory; Vite defaults to ./dist, CRA defaults to ./build |
--deploy-path <path> | Hosting mount path, defaults to /service-name |
Full parameter reference: tcb app deploy command docs.
Path B: tcb hosting deploy (upload an existing dist directly)
If you want to keep the build local (e.g. CI has already run npm run build), you can upload dist/ directly:
# In the project root, after building locally
npm run build
# Deploy the entire dist directory to the cloud root
tcb hosting deploy dist -e <env-id>
# Or deploy to a sub-path
tcb hosting deploy dist /my-app -e <env-id>
tcb hosting deploy <localPath> [cloudPath] is a file-level tool: it does not run install or build, only upload. Full command reference:
| Command | Purpose |
|---|---|
tcb hosting deploy <localPath> [cloudPath] -e <env-id> | Upload a file or folder to the specified path |
tcb hosting list -e <env-id> | List files on the cloud |
tcb hosting detail -e <env-id> | View service info (default domain / status) |
tcb hosting delete <cloudPath> -e <env-id> | Delete a specific file; add --dir to delete a folder, --force to skip confirmation, --dry-run to preview |
tcb hosting delete -e <env-id> | Clear all files in Hosting |
Upload Limits
- Maximum file size: 50 TB; no limit on the number of files.
- If you see
socket hang up/ECONNRESETduring upload, the network middleware is likely terminating the SDK's keep-alive connection. Disable it and retry:export COS_SDK_KEEPALIVE=falsetcb hosting deploy dist -e <env-id>
Full command documentation: CLI Hosting Commands.
Step 4: Configure the SPA Fallback
This step is required for any React Router BrowserRouter / createBrowserRouter deployment on Hosting. Skipping it produces this symptom: the home page loads, but any sub-route (/about, /users/123) returns 404 on direct access or refresh.
Why: history mode uses pushState to change the URL, but physically there is only one file on the cloud — index.html. When the CDN receives a request for /about, it cannot find /about.html and returns 404 by default.
Fix: configure the 4xx error page to serve index.html, which hands routing back to the frontend.
Steps:
- Go to CloudBase Console → Static Website Hosting
- Switch to the "Settings" tab
- Find the "Error Page" configuration
- Set the 4xx error page to
index.html - Save
After this change, accessing <default-domain>/about returns index.html with a 200 status code (note: 200, not a 404 with a modified response body). React Router then parses the URL and renders the correct route.
See the "Redirect Rules → Error Code Redirect" section in the Hosting Console Management documentation.
Step 5: Custom Domain + HTTPS
The default domain <env-id>.xxx.tcloudbaseapp.com works, but it has access rate limits. Production deployments should always use a custom domain.
Prerequisites
- ICP filing: Domains used for China-hosted access must complete ICP filing on Tencent Cloud first (individual filing takes roughly 7–20 days; enterprise is similar).
- SSL certificate: Apply for a free DV certificate in the SSL Certificate Console, or upload an existing certificate.
Configuration Steps
- Go to CloudBase Console → HTTP Access Service
- Click "Add Domain" and enter your custom domain (e.g.
app.example.com) - Upload an SSL certificate or select an existing one
- For CDN type, select "CloudBase CDN" (optimized for Hosting, configures CDN acceleration automatically)
- After adding, the Console provides a CNAME value — copy it
- Go to your DNS provider (Tencent Cloud DNS / Alibaba Cloud / Cloudflare, etc.) and add a CNAME record pointing to that value
- Wait 3–5 minutes for DNS to propagate globally; the Console status changes to "Active" once complete
The full process including a comparison of the three CDN types is covered in Custom Domains.
Cache Configuration (Optional)
Under Console "Settings → Cache Configuration", it is recommended to configure cache TTLs by resource type:
| Resource Type | Recommended TTL |
|---|---|
| Images, fonts | 30 days |
| CSS / JS (with hash suffix) | 7 days |
index.html | 1 hour |
index.html is intentionally set short — otherwise, after a new deployment, users whose browsers have cached the old HTML will still reference old JS bundle filenames and not see the update.
Verification
# 1. Home page via default domain (should return 200 + HTML)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/
# 2. Sub-route direct access (with fallback configured, should return 200, not 404)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/about
# 3. Static asset (confirms base is configured correctly)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/assets/index-xxxxx.js
Also verify in the browser:
- Open the default domain home page and navigate through all routes (
/about→/users/123) — everything should work. - On the
/aboutpage, press F5 to refresh directly — this must not return 404. This is the key test for confirming the fallback is configured correctly. - Once the custom domain DNS is active, repeat the tests with
https://app.example.com.
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
tcb hosting deploy ./dist -e xxx reports ENOENT: no such file or directory | The dist path is wrong, or the current directory is not the project root; CRA build output is in build/, not dist/ | cd to the project root and confirm ls dist (or ls build for CRA) shows index.html before deploying |
Home page loads, but refreshing /about returns 404 | SPA fallback not configured (Step 4) | In Console "Settings → Error Page", set 4xx to index.html |
| Home page opens but all CSS/JS return 404; Network tab shows wrong asset paths | vite.config.ts base does not match the actual cloudPath used during deployment | Use / for root deployment, /my-app/ for sub-path /my-app; rebuild and redeploy after changing |
| Sub-route refresh returns 200 but page is blank; Network shows JS 404 | BrowserRouter basename / createBrowserRouter basename does not match vite.config.ts base | Set both to exactly the same string (/ or /my-app/); rebuild and redeploy |
| CRA project assets return 404 after deployment | CRA homepage field not set; build output contains wrong asset paths | Add "homepage": "." or "homepage": "/my-app" to package.json, then re-run npm run build |
Custom domain shows ERR_CERT_COMMON_NAME_INVALID | The SSL certificate is bound to a different domain, or the certificate has expired | Re-issue a matching certificate in the SSL Certificate Console and re-bind it |
| Custom domain DNS still resolves to old IP | DNS has not switched to CloudBase's CNAME, or local DNS cache is stale | Run nslookup app.example.com to confirm the CNAME points to Tencent Cloud's CDN domain; flush local cache on macOS with dscacheutil -flushcache |
| Custom domain returns 403 | Domain has not completed ICP filing, or the filing has expired | Check filing status at the ICP filing system; file if not yet registered |
socket hang up / ECONNRESET during bulk upload | COS SDK keep-alive connection terminated by network middleware | Run export COS_SDK_KEEPALIVE=false and retry |
Deployment error codes are listed in full at Error Code Reference.
Related Documentation
- Vue SPA + Hosting — The Vue counterpart using the same template
- Hosting Quick Start — Console and CLI upload paths
- Hosting Console Management — Error page, cache, hotlink protection, and other settings
- CLI Hosting Commands — Full
tcb hostingcommand reference - CLI App Deploy Commands — Full
tcb app deployparameters andcloudbaserc.jsonconfiguration - Custom Domains — Three CDN types + full domain ownership verification flow
- Deploy Next.js to CloudBase Run — The alternative path when SSR is required
Next Steps
Once your React SPA is live, the natural next step is connecting it to a backend. Put your LLM proxy in a Cloud Function to avoid exposing API keys in the browser — see connect-openai-api-cloud-function; add CloudBase authentication to your React site with add-auth-web-with-cloudbase-sdk, reusing the same environment; for a multi-environment (dev / staging / prod) secrets layering strategy that works with tcb hosting deploy in CI, see secure-secrets-in-cloud-function.