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, andhub historymarks 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 setrequire-clean: true.nothing changed since the last deploy …; not rebuilding: see Redeploys.waiting for a build slot: other builds are running (the hub'smax_builds). The deploy continues by itself.system packages: …: the apt packages the build installs for the lock file andsystem-packages.N variable(s) from .Renviron stored as the app's secret, followed bykept the values set with 'hub env set' for: …for the keys thathub env setoverrides.pushed … (sha256:…): the build succeeded.hub historylists 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 NAMEshows 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:
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.