Skip to content

Deploying from the IDE

hub deploy, run in a project directory, builds the project inside the hub and publishes it. This page covers what the hub recognises, what it uploads and builds, how to read the output when a build fails, and how several people work on one app.

You need neither Docker on your machine nor any cluster configuration. The hub reads the files, works out what kind of project they are, builds an image with the packages the project records, pushes it to the hub's registry and registers the result. Run the command in a terminal of an IDE session on the hub, because the session is how the hub knows who is deploying. Outside a session the command reports that it cannot reach the deploy service. In R, hub::deploy() and the other functions of the hub package do the same from the console.

What deploys, and how it is recognised

In the directory Kind Runs as
app.R, or ui.R and server.R shiny an app on the apps page
_targets.R targets a pipeline that runs as jobs, now or on a schedule, with nothing on the apps page
_server.yml with engine: mcptools mcp an MCP server at /api/<name>/, registered in your colleagues' coding agents
plumber.R plumber an API at https://apps.hub.<your domain>/api/<name>/
app.py or main.py importing shiny shiny-python an app on the apps page
app.py or main.py importing fastapi, with the application object named app fastapi an API at /api/<name>/
*.qmd with server: shiny, *.Rmd with runtime: shiny Shiny document an app on the apps page
*.qmd, _quarto.yml or *.Rmd without Shiny static document rendered at build time and served as files on the apps page
a .qmd next to pyproject.toml Quarto with the Jupyter engine rendered and served as files

The hub checks the files in the order of the table, and Quarto files before R Markdown files. To override detection, set type: and entry: in hub.yaml. A directory with several documents needs entry:, unless one of them is index.qmd or index.Rmd, or the directory has a _quarto.yml, which the hub renders as a Quarto project.

A directory with its own Dockerfile is built from that file, if your hub allows it. Build from your own Dockerfile explains how.

Kinds describes what each kind does once it runs and how you reach it.

Dependencies

R packages come from renv.lock. Run renv::snapshot() before you deploy, and the build restores exactly those packages on top of the hub's package stack. Python packages come from uv.lock, which uv lock writes. A requirements.txt also works, but it pins nothing.

The hub looks up the system libraries that the R packages in the lock file need and installs them. hub sysreqs shows that list in advance. If the lookup misses a library, add it under system-packages in hub.yaml.

A hub in stack mode (deploy_mode in its configuration) restores no packages. An app may use the packages of the hub's stack and no others, and builds take seconds. The deploy output says which mode the hub is in.

Install R packages inside the project, with renv::install() or install.packages() while the project is active. A package you install in a plain session outside a project goes into your personal library, which renv does not record.

What is uploaded

The hub uploads everything in the directory except .git, .Rproj.user, .Rhistory, renv's library and staging directories, .claude, .posit, .quarto, .venv, _targets and the usual caches. .gitignore does not apply. The upload is compressed and limited to 200 MB, so keep large data files outside the project. hub deploy checks the size before it uploads anything.

A symbolic link to a file or directory inside the project is uploaded as what it points to, so a data link to raw/data reaches the app as a directory. A link to a path outside the project is left out, and the deploy names it in a warning: the build cannot see that path.

With require-clean: true in hub.yaml, the hub uploads the files of the last commit and the .Renviron or .env, and nothing else in the directory.

The hub takes .Renviron (R) or .env (Python) out of the upload and stores it as the app's secret, so the file never enters the image. See Secrets and environment.

The first deploy

$ hub deploy
hub: packing /home/rstudio/sales as 'sales'
hub: building sales as registry.hub.acme.example/apps/sales:latest (shiny; mode: renv, Dockerfile: generated, renv.lock: yes)
hub: system packages: libcurl4-openssl-dev libssl-dev
...
hub: pushed registry.hub.acme.example/apps/sales:latest (sha256:3f9c1a7e2b04)
hub: app registered; the apps server restarts (about a minute)
hub: https://apps.hub.acme.example/app/sales
hub: wrote hub.yaml (app name 'sales'); edit it to change title, groups, env, sizing

