Docs / Working on a static site
Guidedeploymill://guides/stack/static

Working on a static site (deploymill)

Reference for agents modifying a deploymill-managed static site scaffolded from the static template. The scaffold is the simplest of the three stacks: nginx serves HTML files baked into the image — no app server, no env at runtime, no persistence.

What deploymill scaffolds

  • index.html — single-file landing page using the {{PROJECT_NAME}} placeholder.
  • DockerfileFROM nginx:alpine, copies the whole repo root (COPY . /usr/share/nginx/html/) into nginx's default doc root, exposes port 80.
  • .dockerignore — keeps build/meta files (Dockerfile, .deploymill/, .git/, node_modules, dist) out of the image.

The platform contract

  1. Port 80. nginx defaults to 80. The platform routes the published port — don't change it without also updating EXPOSE and the nginx config.
  2. Files baked into the image. Static-stack has no runtime configuration. Anything you want served must be COPY'd in at build time.
  3. Assets live at the repo ROOT — not in public/ or src/. The scaffold's Dockerfile does COPY . /usr/share/nginx/html/, so a file at the repo root is served at /<file>. If you put app.js in public/, it's served at /public/app.js (so <script src="app.js"> 404s), and if the Dockerfile were a narrower pattern it'd be skipped entirely. Do not create a public/ directory — put index.html, CSS, JS, and images directly at the repo root.

Common modifications

  • Add CSS/JS/images: drop them at the repo root and reference them with root-relative or relative paths (<link href="style.css">, <script src="app.js">). The scaffold already copies the whole root, so no Dockerfile edit is needed — just commit + push. Don't put them in public/ (see the platform contract above); that's the most common reason an asset 404s.
  • Subdirectory routing (/about/index.html, etc.): nginx's default config already serves directories. Add the subdir to the repo root (e.g. about/index.html) and push — it's served at /about/. This is fine; the pitfall is a single top-level public//src/ wrapper that shifts everything under one path.
  • Custom nginx config (SPA fallback, redirects, headers): drop an nginx.conf in the repo, add COPY nginx.conf /etc/nginx/conf.d/default.conf to the Dockerfile. The default nginx:alpine config lives at /etc/nginx/conf.d/default.conf.
  • SPA history routing: in a custom nginx config use try_files $uri $uri/ /index.html; so client-side routes don't 404.
  • Switch to a build step (Vite, Next static export): add a build stage to the Dockerfile that produces a dist/, then FROM nginx:alpine + COPY --from=build /app/dist /usr/share/nginx/html/.

What this stack does NOT support

  • No env vars at runtime. Static sites can't read env. If you need config that changes per deploy, you need a build step that templates HTML/JS before nginx starts, or switch to the node/python stack.
  • No database. database: { provider: "neon" } does nothing useful here — there's no app process to read DATABASE_URL.
  • No mounts (in practice). Mounts work mechanically, but nothing is writing data. Pages served are whatever's baked into the image.
  • No /healthz endpoint. It would 404. Use the root path (/) for liveness checks.

What NOT to do

  • Don't expect runtime mutation. The image is your deploy unit. To "change a page", commit + push and let the platform rebuild.
  • Don't put assets in a public/ (or src/) subdirectory. The scaffold serves the repo root, so a public/ wrapper either makes every path wrong (/public/...) or, with a narrower COPY pattern, drops the files entirely. Keep everything at the root.
  • Don't remove the scaffold's .dockerignore. Because the Dockerfile does COPY . /usr/share/nginx/html/, dropping the .dockerignore would publish your .deploymill/project.json, .git/, the Dockerfile, and any secrets sitting in the repo. If you add a build step, extend the ignore list rather than deleting it.

Debugging

  • 404 on a known file → first check whether the file is in a public//src/ subdirectory. If it is, it's being served under that path (/public/app.js) instead of where your HTML references it — move it to the repo root. If it's already at the root, confirm the Dockerfile still does COPY . /usr/share/nginx/html/ and that .dockerignore isn't excluding it.
  • Blank page → the file is being served but is empty; verify with get_file that the committed content is what you expect.
  • nginx errors are in the container's stdout, available via the platform's logs view.