--- title: "Container Strategy" vignette: > %\VignetteIndexEntry{Container Strategy} %\VignetteEngine{quarto::html} %\VignetteEncoding{UTF-8} knitr: opts_chunk: collapse: true comment: "#>" --- ```{r} #| include: false library(shinyelectron) ``` A container holds your app, its runtime, and every system library it needs. Electron is the window onto that container. The user installs Docker or Podman; you ship an image.
Diagram showing the runtime layout of a shinyelectron container app on the user's machine. A dashed outer frame labeled USER'S MACHINE encloses three components. On the left, an Electron app card with traffic-light window controls and a viewport that says WebView pointing to http://localhost:3838. Below it, an App files on disk card showing a folder icon, the path path/to/my-app/, and a list of files (app.R, server.R, ui.R, dependencies.json). On the right, a sky-blue Container Engine card labeled Docker or Podman wraps a Container Instance card containing two layers: an Image layer with a purple stripe describing OS base plus R or Python plus Shiny sourced from rocker/r-ver, python:3.14-slim, your Dockerfile, or a registry; and a /app bind mount layer with an amber stripe describing your app code mounted from disk, with edits visible inside the container live, and noting that extra volumes such as /data and /models attach the same way. Two arrows connect the host components to the container: an HTTP arrow labeled localhost:3838 from the Electron card to the Image layer, and a bind mount arrow from the App files card to the /app layer.
Anatomy of a containerized shinyelectron app on the user's machine. Electron talks to a Shiny server inside a container over `http://localhost:3838`, and your app code on disk is bind-mounted into the container at `/app` so the running container reads your files live.
## When to reach for a container Pick the container strategy when any of these are true: - Your app leans on **heavy system libraries** (GDAL, PROJ, database drivers, C toolchains) that are painful to bundle portably. - **Reproducibility** is the point. The image pins every layer, OS up. - Your team **already builds with Docker** and you want the desktop and server to share an environment. - You are shipping to a **known audience** (internal users, a lab, a team) who can install a container engine. For apps with only R or Python packages and no system extras, use `auto-download` or `bundled`. Those ask nothing of the user. ## Prerequisites The end user needs one of: - **Docker Desktop**: - **[Colima](https://github.com/abiosoft/colima)** (macOS, free Docker drop-in without the Desktop subscription) - **Podman**: The engine has to be running when Electron launches. The engine is set from your `_shinyelectron.yml` config (default: `docker`). Podman users must set `engine: "podman"` explicitly. On the build machine a container engine is optional. If present, shinyelectron confirms the daemon is reachable. If absent, it warns and keeps going: the image is built or pulled on the user's machine at first launch. ## The launch flow When a user opens the packaged app, shinyelectron walks four phases:
Horizontal flow diagram with four phase cards connected by arrows under the title What happens when the user opens the app. Phase 1, Engine, blue, covers steps 1 to 3 to find and pick: Electron splash starts, locate the engine socket, pick Docker or Podman. Phase 2, Image, purple, covers step 4 to make available: pull from registry, or build the embedded Dockerfile locally; cached after first launch. Phase 3, Run, green, covers steps 5 to 7 to start and connect: docker run -d, poll Shiny for up to 120 seconds, WebView loads the URL. Phase 4, Quit, amber, covers step 8 cleanup: stop the container, remove it; the image stays cached for the next launch.
Four phases of launching a containerized shinyelectron app. Each phase groups one or more of the eight low-level steps and is colored consistently with later sections of this guide.
The eight underlying steps: 1. **Electron starts** and shows the lifecycle splash. 2. The `container.js` backend **locates the socket**: `docker context inspect` first, then well-known Unix sockets (`/var/run/docker.sock`, `~/.docker/run/docker.sock`, `~/.colima/docker.sock`) or Windows named pipes. 3. It **reads the engine** from the baked configuration (set via `engine:` in `_shinyelectron.yml`; default is `docker`). 4. If the image is missing, it is **built from an embedded Dockerfile** or **pulled from a registry**. 5. `docker run -d` starts the container. The host port is mapped through; the app directory is bind-mounted to `/app` so the container reads your files live. 6. The backend **polls** the Shiny server for up to 120 seconds. 7. Electron loads `http://localhost:`. 8. On quit, the container is stopped and removed. ## Configuration Set `runtime_strategy: container` in `_shinyelectron.yml`: ```yaml app: name: "My Containerized App" version: "1.0.0" build: type: "r-shiny" runtime_strategy: "container" container: engine: "docker" # "docker" or "podman" image: null # null = use embedded Dockerfile tag: "latest" pull_on_start: true volumes: {} # extra host:container volume mounts env: {} # extra environment variables server: port: 3838 ``` `image: null` (the default) embeds a Dockerfile in the package and builds locally on first launch. Set `image` to a registry reference like `ghcr.io/myorg/myapp` to pull instead. ## Where the image comes from There are three paths. The first two are generated automatically; the third is for when you need more than the built-ins offer.
Three-card horizontal layout under the title Where the image comes from. Card one, blue, Built-in R, generated when type is r-shiny, configured with type r-shiny and image null, uses rocker/r-ver as base, and installs dependencies as P3M binary packages plus apt system requirements baked into the image at build. Card two, green, Built-in Python, generated when type is py-shiny, configured with type py-shiny and image null, uses python:3.14-slim as base, and pip-installs dependencies at first launch via the entrypoint script. Card three, amber, Registry pull, any image you publish, configured with image set to a reference like ghcr.io/org/app and a tag, base is whatever you built, dependencies are already inside the image, and shinyelectron does no Dockerfile generation.
Three ways an image is sourced for a containerized shinyelectron app. Each card shows the configuration, the base image, and when extra dependencies install.
### Built-in R image The built-in R Dockerfile is based on [`rocker/r-ver`](https://github.com/rocker-org/rocker-versioned2), tagged to the resolved R version (for example, `rocker/r-ver:4.6.1`). This image pre-wires the [Posit Public Package Manager](https://packagemanager.posit.co/) binary repository, so `install.packages()` retrieves pre-compiled binaries without needing a C compiler in the image. The base image tag tracks `dependencies.r.version` from `_shinyelectron.yml`. Leave it `null` to use the maintained pin; set it to `"4.6.1"` (or any version string) to pin exactly; set it to `"latest"` to query the upstream source at build time. ```r export( appdir = "path/to/my-r-app", destdir = "path/to/output", app_name = "My R App", app_type = "r-shiny", runtime_strategy = "container" ) ``` The exact Dockerfile that ships with the package, read live from `inst/dockerfiles/r-shiny/Dockerfile`: ```{r} #| echo: false #| results: asis cat(c("```dockerfile", readLines(system.file("dockerfiles/r-shiny/Dockerfile", package = "shinyelectron")), "```"), sep = "\n") ``` The Dockerfile exposes `ARG R_VERSION` so you can override the R version at build time without editing the file: ```sh docker build --build-arg R_VERSION=4.5.1 -t myapp:4.5.1 dockerfiles/ ``` shinyelectron rewrites the `ARG R_VERSION` default to the resolved version when it copies the Dockerfile into the build, so the baked image tag and the `FROM` line stay in sync. The image's `ENTRYPOINT` is the bundled `entrypoint.sh`. It honors `PORT` and `HOST` env vars (defaulting to `3838` / `0.0.0.0`) and launches the app via `Rscript`. R package dependencies are baked into the image at build time via `install.packages()` using the P3M binary repository. System libraries (detected automatically via the Posit Package Manager system-requirements service) are also installed as apt packages at build time. ```{r} #| echo: false #| results: asis cat(c("```bash", readLines(system.file("dockerfiles/r-shiny/entrypoint.sh", package = "shinyelectron")), "```"), sep = "\n") ``` ### Built-in Python image The built-in Python Dockerfile uses `python:-slim` (for example, `python:3.14-slim`), tagged to the major.minor portion of the resolved Python version. The base image tracks `dependencies.python.version` from `_shinyelectron.yml`. The Dockerfile exposes `ARG PY_VERSION` so you can build with a different Python minor version: ```sh docker build --build-arg PY_VERSION=3.12 -t myapp:3.12 dockerfiles/ ``` shinyelectron rewrites the `ARG PY_VERSION` default to the configured major.minor value when it copies the Dockerfile into the build. ```r export( appdir = "path/to/my-py-app", destdir = "path/to/output", app_name = "My Python App", app_type = "py-shiny", runtime_strategy = "container" ) ``` Read live from `inst/dockerfiles/py-shiny/Dockerfile`: ```{r} #| echo: false #| results: asis cat(c("```dockerfile", readLines(system.file("dockerfiles/py-shiny/Dockerfile", package = "shinyelectron")), "```"), sep = "\n") ``` The `ENTRYPOINT` is `entrypoint.sh`. Unlike the R image, it installs Python packages listed in `/app/dependencies.json` at startup using `pip --only-binary :all:`, then launches the Shiny server: ```{r} #| echo: false #| results: asis cat(c("```bash", readLines(system.file("dockerfiles/py-shiny/entrypoint.sh", package = "shinyelectron")), "```"), sep = "\n") ``` ### System dependency baking When `runtime_strategy` is `"container"`, shinyelectron bakes both R/Python packages and their system dependencies into the image at build time. This means the container starts fast (no compile-or-install step at launch) and works offline after the first build. **For R apps**, shinyelectron queries the Posit Package Manager system-requirements service to automatically detect the apt packages that your R packages need (for example, `libgdal-dev` for `sf`, `libcurl4-openssl-dev` for `curl`). Those are added to the Dockerfile as an `apt-get install` layer alongside the `install.packages()` call. Use `dependencies.system_packages` in `_shinyelectron.yml` to add anything the auto-detection misses. **For Python apps**, system-requirement auto-detection is not performed. Use `dependencies.system_packages` to list any C-level build dependencies your Python packages need. Both paths emit a Dockerfile layer like: ```dockerfile RUN apt-get update && apt-get install -y --no-install-recommends \ libgdal-dev libproj-dev \ && rm -rf /var/lib/apt/lists/* ``` Configure additional system packages in `_shinyelectron.yml`: ```yaml dependencies: r: version: null system_packages: - libgdal-dev - libproj-dev - libpq-dev ``` ### Registry image For dependencies that go beyond what the built-ins offer (heavy system libraries like GDAL or PROJ, custom Python ML stacks, database drivers), build your own image, publish it to a registry, and point shinyelectron at the reference: ```yaml container: image: "ghcr.io/myorg/myapp" tag: "v1.2.0" pull_on_start: true ``` Any OCI registry works: GHCR, Docker Hub, ECR, or an internal registry the user's machine can reach. shinyelectron skips Dockerfile generation entirely and just pulls + runs. Your published image has four obligations: 1. **Mount point at `/app`.** shinyelectron bind-mounts the app directory to `/app`. Set `WORKDIR /app` in your Dockerfile and read app files from there. 2. **Serve on `$HOST:$PORT`.** shinyelectron sets `PORT` (default `3838`) and `HOST` (default `0.0.0.0`). Your server command must honor both env vars. 3. **Expose the port.** Add `EXPOSE 3838` to the Dockerfile so the container engine maps the port correctly. 4. **Pass-through config.** Any `container.env` entries in `_shinyelectron.yml` are forwarded as `-e` flags to `docker run`; any `container.volumes` entries are forwarded as `-v` flags. Your image receives them automatically and does not need to bake them in. A reference R image with spatial libraries: ```dockerfile FROM rocker/r2u:24.04 RUN apt-get update && apt-get install -y --no-install-recommends \ r-cran-shiny r-cran-sf r-cran-terra libgdal-dev libproj-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /app EXPOSE 3838 CMD ["Rscript", "--vanilla", "-e", \ "shiny::runApp('/app', port=as.integer(Sys.getenv('PORT',3838)), host=Sys.getenv('HOST','0.0.0.0'), launch.browser=FALSE)"] ``` A reference Python image with ML dependencies: ```dockerfile FROM python:3.12-slim RUN pip install --no-cache-dir shiny pandas scikit-learn WORKDIR /app EXPOSE 3838 CMD ["python3", "-m", "shiny", "run", "--port", "3838", \ "--host", "0.0.0.0", "--app-dir", "/app", "--no-dev-mode"] ``` Build, push to your registry, and reference it from `_shinyelectron.yml`. The user's machine pulls on first launch and caches afterward. ## Passing volumes and env vars Extra mounts and variables from the config are forwarded as `-v` and `-e` flags to `docker run`. ```yaml container: engine: "docker" volumes: "/path/to/data": "/data" "/path/to/models": "/models" env: SHINY_LOG_LEVEL: "debug" DATABASE_URL: "postgresql://localhost:5432/mydb" ``` ## Verifying the engine Check what shinyelectron sees on this machine: ```r sitrep_electron_system() ``` The report names the container engine installed on this machine (Docker or Podman) and where the binary was found. It checks only whether the engine binary is on PATH; it does not probe the daemon socket, so a stopped daemon is not detected. Use `validate_container_available()` or attempt a build to confirm the daemon is running. ## Limitations For the security side (volumes, root, escapes), see [Security Considerations](security.html#container-strategy-security). **Your users need a container engine.** That puts containers out of reach for the casual download-and-launch crowd. For a broader audience, reach for `bundled` or `auto-download`. Those ask nothing of the host. **First launch is slow.** Pulling or building a fresh image takes a minute or two. Every launch after that is seconds, since the image is cached. **Docker inside another VM is touchy.** Docker Desktop running under Parallels or VMware on macOS sometimes refuses to cooperate, and rarely says why. Native Podman on the host is the usual escape hatch. **The daemon must be running first.** If Docker Desktop is off, the app surfaces a lifecycle error asking the user to start it. shinyelectron cannot start the daemon on their behalf. ## Platform notes **Docker Desktop is paid at scale.** Larger organizations need a subscription. [Podman](https://podman.io/) is a free, daemonless drop-in, but Podman users must set `engine: "podman"` explicitly in `_shinyelectron.yml`. The default engine is `docker`; leaving it unchanged will fail on a Podman-only machine. **Architecture matching.** shinyelectron pulls or builds for the host's CPU: `linux/arm64` on Apple Silicon, `linux/amd64` elsewhere. That keeps Apple Silicon off the Rosetta emulation path and the performance tax it carries. **Colima on macOS.** [Colima](https://github.com/abiosoft/colima) is a Docker drop-in, not a third engine: same `docker` CLI, daemon hosted in a Lima VM rather than Docker Desktop. Keep `engine: "docker"` in your config; shinyelectron finds the socket at `~/.colima/docker.sock` automatically.