# Deployments

A repository is connected once you register the webhook address from the console's deployments screen on GitHub and **push for the first time**.
After that, every push is built and deployed.

```
list_apps    connected repositories, production branch, folder to build
update_app   change the production branch, folder to build, framework preset, build/output/install settings, and who the server runs as (run_as)
delete_app   delete one app: its addresses go and its deployments are deleted (the app name in confirm)
```

- A push to the **production branch** changes the project's main address. Other branches go to preview addresses named after the branch.
  A new repository starts with the GitHub repository's default branch.
- For a **monorepo**, put the app's folder (for example `apps/web`) in `root_directory`. The whole repository is `.`.
- The framework is detected at build time. If detection is wrong, choose a preset with `framework`, and override `build_command`, `output_directory` or `install_command` if needed (the same values as the console's build settings). null is the preset default; an empty command skips that step.
  If the repository's `blackwall.json` sets the same item, its value comes first; the build log says where each value came from.
- A **background worker** (a queue consumer, a chat bot, a connection held open to an exchange: a server with no web requests) sets `"worker": true` in `blackwall.json` and its command in package.json's `start`. It need not open a port; it is kept running and restarted if it exits. Web requests to it get a 404.
- **GPU jobs and GPU servers**: on Pro and up, rent a GPU by the second. `create_gpu_job` runs a command in a public image to its end (training, a batch) and lets the GPU go when it ends; results last only if the job uploads them somewhere, such as the project's storage. `get_gpu_job` shows its status and the end of its output, `cancel_gpu_job` stops it. `create_gpu_server` hosts a model, called with the secret key at `https://<ref>.<zone>/gpu/v1/servers/<name>/`: the first request starts it (503 meanwhile) and it stops after its idle minutes. `stop_gpu_server` and `delete_gpu_server` stop or remove it. `create_gpu_volume` makes GPU storage in one data center; give it to a job or server as `volume` and it is mounted at `/workspace`, so a model is downloaded once (billed by the GB-month while it exists; `delete_gpu_volume` deletes it). `region` (auto, asia, us, eu) chooses where to rent; with storage, the GPU is rented in its data center. The closest to Korea is AP-JP-1 (Japan). The image, command, environment variables and stored files are processed in RunPod's data centers. Prices and how many run are in `list_gpu`. From ₩4,500 an hour for an RTX 4090, billed by the second from when the GPU is rented until it is let go, image download included.
- **MySQL databases (WordPress)**: `create_mysql_database` makes a MariaDB 11.4 database and its own login (usually within a minute). With `app` only that app gets it; without, it is the whole project's (while it is the only one, every app gets it). `bind_mysql_database` changes which app gets it. That app's servers start with `MYSQL_URL`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` and, as the official WordPress image reads them, `WORDPRESS_DB_HOST` (host:port), `WORDPRESS_DB_USER`, `WORDPRESS_DB_PASSWORD`, `WORDPRESS_DB_NAME`. Never in a build, and a variable saved under the same name wins. Previews get the same database. The address is reachable only from apps on this server. `get_mysql_connection` shows how to connect; `list_mysql_databases` lists them with the plan's limit (Free 1, Pro 10, Enterprise 50). Backed up every night; `create_mysql_backup`, `list_mysql_backups` and `restore_mysql_backup` back up now and put one back (the state just before is backed up first). `delete_mysql_database` deletes the database, its login and every backup. Deleting and restoring take the database name exactly in `confirm`. REST: `/mysql`, `/mysql/<name>`, `/mysql/<name>/connection`, `/mysql/<name>/backups`, `/mysql/<name>/restore`.
- **Phone calls and websockets**: a server deployment takes websockets. Point a Twilio number's voice webhook at `https://<address>/voice` and the call's audio streams in live (starter: `examples/phone-voice` in the repository). While a connection is open the server is left out of the idle stop and of making room, so a long call is not cut. Keep a main number on the production deployment: waking a stopped server makes the first call a few seconds late.
- **Server memory** is 512 MB (one CPU) by default; choose 1024, 2048 or 4096 MB with `set_server_memory` (or the console's Deployments → Build settings; the current size and a month's cost are in `get_server_memory`). Above 1024 MB a server gets two CPUs. The memory held is metered by the hour as `runtime_gb_hour`. Each plan sets the largest size it allows. Servers at production and branch addresses always run, requests or not (minimum one instance; there is no switch for it), so the month in `get_server_memory` is what one server costs a month.
- **Concurrent builds**: how many of this project's builds run at once, 1 to 20 with `set_concurrent_builds`, or null for no limit (the console's Deployments → Build settings, right below server memory; the current value is in `get_concurrent_builds`). Builds over the limit wait in the queue, and a waiting build is not billed for build minutes.
- **Dockerfile**: a project folder with a Dockerfile and no package.json (Python, Go, any language) is built from that Dockerfile and run as an image. A repository that also has a package.json asks for the image by choosing the `docker` preset or `"framework": "docker"` in blackwall.json. It starts with the image's CMD and listens on the port the image EXPOSEs (the lowest TCP port, or 3000 if none); set another with `port` on `update_app`. Whatever USER the image names, it runs as an unprivileged user (uid 10001) on a read-only file system. It can write to `/tmp` (64 MB), to `/run`, `/var/run/apache2` and `/var/lock/apache2` (16 MB each, empty after a restart), to the paths listed in blackwall.json's `"writablePaths"` (up to 8, 16 MB each), and to its volumes. Keep files that must last on a volume (below). It may listen on a port below 1024, such as 80. An image that must start as root, like the official WordPress image, needs "Run as the image user" turned on in the build settings (`run_as: "image"` on update_app). It then runs as the image's USER, root included, inside a gVisor sandbox, so even as root it cannot reach other projects. On a server without gVisor, turning it on is refused with a message saying so.  Build-time variables reach the build as `--build-arg` and stay in the image's history, so keep secrets in the server's variables. Each build step gets 2 GB and 2 CPUs, a build at most 45 minutes, and an image at most 4 GB.
- **`/data`**: a server that is not a Dockerfile build (Node and the like) can write to `/data` as it is, because SDKs written for Railway and Fly keep their cache and tokens there. It holds up to an eighth of the server's memory (16 MB to 256 MB) and is empty after a restart. To keep what is in it, attach a volume at `/data` (below), which takes its place.
- **Volumes** are disk that stays when a server restarts or a new deployment goes out. Add one to an app's production servers with `create_volume` (WordPress: `/var/www/html`), see them with `list_volumes`, grow one with `resize_volume` (it cannot shrink), and remove one with `delete_volume` (its files and backups are gone and it cannot be undone, so confirm takes its path). `create_volume_backup` packs the files and keeps them 30 days; `restore_volume_backup` puts one back (the files it replaces are backed up for 7 days first). A volume is 1 to 500 GB and its size is a hard cap: past it, writes fail. The plan sets what a project's volumes may hold together (Free 1 GB, Pro 50 GB). Previews, and older deployments production no longer serves, get none. Adding or removing one restarts the servers within a minute. The console has the same under Deployments → Build settings.
- Changed settings apply **from the next push**. Only owners and admins can change them.