The app takes the directory's name in lower case, unless --name or hub.yaml sets another. A new app restarts the apps server, so its URL may take about a minute to open.

The first deploy writes a hub.yaml that sets name and title and lists the other keys of the project's kind, commented out, with their defaults. Edit it next: set the title that the apps page shows and, if not everyone on the hub should see the app, the groups that may.

In a git repository, the deploy records the commit it was built from. It warns when the directory has changed or untracked files, or files that git ignores, because the upload takes those as well and what runs then differs from the commit. With require-clean: true in hub.yaml, the deploy refuses while a committed file has changes or hub.yaml itself is not committed, and uploads the commit's files only, so untracked and ignored files neither block the deploy nor reach the app. A directory outside git deploys as well.

Redeploys

If you run hub deploy again and neither the directory nor the hub's base image has changed, the hub skips the build. The output says nothing changed since the last deploy, and the hub still stores .Renviron or .env as the app's secret. Use --force to build anyway.

If anything in the directory has changed, hub.yaml included, the build runs. It reuses the cached layers of the packages unless the lock file or the system packages changed. The hub pushes the image under the same name and adds an entry to hub history. New sessions of the app start the new image, and open sessions keep the old one.

For an app or document, a change to title, description, access-groups, env, resources, prestart, seats-per-container or uses in hub.yaml restarts the apps server (about a minute). An API or MCP server restarts with every new build, without failed requests. The hub.yaml reference lists what a change to each key costs.

Reading the output

