Browse docs All docs
Docs / Working on a static site
Guidedeploymill://guides/stack/static

Working on a static site

Reference for an agent modifying a DeployMill-managed static site scaffolded from the static template. Read it before adding assets or editing the Dockerfile. This is the simplest of the three stacks: nginx serves HTML files baked into the image, with no app server, no env at runtime, and no persistence.

What DeployMill scaffolds

  • index.html: single-file landing page using the {{PROJECT_NAME}} placeholder.
  • Dockerfile: FROM nginx:alpine, copies the whole repo root (COPY . /usr/share/nginx/html/) into nginx's default doc root, and 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. The 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, the file would 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, and 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/.

Build-time vs runtime env vars in a bundled frontend

Bundled frontends (Vite, Create React App) inline VITE_* / REACT_APP_* env vars into the JS bundle at build time (docker build). The value is frozen into the shipped JavaScript. Setting those same vars as runtime env vars on the app does nothing: the browser never reads process.env, it reads what the build baked in. So set_env_vars MY_VITE_KEY=... won't change a deployed SPA.

To configure a containerized SPA at runtime, don't try to inject the var into the bundle. Instead, serve a small /runtime-config endpoint from a Node server that reads process.env at request time (returning a JSON blob of the keys the SPA needs), and have the SPA fetch('/runtime-config') on boot before reading config. That keeps the value out of the build and lets one image run with different config per deploy.

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: { engine: "postgres", provider: "deploymill" } 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 filefirst 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.