## Git connection: importing, private repositories and GitLab

The same things as Deployments → **Git connection** in the console.

```
list_git_access               connected repositories (connected), GitHub accounts, saved tokens (last four characters)
list_importable_repositories  find repositories the GitHub App can see (query searches all, refresh asks again)
import_repository             connect a repository and start its first deployment (with build settings and dotenv)
disconnect_repository         disconnect one repository: pushes stop deploying, history stays
set_checks_on_server          Run checks on One Server: a GitHub repository's Actions jobs on One Server's machines (on/off)
add_repository                connect a repository before the first push (github: owner/name, gitlab: group/name)
set_repo_token                a token to fetch a private repository (gitlab_base for self-hosted GitLab)
remove_repo_token             delete a token
link_github_app               link the GitHub account another project of the organisation already linked
unlink_github_app             disconnect one GitHub account (App installation), or all of them
```

**Connecting** a GitHub account needs a person to confirm it on GitHub, so it can only be done in the console under Deployments → Git connection. Importing after that works through MCP.
If a private repository's build fails at the fetch step, start with `list_git_access`. Only owners and admins can change any of this.

## Import from another server

The same as **Import from another server** under the organisation in the console. Source accounts (Supabase, Vercel, PostgreSQL) are connected once in the console, so a token is never pasted into a chat. Supabase connects with [Sign in with Supabase]: press Authorize, nothing to copy.

