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.

Static site

Nothing runs, so there is nothing to declare. Drop the build output — not the project — and it is served as-is.

dist/ ← zip THIS, not the repo 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. Vite and Astro build to dist/; a Next.js static export lands in out/.

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