Docs / Source / file storage reference
Guidedeploymill://guides/source

Source / file storage

This guide explains where a deploymill project's files live, how an agent reads and writes them, and how the storage backend is modeled so it can be swapped without changing any tool.

The model: source is a provider-neutral primitive

deploymill has five provider-neutral primitives: compute, database, domain, secrets, and source. "Source" is where your project's files live and the git history behind them. Like the others, it's an interface with a swappable backend, not a hardcoded vendor.

  • Today (v1) there is exactly one source backend: GitHub. A project is

    a GitHub repo; Dokploy builds from it via the server's configured GitHub App provider. This is the default and it just works — you don't configure anything.

  • The backend is selected by the server's SOURCE_PROVIDER env var (default

    github). A repo can also record its own backend in .deploymill/project.json (see below), so a future second backend — the most likely being a self-hosted Gitea/Forgejo instance with one namespace per tenant, so users get git without needing a GitHub account — slots in behind the same tools.

You don't pick a provider as an agent. You use the file tools and they talk to whatever backend the server runs.

The file tools (work against any backend)

These are provider-neutral by construction — they take a repo name, branch, and path, never anything GitHub-specific:

  • push_files — commit one or more files in a single atomic commit. Auto-deploy

    fires on push. Pass createBranchIfMissing: true to create the branch first.

  • get_file — read one file's decoded contents.
  • list_files — list a directory (repo root by default).
  • create_branch — create a branch (idempotent).
  • start_project / import_repo create or adopt the repo for you.

Repos are created/resolved under the server's configured default owner/namespace; you only supply the repo name.

The source block in .deploymill/project.json

Optional. Records which backend a repo lives on so reconcile_project, import_repo, and previews know where to read from and how to bind the app for builds.

{
  "version": 2,
  "name": "my-app",
  "domains": { "prod": "my-app.example.com" },
  "mounts": [],
  "rollback": false,
  "source": { "provider": "github" }   // optional; omit ⇒ github (the default)
}

Fields:

  • provider"github" (the only wired backend in v1), "gitea", or

    "external". The latter two are reserved: the contract is defined so a repo can declare them, but the backends aren't implemented yet — declaring one today raises a clear "not available on this server" error rather than silently writing to the wrong place.

  • owner, repo, url — optional hints. For GitHub the location is already

    implied by where the file lives, so start_project writes just { provider }. url is the slot a bring-your-own external git remote would use.

Backward compatibility: a config with no source block means GitHub. Older files written before this field keep working unchanged.

How Dokploy builds from source (background)

When an app is created, the source provider translates the repo binding into Dokploy's clone instructions. For GitHub that's the configured GitHub App provider (Dokploy pulls owner/repo@branch). A generic-git backend would instead hand Dokploy a clone URL plus a deploy key. This split lives entirely behind the provider interface — no tool signature encodes it — which is what keeps the source primitive swappable.

Editing files: two flows

There are two ways files get written, and you can use either:

  1. API commits via MCP tools (no checkout). push_files commits straight

    through the backend's API in one atomic commit; get_file / list_files read. This is the primary deploymill loop — auto-deploy fires on push. It needs no clone and works the same across any backend.

  2. A real git clone + push (working tree). When you (or a local coding

    agent) want an actual checkout to build/test against, call get_clone_credentials({ repo, write? }). It returns a short-lived (≤1 hour), repo-scoped authenticated clone URL:

   git clone https://x-access-token:<token>@github.com/<owner>/<repo>.git

Clone, commit (your local git config sets the author), push for the token's lifetime, and re-call the tool for a fresh token when it expires. The token is scoped to the single repo with just contents permission and is revocable — treat the URL as a secret (it embeds the token).

Tenancy: where repos live and namespacing

In the hosted model, repos live under a single deploymill-owned GitHub organization (not anyone's personal account), accessed via a GitHub App rather than a shared token. To keep tenants from colliding on a repo name, repo names are prefixed with the tenant's org slug (<orgSlug>_<name>) when SOURCE_NAMESPACE_REPOS is enabled. The prefixed name is the repo's real name — start_project returns it, and that's the name you pass to push_files, get_file, get_clone_credentials, etc. The delimiter is an underscore (_), which is illegal in a slug, so the prefix is an unambiguous tenant boundary.

get_clone_credentials enforces tenant isolation off this prefix: you can only mint credentials for repos under your own <orgSlug>_ namespace.

Users who already have a GitHub account and want native, persistent access (their own identity on commits, PRs) can be added as a per-repo collaborator — an opt-in that doesn't change the default no-GitHub-account-required flow.

What to do as an agent

  • Just use push_files / get_file / list_files. They work regardless of

    backend.

  • Leave the source block out of project.json unless you have a reason to

    pin a non-default backend. Omitting it = GitHub.

  • If a tool returns a "source provider … is not available" error, the repo's

    config declares a backend the server hasn't enabled — fix the source.provider field or ask the operator which backend is configured.