```
import_sources                connected source accounts, their projects, and imports so far
start_import                  start an import (one split in the console, or connection+project with one new project per group)
get_import                    each destination's progress by step, what is not moved and why
copy_auth_from_import         copy a Supabase project's sign-in settings (social providers, redirect addresses) into a project here
set_vault_project_keys       write this project's own address and secret key to its vault (they move inside the server)
copy_vault_secrets_from_project  copy another project's vault secrets, values included (values move inside the server)
copy_jobs_from_project        copy another project's scheduled jobs, switched off
copy_auth_from_project        copy another project's sign-in settings into this one (secrets move inside the server)
copy_storage_from_project     copy another project's buckets and files into this project (same size: passed over)
copy_storage_from_import      copy a Supabase project's buckets and files into a project here (same size: passed over)
```

A Supabase source needs its session pooler connection string in start_import's address. It is used for that run only and then deleted. Scheduled jobs arrive switched off. Over REST: `GET /v1/imports/sources`, `POST /v1/imports {org, import, connection, project, address}` and `GET /v1/imports/<id>`.

To move only the sign-in settings into a project that already exists, use `copy_auth_from_import`. Secrets are read from Supabase with the connected account and stored, never put in the answer. See what would move with `dry_run` first, and afterwards register the answer's `callback_url` in each provider's console. Sign-in fails until it is registered. Over REST: `POST /v1/projects/<ref>/config/auth/copy-from-import {connection, project, providers, dry_run}`.

To move only the files, use `copy_storage_from_import`. A bucket missing here is made with Supabase's public flag and limits, and a file already here with the same size is passed over. One call copies up to 200 files within 45 seconds; when files remain, call again with the answer's `next` until `done` is true. The Supabase key is used inside the server only and never put in the answer. Bucket access rules are not copied. Over REST: `POST /v1/projects/<ref>/storage/copy-from-import {connection, project, buckets, rename, dry_run, next, max_files}`.

## Deploying without a push

```
redeploy             rebuild the same commit (when you added environment variables later)
deploy_commit        deploy a specific commit
promote_deployment   a finished preview build, to production
rollback_deployment  production, back to an earlier production deployment (no rebuild; holds production)
create_deploy_hook   an address that, when called, builds the branch's latest commit (CMS publishing, etc.)
list_deploy_hooks    those addresses
remove_deploy_hook   delete an address
```

**If you changed environment variables, you need a redeploy.** Values baked into a build change only when it is rebuilt.
Promoting a preview to production **rebuilds** it with the production environment variables. The preview build is not reused as it is.
Only owners and admins can deploy to production.

## Deploying without a repository

```
deploy_files    send files (path to content), build and deploy them (up to 3MB in total; more: create_source_upload_url → deploy_source)
deploy_source   deploy an archive already uploaded (source)
create_source_upload_url   an address to PUT a large archive to without the token (1 hour)
```

- They go through **the same build** as a pushed commit. With a package.json the framework is detected and built; with only `index.html` it is served as it is.
- With `prebuilt: true` nothing is built and the files are served as sent (an already built `dist/`).
- `target` is `preview` (the default) or `production`. A preview address comes from `branch` (`preview` by default). A project with no app gets a `site` app.
- For a large site use `oneend deploy` (packs the folder and uploads it) or `POST /deployments/sources` (body: .zip, .tar, .tar.gz, up to 50MB), then deploy the `source` it returns.
- With tools alone, get an address with `create_source_upload_url`, PUT the archive to it (`curl -X PUT --data-binary @site.zip '<url>'`), and call `deploy_source` with the `source` in the answer. The address works for that project for 1 hour, with the same size limit and checks as `POST /deployments/sources`. Over REST it is `POST /v1/projects/<ref>/deployments/sources/upload-urls`.
- `node_modules` and `.git` are left out. Paths or links pointing outside the folder, and archives that would be too large unpacked, are refused.

