claudetohost

Contract

What ClaudeToHost accepts

Three shapes of project, and one explicit contract for the two that have to run. Nothing here is a convention you have to guess: if your project does not match, you are told which rule it missed.

A front end

Nothing to declare. Zip the project folder — the one with package.json in it — and the build runs on our side.

my-app/ ← zip THIS package.json ← this is what tells us to build index.html ← a Vite template, not the page src/… node_modules/ ← leave it out, it is reinstalled anyway

We run npm ci (or npm install when there is no lockfile), then npm run build, and serve whatever lands in dist/, out/, build/ or .output/public. It takes about a minute; meanwhile your address already answers, with a page that says so.

The build runs sandboxed, without cluster credentials, and can reach nothing private — npm install executes third-party install scripts, and that is treated as what it is.

Something already built

If you would rather build it yourself, drop the output and we skip straight to serving it.

dist/ ← no package.json, so no build index.html ← must exist at the root assets/…

A single top-level folder is stripped automatically, so zipping dist/ and zipping its contents both work. The rule is simply: a package.json at the root means build, no package.json means serve.

Projects that run: claudetohost.yaml

A service or a worker needs two files at the root of what you drop: a Dockerfile, and this manifest. It declares how to run the thing — and, for env vars, only their NAMES.

version: 1 type: web # web | worker | static runtime: node # node | python | docker command: ["node", "dist/index.js"] # web only — a worker must declare NEITHER of these port: 3000 # 1024-65535 (the container runs non-root) health: path: /healthz env: required: [DATABASE_URL] # NAMES ONLY, never values optional: [LOG_LEVEL] resources: cpu: "500m" memory: "512Mi"

Why a worker has no port

A Discord bot or a queue consumer is not a website. Declaring a port on one is rejected rather than ignored: a worker gets a deployment and nothing else — no service, no ingress, no public surface it never asked for.

Why env is names, not values

A mapping of KEY to value is refused outright. Values are supplied later from the site's page and land in a Kubernetes Secret in your own namespace; they never enter the manifest, the repository, or the deployment record.

Dockerfile requirements

  • A non-root final USER is mandatory. Not a suggestion — a build whose last USER is root, or absent, is refused. It is also why a web port must be ≥ 1024: a non-root process cannot bind lower.
  • Multi-stage where it makes sense: build in one stage, copy the artifact into a slim runtime. A single-stage image is a warning, not a refusal.
  • Logs to stdout/stderr. Anything written to a file inside the container is invisible and lost on restart.
  • No interactive shell as the entrypoint — the image has to start on its own.
  • No secrets in the image or the repo. Files are scanned for key-shaped strings and a match fails the upload with the offending line.
  • ADD <url> is refused: an opaque network fetch at build time is not reproducible.
FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules RUN adduser -S app USER app # ← required EXPOSE 3000 CMD ["node", "dist/index.js"]

Limits

Archive
40 MB, 5000 files
Formats
.zip · .tar.gz
Per site
1 replica · 1 vCPU · 512 MiB
Refused outright
symlinks · paths escaping the archive
← Deploy a site