# Deploy a Site
Publish static files, a compiled frontend, or a production Node server from a repository or ZIP.
Source: https://webhost.photonspark.com/docs/sites
## Choose the build mode
A Site builds from the repository root. It does not read platform.app.yaml or build a backend Dockerfile. Choose an App when you need a separately deployed backend and managed PostgreSQL.
Keep server rendering, authentication, and API routes on node-server when they need a running server. Static output alone cannot run those features.
- static: Ready-to-serve HTML, CSS, JavaScript and images. No install or build command runs. Publish the first directory containing index.html: repository root, public, dist, build, out.
- node-static: React/Vite and other frontends that compile to static files. No server stays running. Install dependencies → npm run build → publish the first matching output: dist, build, out, .output/public.
- node-server: A production Node process for server rendering or routes such as a contact API. Install dependencies → npm run build → npm prune --omit=dev → deploy → npm start.
## Repository structure
With a committed npm lockfile the builder uses npm ci; otherwise it uses npm install. Both Node build modes require a build script. A static Site needs ready-to-publish files including index.html.
For Vite/React, keep your normal build command and choose node-static. The generated dist directory is published; vite dev and vite preview are not production startup requirements.
```text
my-site/
package.json # build; also start for node-server
package-lock.json # commit the lockfile
src/ # your application source
public/ # assets or starter input
.gitignore # exclude node_modules and private .env files
```
## Copy or download a starter
Choose a mode below to view its files, download the complete ZIP, or copy an AI prompt. These minimal Node starters use no external packages: their build copies public files into dist. Adapt them to an existing project rather than replacing its features.
## Production runtime contract
- node-server is installed and built before startup, then development dependencies are pruned. Packages needed at runtime belong in dependencies.
- npm start, prestart, and poststart must only start the production server. Do not install packages, rebuild, or launch a development server at startup.
- Bind to 0.0.0.0 and honor process.env.PORT; the default Site port is 3000. Webhost provides HOST, HOSTNAME, PORT, and NODE_ENV.
- The release under /app is read-only. /tmp is temporary and does not survive replacement. Use a persistent external service for durable data.
- A Site does not automatically receive an App database.
```sh
npm ci
npm run build
npm prune --omit=dev
npm start
# In another terminal, test the application and its real routes:
curl --fail http://127.0.0.1:3000/
```
## Connect and deploy
For manual static publishing, choose the upload method and supply a folder or ZIP with the intended publish-root files. A root index.html is the simplest layout. Check the uploaded release before switching traffic.
1. Create a Site, choose Git Auto Deploy, and enter the repository, branch, and matching build mode.
2. For private SSH repositories, install the displayed public deploy key with read-only repository access and verify the server key. HTTPS token credentials are entered in the dashboard.
3. Set build-time public values and runtime secrets in Environment. Commit the source and lockfile, then deploy the latest commit.
4. Check the build log, runtime state, HTTPS hostname, and all required application routes. Later commits can be picked up by Git polling.
- [Environment variables](https://webhost.photonspark.com/docs/environment)
- [Troubleshooting](https://webhost.photonspark.com/docs/troubleshooting)
## Reference starter files
## .gitignore
```
node_modules/
dist/
.env
.env.*
!.env.example
*.log
```
## index.html
```
My site
Hello from my site
Published with PhotonSpark.
```
## 404.html
```
Page not found
Page not found
Return home
```
## README.md
```
# PhotonSpark Site starter
Choose **Static files** (static) in Site → Git Auto Deploy.
Publish the first directory containing index.html: repository root, public, dist, build, out.
Commit the files in this folder at the repository root. Node examples have no
external dependencies; their build copies public/ to dist/. Adapt the build script
to your framework and update package-lock.json when adding dependencies.
No npm commands are needed.
Static hosting cannot execute a contact handler or other server API.
Connect the repository and branch in the dashboard. For private SSH repositories,
install the displayed public deploy key with read-only access, then deploy.
Keep credentials in Environment, never in Git or browser bundles.
Guide: https://webhost.photonspark.com/dashboard/site-deployment
```
## .gitignore
```
node_modules/
dist/
.env
.env.*
!.env.example
*.log
```
## package.json
```
{
"name": "photon-site-static",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node scripts/build.mjs"
},
"engines": {
"node": ">=22"
}
}
```
## package-lock.json
```
{
"name": "photon-site-static",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "photon-site-static",
"version": "1.0.0",
"engines": {
"node": ">=22"
}
}
}
}
```
## public/index.html
```
My site
Hello from my site
Published with PhotonSpark.
```
## public/404.html
```
Page not found
Page not found
Return home
```
## scripts/build.mjs
```
import { cp, mkdir, rm } from "node:fs/promises";
const output = new URL("../dist/", import.meta.url);
await rm(output, { recursive: true, force: true });
await mkdir(output, { recursive: true });
await cp(new URL("../public/", import.meta.url), output, { recursive: true });
```
## README.md
```
# PhotonSpark Site starter
Choose **Node build (npm run build)** (node-static) in Site → Git Auto Deploy.
Install dependencies → npm run build → publish the first matching output: dist, build, out, .output/public.
Commit the files in this folder at the repository root. Node examples have no
external dependencies; their build copies public/ to dist/. Adapt the build script
to your framework and update package-lock.json when adding dependencies.
Test with npm ci and npm run build. Commit package-lock.json; do not commit node_modules/.
Static hosting cannot execute a contact handler or other server API.
Connect the repository and branch in the dashboard. For private SSH repositories,
install the displayed public deploy key with read-only access, then deploy.
Keep credentials in Environment, never in Git or browser bundles.
Guide: https://webhost.photonspark.com/dashboard/site-deployment
```
## .gitignore
```
node_modules/
dist/
.env
.env.*
!.env.example
*.log
```
## package.json
```
{
"name": "photon-site-server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node scripts/build.mjs",
"start": "node server/index.mjs"
},
"engines": {
"node": ">=22"
}
}
```
## package-lock.json
```
{
"name": "photon-site-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "photon-site-server",
"version": "1.0.0",
"engines": {
"node": ">=22"
}
}
}
}
```
## public/index.html
```
My site
Hello from my site
Published with PhotonSpark.
```
## public/404.html
```
Page not found
Page not found
Return home
```
## scripts/build.mjs
```
import { cp, mkdir, rm } from "node:fs/promises";
const output = new URL("../dist/", import.meta.url);
await rm(output, { recursive: true, force: true });
await mkdir(output, { recursive: true });
await cp(new URL("../public/", import.meta.url), output, { recursive: true });
```
## server/index.mjs
```
import http from "node:http";
import { readFile } from "node:fs/promises";
// Read the completed build. Startup never installs packages or rebuilds.
const index = await readFile(new URL("../dist/index.html", import.meta.url));
const notFound = await readFile(new URL("../dist/404.html", import.meta.url));
const port = Number(process.env.PORT || 3000);
const host = process.env.HOST || "0.0.0.0";
const server = http.createServer((req, res) => {
let path;
try { path = new URL(req.url || "/", "http://localhost").pathname; }
catch { res.writeHead(400); res.end(); return; }
if (path === "/healthz") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
const found = path === "/" || path === "/index.html";
res.writeHead(found ? 200 : 404, { "content-type": "text/html; charset=utf-8" });
res.end(found ? index : notFound);
});
server.listen(port, host);
process.on("SIGTERM", () => server.close());
process.on("SIGINT", () => server.close());
```
## README.md
```
# PhotonSpark Site starter
Choose **Node server (npm start)** (node-server) in Site → Git Auto Deploy.
Install dependencies → npm run build → npm prune --omit=dev → deploy → npm start.
Commit the files in this folder at the repository root. Node examples have no
external dependencies; their build copies public/ to dist/. Adapt the build script
to your framework and update package-lock.json when adding dependencies.
Test with npm ci and npm run build. Commit package-lock.json; do not commit node_modules/.
Then run npm prune --omit=dev and npm start. Start only the built production server.
/app is read-only; use /tmp only for temporary files. Keep runtime packages in dependencies.
Listen on 0.0.0.0 and process.env.PORT (3000 by default). Preserve your existing API routes.
Connect the repository and branch in the dashboard. For private SSH repositories,
install the displayed public deploy key with read-only access, then deploy.
Keep credentials in Environment, never in Git or browser bundles.
Guide: https://webhost.photonspark.com/dashboard/site-deployment
```