Deploy a Vite + React app

A Vite single-page app, built to static files and served on a live HTTPS URL.

A Vite app compiles to plain static files in dist/. The most reliable path is a short Dockerfile that builds those files and then serves them on the port Dockhold assigns — it pins exactly how the app builds and starts, so it deploys the same way every time. This recipe is the whole path.

In a hurry? Start from the ready-made vite-react-starter template — click Use this template on GitHub, then deploy it. Or follow the steps below to add this to a project you already have.

1. Add a start command that serves the build

Add the serve static server and a start script. Vite already gives you a build script:

npm install --save-dev serve

Then in package.json:

{
  "scripts": {
    "build": "vite build",
    "start": "serve -s dist -l $PORT"
  }
}

serve -s dist serves the built app with single-page-app fallback (so client-side routes work), and -l $PORT binds the port Dockhold gives you, on all interfaces.

Your app must listen on 0.0.0.0 and read its port from the PORT environment variable — never localhost, never a hardcoded port. Dockhold assigns PORT at runtime; an app that ignores it can't receive traffic.

2. Add a Dockerfile

Put this Dockerfile at the repo root:

# Build the app to static files
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Serve the build on the assigned port
FROM node:20-alpine
WORKDIR /app
RUN npm install -g serve@14
COPY --from=build /app/dist ./dist
# Shell form so $PORT expands at runtime
CMD serve -s dist -l $PORT

A Dockerfile at the repo root takes priority and tells Dockhold exactly how to build and run the app. Without one, the build system may try to serve the project with its own static server, which won't bind the assigned port — so include the Dockerfile.

3. Connect the repo and deploy

  1. Push the project (with the Dockerfile) to a GitHub repository.
  2. Open the dashboard, connect GitHub, and pick the repo.
  3. Dockhold builds from your Dockerfile and runs it.

Your app goes live at https://<your-app>.dockhold.app with HTTPS handled for you. Every later push to your main branch redeploys automatically.

4. Configuration

A static SPA has no server, so it can't read your dashboard variables directly. To configure it from the dashboard — an API URL, for example — use runtime config: have the container write your dashboard values into the page when it starts, and read them from window.__APP_CONFIG__. You set and change those values in the dashboard with no rebuild. The full-stack recipe and the fullstack-web template show the exact setup.

Don't bake config into the build with a committed .env.production, and never put a secret in browser code — anything shipped to the browser is public, readable by every visitor.

Troubleshooting

  • Blank page, and the browser is fetching /src/main.jsx: the un-built source is being served instead of dist/. Make sure the Dockerfile is at the repo root so the app is built before it's served.
  • 404 on refresh of a client route: make sure you used the -s flag (serve -s dist) — it enables the SPA fallback that sends every route to index.html.
  • Config is missing in the browser: a static build can't read dashboard variables — write them into the page at startup and read window.__APP_CONFIG__ (see the full-stack recipe).

Next