Every line starts with hub:. These are the lines to look for:

  • warning: N changed or untracked file(s) …: the deploy goes ahead, and hub history marks it with *. Commit first if the deploy should match a commit.
  • warning: N git-ignored file(s) or folder(s) … are uploaded as well: the same for files that git ignores, such as rendered output. Delete them, or set require-clean: true.
  • nothing changed since the last deploy …; not rebuilding: see Redeploys.
  • waiting for a build slot: other builds are running (the hub's max_builds). The deploy continues by itself.
  • system packages: …: the apt packages the build installs for the lock file and system-packages.
  • N variable(s) from .Renviron stored as the app's secret, followed by kept the values set with 'hub env set' for: … for the keys that hub env set overrides.
  • pushed … (sha256:…): the build succeeded. hub history lists this digest.
  • app registered, new sessions of the app use this build, app settings changed, pre-started app: the apps server restarts …: what happened to a running app.
  • API service rolling out, MCP server rolling out, pipeline registered: the kinds that are not apps. The line says how to reach the result.
  • connection to the deploy service lost: the deploy service restarted or the network dropped. The build continues and the hub records it when it ends. hub history NAME shows it a minute later. The command exits with status 0, because the deploy has not failed.
  • The last line is the URL. If the output ends in hub: FAILED, the lines above it are the build log. Read it from the bottom up.

A problem with what you uploaded stops the deploy before any build starts. It shows as a single line with an HTTP status, such as hub: 400: hub.yaml: …, without hub: FAILED.

When a build fails

Fix the first real error in the log. Lines starting with #N are build steps.

Message Cause Fix
cannot open shared object file in the load check A package needs a system library at run time. Run hub sysreqs and add the apt package under system-packages.
there is no package called 'X' in the load check X is not in renv.lock. Run renv::install("X") and renv::snapshot(), then deploy again.
this hub builds apps from a fixed package stack after a package that does not load In stack mode, the app uses a package outside the hub's stack. Remove the package, or ask your hub admin to extend the stack.
renv::restore() fails to install a package A compile error, usually from a missing -dev library. Run hub sysreqs, then add the package under system-packages.
ModuleNotFoundError in python -c 'import …' A dependency is missing from uv.lock. Run uv add … and deploy again.
Requirement name … matches project name uv refuses because the directory has the same name as a package. Rename the project in pyproject.toml.
several .qmd files … set entry The directory has more than one document. Set entry: in hub.yaml.
app.py/main.py found but neither shiny nor fastapi is imported Detection could not tell the kind. Set type: in hub.yaml.
hub.yaml: … The manifest is invalid. The message names the key. Fix hub.yaml.
uses: mounts a pipeline's store … needs a ReadWriteMany storage class This hub cannot share store volumes between pods. Remove uses, or ask your hub admin.
502: could not resolve system dependencies The package manager API did not answer. Retry. If it keeps failing, list system-packages by hand.
403: app 'X' belongs to … Another user owns an app of that name. Choose another name in hub.yaml (--name on a first deploy), or ask the owner to add you as a maintainer.
403: hub.yaml changes maintainers … You are a maintainer, and your hub.yaml lists other maintainers than the app has. Write the values from the message into hub.yaml, and ask the owner to make the change.
413: upload larger than 200 MB The directory contains large files, and the compressed upload exceeds 200 MB. Move them out.
exceeded quota on an app that will not start The hub's quota is used up. Ask your hub admin.

If an app builds, starts and then fails, it shows its error in the browser. On a hub with monitoring, admins also see it in Grafana. Reproduce the error in your session first: run shiny::runApp(port = 3838) in one terminal and curl -s http://127.0.0.1:3838/ in another, or run uv run shiny run app.py.

Work on an app as a team

The person who deploys an app first owns it. There are two ways for others to work on the same app.

Maintainers. The owner lists people by user id under maintainers in the app's hub.yaml, or groups under maintainer-groups, and deploys:

maintainers: [alice@example.com, bob@example.com]
maintainer-groups: [sales-analysts]

A maintainer can do with the app what its owner can do. They can deploy it and roll it back, set its secret variables, see its history and the key of an API, run a pipeline and read its logs, and bind the app to a repository or unbind it. Three things remain with the owner and hub admins: deleting the app, changing the two maintainer keys, and handing the app to another owner with hub owner. If a maintainer deploys a hub.yaml whose maintainer keys differ from the app's, the hub refuses the deploy, so that the file and the app do not drift apart. The maintainer then asks the owner to make the change.

The app keeps its owner when a maintainer deploys it. The pipelines under uses: must admit the owner, and a pipeline's runs call APIs as the owner.

A shared repository. Bind the app to a branch with hub git bind, as Deploying from a repository describes. Every push to the branch deploys the app, so the people who may push to the branch decide what runs, and they need no session on the hub to do it. The hub commands on the app, such as hub env set or hub logs, remain with the owner, the maintainers and admins. A push that changes the maintainer keys in hub.yaml fails to deploy, and hub git status says why. The owner applies such a change by running hub git bind again.

Anyone who can deploy an app can read its secrets, because the code they deploy runs with them. This applies to maintainers and to everyone who may push to a bound branch. List only people you would give the app's secret variables to.

New projects

In RStudio, hub::new_app("sales", template = "shiny") or File > New Project > New Directory > Hub project creates a project from a template. hub::new_app() creates the directory sales in the current working directory. You get an renv project with the template's packages, a hub.yaml and a git repository with a first commit. On a hub with AI assistants, the project also has a CLAUDE.md that tells the coding agent how the project is laid out. hub::templates() lists the templates your hub offers. The platform provides these:

Template Makes Entry
shiny Shiny app with bslib and box modules app.R
plumber HTTP API with plumber plumber.R
plumber2 HTTP API with plumber2, Posit's successor to plumber plumber.R
mcp MCP server: R functions as tools for Claude Code and Posit Assistant tools.R
quarto Quarto HTML report with parameters and _brand.yml index.qmd
quarto-dashboard Quarto dashboard, static or with server: shiny index.qmd
rmarkdown R Markdown HTML report index.Rmd
targets targets pipeline with a persistent store _targets.R

Your hub may replace these templates or add its own. They appear in the same list.