Deploy an existing app (bring your own code)
You already wrote an app (in Cursor, on your laptop, anywhere) and now you want it live on DeployMill. This is the playbook for that path: a repo that DeployMill never scaffolded, often with no .deploymill/project.json and no Dockerfile. The tool that adopts it is import_repo. The one thing you almost always have to add by hand is a Dockerfile.
If you're starting from nothing instead, use start_project. It scaffolds a known-good stack (Dockerfile included) and you skip most of this. This guide is specifically for code that already exists.
The shape of the flow
- Get the code onto GitHub. DeployMill deploys from a GitHub repo, so the code has to live in one.
- Call
import_repowith the repo URL. It scaffolds a.deploymill/project.jsonif the repo doesn't have one, wires an app, and runsreconcile_project. - Read the
readinessreport it returns. This is the "what will probably break, and how to fix it" list. For an app DeployMill didn't generate, it's the most important part of the response. - Add a Dockerfile if you don't have one (the usual blocker, see below).
- Fix anything else readiness flagged (env vars, database, persistence), then
deploy.
Step 1: get the code onto GitHub
Two ways, depending on whether you're driving by hand or through the agent:
- You push it yourself. Create a GitHub repo, then from your local checkout
git remote add+git push. Standard git, nothing DeployMill-specific. Then hand the repo URL toimport_repo. - The agent commits it. Use
push_filesto commit text files into a repo (it can create the branch and even trigger a deploy in the same call). For a large or binary-heavy tree, callget_clone_credentialsfor a short-lived, repo-scoped token and do a realgit push. Seedeploymill://guides/sourcefor the file tools.
Either way the repo is the source of truth. DeployMill builds what's on the branch, not what's on your disk.
Step 2: import it
import_repo({ repoUrl: "https://github.com/<owner>/<repo>" })
- If the repo already has
.deploymill/project.json, that file wins and is used as-is. - If it doesn't,
import_reposcaffolds one: it infers a defaultwebconfig (name from the repo, port auto-detected fromEXPOSE/package.json/pyproject.toml, prod host derived), commits it to the repo, and returnsscaffolded: truewith ascaffoldedConfigblock. Treat the inferred config as a starting point. Review it and adjustworkload/port/domains/mounts, and adddatabase/storage/secretsblocks as needed, then re-runreconcile_project. (PassscaffoldConfig: falseto fail fast withconfig_not_foundinstead of writing to the repo.)
You don't need a connection id. import_repo picks the source connection from the repo owner, falling back to your default. Full config schema and the scaffold details are in deploymill://guides/project-config.
Step 3: read the readiness report
Every import returns a structured readiness block so you don't discover a broken deploy by reading logs. Each finding has a machine-readable code, a severity, and a concrete fix. The ones you'll hit most when bringing your own code:
no_dockerfile(high): DeployMill builds every app from a Dockerfile. Without one the build fails. This is Step 4.database_url_expected(high): your app wants aDATABASE_URLconnection string but no managed database is declared. Adddatabase: { engine: "postgres", provider: "deploymill" }(the house default, orprovider: "neon"/"supabase") and reconcile, or setDATABASE_URLyourself withset_env_varsif you have an external DB.local_sqlite_ephemeral(high): yourDATABASE_URLis a file path (local SQLite). Container storage is ephemeral, so that file is wiped on every redeploy unless its directory is mounted. Mount the parent dir or switch to managed Postgres.env_vars_expected(medium): env/build vars the Dockerfile declares. DeployMill does not set runtime values for them. Set real values withset_env_vars, and route secrets throughrequest_secret+ asecretsblock (they never live in the image).unpersisted_path/unpersisted_volume(high): the app writes to a directory the config doesn't mount (uploads, cache, a data dir). Add amountsentry, or use object storage for blobs. Seedeploymill://guides/storage.assumed_web_workload(info): scaffolding guessedweb. If it's a background process (queue consumer, scheduler), setworkload: "worker"and drop thedomains/portblock.
Step 4: write a Dockerfile
DeployMill builds the image from the Dockerfile at your repo root and runs it. There are no buildpacks. If there isn't a Dockerfile, you write one (the contract below is small).
For a recognizable Node or Python stack you can let import_repo synthesize a starting Dockerfile instead: re-run it with generateDockerfile: true. When the repo has no Dockerfile, DeployMill infers the start command (from package.json's start/main, or a [tool.deploymill] start / uvicorn entry for Python) and the build flavor (TS vs JS, uv vs pip), then commits a known-good Dockerfile (exec-form CMD for clean signal handling) and returns a generatedDockerfile block. It's off by default and only a starting point. Review the committed file against the contract below before relying on it. If the start command can't be inferred, nothing is generated and the missing-Dockerfile readiness item stands.
The contract the platform cares about is small:
- Listen on
process.env.PORT(don't hard-code a port the platform doesn't know about). EXPOSE <port>in the Dockerfile, matching the listen port. This is whatimport_reporeads to wire the route.- Bind to
0.0.0.0, notlocalhost. A localhost-only bind makes the container unreachable from the platform's router. - Expose a health endpoint (default
/healthz, returns200when healthy) so the deploy health gate and auto-rollback work. Seedeploymill://guides/health.
The fastest way to get a known-good Dockerfile for your stack is to run start_project once with the matching stack and copy its Dockerfile, or read the per-stack conventions in deploymill://guides/stack/node, deploymill://guides/stack/python, and deploymill://guides/stack/static. Sketches:
Node (matches the node stack contract):
FROM node:22-alpine AS build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
# If you compile (TypeScript), build here: RUN pnpm build
FROM node:22-alpine
WORKDIR /app
RUN corepack enable
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/src ./src
ENV PORT=3000
EXPOSE 3000
CMD ["node", "src/index.js"]
Python (matches the python stack contract):
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8000
EXPOSE 8000
# Bind to 0.0.0.0 and read $PORT
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"]
Adjust the entrypoint, build step, and EXPOSE to your app. The rules above are what matter; the rest is ordinary Docker.
Commit the Dockerfile to the repo (push it, or push_files), then deploy.
Step 5: deploy
Once the Dockerfile is in place and readiness is clean:
- If
import_repoalready created the app, rundeploy({ applicationId })(orreconcile_projectfirst if you edited the config), and watch the health-gated rollout. - Read build/runtime logs with
get_logsif the build fails. Seedeploymill://guides/logsanddeploymill://docs/troubleshooting.
Deploy after every code push: don't stack undeployed commits
deploy builds whatever is at the branch HEAD, but the layer cache keys on file content, not on how many commits you stacked up. If you push_files real app code, then push_files a second commit (say a project.json fix) without deploying in between, and only then deploy, you can hit the cache footgun: the build may serve cached layers from an earlier image and ship stale code while still reporting status: "done". So:
- Prefer
push_files({ …, deploy: { applicationId } }). It commits and then deploys in one call, so no commit is ever left undeployed. Use it for each code-bearing push. - Watch the deploy response.
rolledOver: true(or a changedimageDigest) confirms the new code is live.rolledOver: falsewith arolloverNoteflagging a different commit means the new source didn't make it into the image. Re-rundeploy({ applicationId, noCache: true })to force a clean rebuild. rolledOver: falseafter a config-only change is fine. If you only ranreconcile_project(added a mount, changed resources/env) and didn't touch source, the image is legitimately byte-identical. The deploy still re-applies the pod spec (specReapplied: true), so your config change has taken effect, nonoCacheneeded.
What "done" looks like
- The repo has a
.deploymill/project.jsonand aDockerfileat its root. import_repo'sreadiness.findingshas no remaininghigh-severity items.deploysucceeds andget_app'shealthblock reports the app healthy on its domain.
See also
deploymill://guides/project-config: the full.deploymill/project.jsonschema and the edit-then-reconcile workflow.deploymill://guides/source: getting files into the repo (push_files,get_clone_credentials).deploymill://guides/stack/node·…/python·…/static: per-stack Dockerfile conventions.deploymill://guides/health: the health-endpoint contract the deploy gate relies on.deploymill://docs/troubleshooting: when the build or deploy fails.