Skip to contents

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:

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:<port>.
  8. On quit, the container is stopped and removed.

Configuration

Set runtime_strategy: container in _shinyelectron.yml:

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, tagged to the resolved R version (for example, rocker/r-ver:4.6.1). This image pre-wires the Posit Public Package Manager 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.

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 + Shiny image for the shinyelectron container strategy.
# rocker/r-ver pins an exact R version and pre-wires P3M binaries, so
# install.packages() pulls precompiled binaries. System libraries those
# binaries need are baked in by the build (via the Posit Package Manager
# system-requirements API).
ARG R_VERSION=latest
FROM rocker/r-ver:${R_VERSION}

ENV DEBIAN_FRONTEND=noninteractive

RUN R -e "install.packages('shiny')"

WORKDIR /app

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 3838

ENTRYPOINT ["/entrypoint.sh"]

The Dockerfile exposes ARG R_VERSION so you can override the R version at build time without editing the file:

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.

#!/bin/bash
set -e

PORT=${PORT:-3838}
HOST=${HOST:-0.0.0.0}

echo "Starting Shiny app on $HOST:$PORT..."
exec Rscript --vanilla -e ".libPaths(c('/usr/local/lib/R/site-library', '/usr/lib/R/site-library', '/usr/lib/R/library')); shiny::runApp('/app', port = ${PORT}, host = '${HOST}', launch.browser = FALSE)"

Built-in Python image

The built-in Python Dockerfile uses python:<major.minor>-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:

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.

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:

# Python + Shiny image for the shinyelectron container strategy.
ARG PY_VERSION=3.14
FROM python:${PY_VERSION}-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN pip install --no-cache-dir shiny

WORKDIR /app

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 3838

ENTRYPOINT ["/entrypoint.sh"]

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:

#!/bin/bash
set -e

PORT=${PORT:-3838}
HOST=${HOST:-0.0.0.0}

# Install dependencies from manifest if present
if [ -f /app/dependencies.json ]; then
  echo "Installing Python package dependencies..."
  python3 -c "
import json
with open('/app/dependencies.json') as f:
    deps = json.load(f)
if deps.get('packages'):
    import subprocess
    pkgs = deps['packages']
    index = deps.get('index_urls', ['https://pypi.org/simple'])[0]
    subprocess.run(['pip', 'install', '--only-binary', ':all:', '-i', index] + pkgs, check=True)
"
fi

echo "Starting Shiny app on $HOST:$PORT..."
exec python3 -m shiny run --port "$PORT" --host "$HOST" --app-dir /app --no-dev-mode app:app

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:

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:

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:

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:

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:

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.

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:

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.

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