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
- Port 80. nginx defaults to 80. The platform routes the published port. Don't change it without also updating
EXPOSEand the nginx config. - Files baked into the image. The static stack has no runtime configuration. Anything you want served must be
COPY'd in at build time. - Assets live at the repo ROOT, not in
public/orsrc/. The scaffold's Dockerfile doesCOPY . /usr/share/nginx/html/, so a file at the repo root is served at/<file>. If you putapp.jsinpublic/, 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 apublic/directory. Putindex.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 inpublic/(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-levelpublic//src/wrapper that shifts everything under one path. - Custom nginx config (SPA fallback, redirects, headers): drop an
nginx.confin the repo, and addCOPY nginx.conf /etc/nginx/conf.d/default.confto the Dockerfile. The defaultnginx:alpineconfig 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/, thenFROM 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/pythonstack. - No database.
database: { engine: "postgres", provider: "deploymill" }does nothing useful here. There's no app process to readDATABASE_URL. - No mounts (in practice). Mounts work mechanically, but nothing is writing data. Pages served are whatever's baked into the image.
- No
/healthzendpoint. 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/(orsrc/) subdirectory. The scaffold serves the repo root, so apublic/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 doesCOPY . /usr/share/nginx/html/, dropping the.dockerignorewould publish your.deploymill/project.json,.git/, theDockerfile, and any secrets sitting in the repo. If you add a build step, extend the ignore list rather than deleting it.
Debugging
404on a known file → first check whether the file is in apublic//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 doesCOPY . /usr/share/nginx/html/and that.dockerignoreisn't excluding it.- Blank page → the file is being served but is empty; verify with
get_filethat the committed content is what you expect. - nginx errors are in the container's stdout, available via the platform's logs view.