Because an aurora app is stateless, deployment is the boring, scalable kind: a container that serves static assets plus JSON routes. Run as many replicas as you like behind a load balancer — there are no sticky sessions to worry about.
Build an image
The static UI is compiled at build time and shipped as
www/index.html; the container serves it
and does not rebuild it, so the runtime image installs no UI
dependencies (bslib, and transitively shiny). Build the UI before the
image:
library(aurora)
aurora_build_ui("meu_app") # compile www/index.html (needs bslib; run on dev/CI)
# Generate a Dockerfile (+ .dockerignore). It installs only the runtime deps
# your routers/helpers need, plus plumber2 and aurora.
aurora_dockerfile("meu_app")
# Build (and optionally push) the image using the docker CLI.
aurora_build_image("meu_app", tag = "org/meu_app:latest", push = TRUE)The generated Dockerfile pulls R packages as prebuilt binaries from Posit Package Manager, so builds are fast and need no compiler toolchain at run time.
aurora_build_image() targets
linux/amd64 by default (it passes
--platform linux/amd64 to docker build).
Production servers are almost always x86-64, and an image built natively
on an Apple Silicon machine is arm64 — it fails there with
exec format error. The default also matches Posit Package
Manager, which only serves amd64 Linux binaries. Building the amd64
image on an arm64 Mac works via emulation (enable Rosetta in Docker
Desktop); it is slower, but the image runs everywhere you deploy. To
build for the host architecture instead — e.g. deploying to an arm64
server — pass platform = NULL, or any explicit target like
platform = "linux/arm64".
aurora_dockerfile() writes a Dockerfile whose entry
point is Rscript api.R, so the container and local
development share one assembly path. Key arguments:
-
flavor—"debian"(default) or"alpine"(see below). -
base— base image;NULLresolves per flavor (rocker/r-ver/rhub/r-minimal). -
sysdeps—"auto"uses a curated default set covering the plumber2 + bslib baseline plus common TLS/curl/db/geo/graphics needs; pass a vector to override. -
port— exposed port (default8000).
Choosing a flavor
debian (default) |
alpine |
|
|---|---|---|
| Base | rocker/r-ver |
rhub/r-minimal |
| R packages | binaries from Posit Package Manager (fast) |
compiled from source via installr
(slower) |
| Image size | larger | tiny (~25 MB base) |
| Arch | amd64 binaries (arm64 compiles) | builds natively on amd64 and arm64 |
| Best for | heavy/geo apps, fast CI, broad compatibility | size-sensitive / edge deploys, simple deps |
aurora_dockerfile("meu_app", flavor = "alpine")The alpine flavor compiles everything (no CRAN binaries
on Alpine) and uses
installr -d -t "<build deps>" -a "<runtime libs>".
aurora ships defaults that cover the plumber2 + bslib baseline; for
extra system libraries (e.g. GDAL/GEOS for sf) pass them
via sysdeps. Note that aurora’s baseline still pulls a
non-trivial tree (httpuv, the fiery stack, roxygen2, the graphics
packages), so even a small app compiles a fair amount on Alpine — the
win is final image size.
Publishing to a registry
aurora_build_image(push = TRUE) publishes after a
successful build. The tag chooses the registry — that
is how Docker addresses images, so no extra argument is needed:
# Docker Hub (the default registry)
aurora_build_image("meu_app", tag = "myorg/meu_app:latest", push = TRUE)
# GitHub Container Registry
aurora_build_image("meu_app", tag = "ghcr.io/myorg/meu_app:latest", push = TRUE)Authenticate once with the docker CLI before pushing — aurora deliberately does not wrap registry login (credential helpers and tokens belong to the docker config, not to an R session):
For a private app, create the repository as private on the registry (on Docker Hub, free accounts default new repositories to public).
Runtime configuration (environment variables)
The generated api.R reads its bind address and port from
the environment, and aurora features are env-toggleable, so the
same image runs in dev and prod:
| Variable | Used for |
|---|---|
AURORA_HOST / AURORA_PORT
|
bind address / port (api.R) |
AURORA_OTEL |
enable OpenTelemetry logging
(vignette("telemetry")) |
AURORA_JWT_SECRET |
signing secret for the auth template |
AURORA_ENV=prod |
Secure; SameSite=Strict auth cookies (behind
HTTPS) |
Never bake secrets into the image — inject them at run time:
Sharing assets across apps (statics:)
When several apps on a server share the same static files (a logo,
common JS libraries, a stylesheet), keep one copy in a server-side
directory, mount it as a read-only volume, and declare it in
_aurora.yml under statics: – a map of URL
prefix to directory:
aurora_app() serves that directory at the prefix (in
addition to www/ at /), so the app references
the files by URL:
docker run -p 8000:8000 \
-v ./data:/app/data:ro \
-v /srv/aurora-shared:/srv/aurora-shared:ro \
org/meu_app:latestUpdate the shared directory once and every app picks it up. Relative
paths resolve against the app root; a missing directory (e.g. a volume
that was not mounted) is skipped with a warning so the app still starts.
The root path / is reserved for the app’s own
www/ – mount shared files under a sub-path, or simply drop
them in a sub-folder of www/ (also served, no config
needed) if they don’t need to be shared across apps.
Behind a reverse proxy / load balancer
Serve the app under a path prefix or a subdomain via your proxy
(nginx, Traefik, an ingress). The runtime resolves API paths against the
page’s base path, so an app served under /meu_app/ still
calls its routes correctly. Run multiple replicas freely — state lives
in the client (cookies) or an external store, not in the R process (see
vignette("aurora") on
aurora_data_store()).
ShinyProxy
ShinyProxy launches the container like any Docker-backed app.
aurora_shinyproxy_yaml() emits the proxy.specs
entry for you:
aurora_shinyproxy_yaml(
image = "org/meu_app:latest",
dir = "meu_app", # defaults id / display-name from the app name
env = list(AURORA_ENV = "prod")
)
#> - id: meu_app
#> display-name: meu_app
#> container-image: org/meu_app:latest
#> port: 8000
#> container-env:
#> AURORA_ENV: prodPaste that under proxy.specs in your ShinyProxy config,
or pass wrap = TRUE for a complete
proxy: specs: snippet (and write = TRUE to
save it to a file).
Ruscker
Ruscker is
a reverse proxy and container orchestrator (a lightweight ShinyProxy
alternative) that reads the same application.yml schema and
adds fields for stateless APIs and replica pools. Because an aurora app
is a stateless ‘plumber2’ API,
aurora_ruscker_yaml() emits a type: api spec:
Ruscker load-balances a replica pool of the container rather than
running one container per session.
aurora_ruscker_yaml(
image = "org/meu_app:latest",
dir = "meu_app", # defaults id / display-name from the app name
rate_limit = "100/min", # optional proxy-side throttle
cors = TRUE, # optional permissive CORS headers
env = list(AURORA_ENV = "prod")
)
#> - id: meu_app
#> display-name: meu_app
#> container-image: org/meu_app:latest
#> type: api
#> api:
#> port: 8000
#> docs-path: /__docs__
#> health-path: /__healthz__
#> rate-limit: 100/min
#> cors: yes
#> min-replicas: 0
#> max-replicas: 3
#> container-env:
#> AURORA_ENV: prodmin_replicas defaults to 0 (spawn on
demand); raise it to keep instances warm, and set
max_replicas for the auto-scale ceiling. As with
ShinyProxy, wrap = TRUE emits a full
proxy: specs: snippet and write = TRUE saves
it.