--- title: "Deploying aurora apps" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Deploying aurora apps} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(eval = FALSE) ``` 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: ```{r} 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; `NULL` resolves 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 (default `8000`). ### 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 | ```{r} aurora_dockerfile("meu_app", flavor = "alpine") ``` The `alpine` flavor compiles everything (no CRAN binaries on Alpine) and uses `installr -d -t "" -a ""`. 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: ```{r} # 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): ```sh docker login # Docker Hub echo "$GITHUB_PAT" | docker login ghcr.io -u --password-stdin ``` 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: ```sh docker run -p 8000:8000 \ -e AURORA_JWT_SECRET="$(openssl rand -hex 32)" \ -e AURORA_ENV=prod \ org/meu_app:latest ``` ## 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: ```yaml # _aurora.yml statics: /assets: /srv/aurora-shared ``` `aurora_app()` serves that directory at the prefix (in addition to `www/` at `/`), so the app references the files by URL: ```html ``` ```sh docker run -p 8000:8000 \ -v ./data:/app/data:ro \ -v /srv/aurora-shared:/srv/aurora-shared:ro \ org/meu_app:latest ``` Update 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: ```{r} 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: prod ``` Paste 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](https://github.com/StrategicProjects/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. ```{r} 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: prod ``` `min_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. ## Checklist - [ ] Strong `AURORA_JWT_SECRET` injected at run time (if using auth). - [ ] `AURORA_ENV=prod` and HTTPS terminated at the proxy. - [ ] Image rebuilt after dependency changes (sysdeps are resolved at build). - [ ] Health check wired to a public route (e.g. `/health`).