---
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.
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:
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 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.