## More build settings, system variables, deployment security

```
get_build_settings        per app: development command, Node.js version, ignored build step, production first, required checks
set_build_dev_settings    development command (oneend dev) and Node.js version (18 | 20 | 22 | 24)
set_ignored_build_step    which pushes are built (automatic | every | production | preview | folder | command)
set_production_first      production builds first in the queue
set_git_checks            required GitHub checks, Git LFS, verified commits only
set_expose_system_env     set or leave out system variables such as BLACKWALL_ENV and BLACKWALL_URL
set_directory_listing     list the files of a folder with no index.html
get_deployment_security   who sees build logs and output, fork protection, OIDC
set_deployment_security   change those (what is left out stays)
```

The framework, the build, install and output commands and the build machine are `update_app`. Only owners and admins can change any of these.

## Deployment retention and restoring

```
get_deployment_retention   how long each kind of deployment is kept (days in force, the project's number, the plan default)
set_deployment_retention   change it (null for the plan default; project admins)
list_deleted_deployments   deployments deleted in the last 30 days, and until when each can be restored
delete_deployment          delete a deployment (restorable for 3 days)
restore_deployment         restore a deleted deployment
```

- Plan defaults: Free keeps every kind 30 days. Pro and Enterprise keep canceled 30 days, failed 90, preview 180 and production 1 year (365). A project's own number wins.
- Kept whatever the period: the 10 newest deployments, the 20 newest ready production deployments, the 20 newest ready deployments that are not production, and anything production, a branch or a domain is serving now (including each branch's latest preview).
- A deployment you delete, or one deleted because its period ended, can be restored with `restore_deployment` for 3 days. Meanwhile its `dpl-…` address answers 410; after 3 days its files and build log are removed and it cannot come back.
- Over REST: `GET·PUT /v1/projects/<ref>/deployment-retention`, `GET /v1/projects/<ref>/deployments/deleted`, `DELETE /v1/projects/<ref>/deployments/<id>`, `POST /v1/projects/<ref>/deployments/<id>/restore`. In the CLI: `oneend deployments retention`, `oneend deployments deleted`, `oneend deployments rm`, `oneend deployments restore`.

## Addresses, waiting and the security check

- The tools that make a deployment (`deploy_files`, `deploy_source`, `deploy_commit`, `redeploy`, `promote_deployment`), and `get_deployment`, `list_deployments` and `rollback_deployment`, answer with its addresses. `url` is that deployment's own `dpl-…` address, `production_url` the production address, `preview_url` its branch's preview address. With preview protection on, `note` says who can open them.
- After `rollback_deployment` the app's production is held (`production_held`, "Production held" in the console): new production builds are built but not served on the production address. `promote_deployment` with a finished production deployment serves it without a rebuild and lifts the hold; with the one serving now it only lifts the hold. `list_deployments` lists held apps in `production_held`.
- With `wait` (seconds, up to 120) they wait for the build to finish (`ready` or `error`) and answer with the result. A failed build comes with `error` and the end of `build_log`; one not finished in time comes with `still_running: true` and `retry_after_seconds`, after which `get_deployment` with `wait` waits on. `wait` on `create_project` waits for the database, and `status` in `list_projects` is `ready`, `provisioning`, `failed` or `paused`.
- `advisors` in the answer is the number of critical security problems `get_advisors` finds (a rule that lets anyone write, a view that skips the rules) and the top 3. By default they are reported and the deploy goes ahead. With `block_on_advisors: true` a production deploy is refused while there are any (previews are not held back); `oneend deploy --block-on-advisors` does the same. Deploys started by a git push are not held back.

---

https://end.blackall.ai/docs/deploy?lang=en
