Browse docs All docs
Docs / Deploy an existing app (bring your own code)
Guidedeploymill://guides/deploy-existing-app

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

  1. Get the code onto GitHub. DeployMill deploys from a GitHub repo, so the code has to live in one.
  2. Call import_repo with the repo URL. It scaffolds a .deploymill/project.json if the repo doesn't have one, wires an app, and runs reconcile_project.
  3. Read the readiness report 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.
  4. Add a Dockerfile if you don't have one (the usual blocker, see below).
  5. 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 to import_repo.
  • The agent commits it. Use push_files to 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, call get_clone_credentials for a short-lived, repo-scoped token and do a real git push. See deploymill://guides/source for 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_repo scaffolds one: it infers a default web config (name from the repo, port auto-detected from EXPOSE / package.json / pyproject.toml, prod host derived), commits it to the repo, and returns scaffolded: true with a scaffoldedConfig block. Treat the inferred config as a starting point. Review it and adjust workload / port / domains / mounts, and add database / storage / secrets blocks as needed, then re-run reconcile_project. (Pass scaffoldConfig: false to fail fast with config_not_found instead 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 a DATABASE_URL connection string but no managed database is declared. Add database: { engine: "postgres", provider: "deploymill" } (the house default, or provider: "neon"/"supabase") and reconcile, or set DATABASE_URL yourself with set_env_vars if you have an external DB.
  • local_sqlite_ephemeral (high): your DATABASE_URL is 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 with set_env_vars, and route secrets through request_secret + a secrets block (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 a mounts entry, or use object storage for blobs. See deploymill://guides/storage.
  • assumed_web_workload (info): scaffolding guessed web. If it's a background process (queue consumer, scheduler), set workload: "worker" and drop the domains/port block.

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:

  1. Listen on process.env.PORT (don't hard-code a port the platform doesn't know about).
  2. EXPOSE <port> in the Dockerfile, matching the listen port. This is what import_repo reads to wire the route.
  3. Bind to 0.0.0.0, not localhost. A localhost-only bind makes the container unreachable from the platform's router.
  4. Expose a health endpoint (default /healthz, returns 200 when healthy) so the deploy health gate and auto-rollback work. See deploymill://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_repo already created the app, run deploy({ applicationId }) (or reconcile_project first if you edited the config), and watch the health-gated rollout.
  • Read build/runtime logs with get_logs if the build fails. See deploymill://guides/logs and deploymill://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 changed imageDigest) confirms the new code is live. rolledOver: false with a rolloverNote flagging a different commit means the new source didn't make it into the image. Re-run deploy({ applicationId, noCache: true }) to force a clean rebuild.
  • rolledOver: false after a config-only change is fine. If you only ran reconcile_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, no noCache needed.

What "done" looks like

  • The repo has a .deploymill/project.json and a Dockerfile at its root.
  • import_repo's readiness.findings has no remaining high-severity items.
  • deploy succeeds and get_app's health block reports the app healthy on its domain.

See also

  • deploymill://guides/project-config: the full .deploymill/project.json schema 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.