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.
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: https://docs.docker.com/get-docker/
- Colima (macOS, free Docker drop-in without the Desktop subscription)
- Podman: https://podman.io/getting-started/installation
The engine has to be running when Electron launches. shinyelectron picks whichever it finds, checking Docker first, then Podman.
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:
The eight underlying steps:
- Electron starts and shows the lifecycle splash.
- The
container.jsbackend locates the socket:docker context inspectfirst, then well-known Unix sockets (/var/run/docker.sock,~/.docker/run/docker.sock,~/.colima/docker.sock) or Windows named pipes. - It selects an engine (Docker or Podman) based on what is available.
- If the image is missing, it is built from an embedded Dockerfile or pulled from a registry.
-
docker run -dstarts the container. The host port is mapped through; the app directory is bind-mounted to/appso the container reads your files live. - The backend polls the Shiny server for up to 120 seconds.
- Electron loads
http://localhost:<port>. - 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: 3838image: 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.
Built-in R image
The built-in R Dockerfile is based on rocker/r2u:24.04, which serves pre-compiled R packages via apt. It supports amd64 and arm64.
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:
# Minimal R + Shiny image for shinyelectron container strategy
# Uses rocker/r2u which provides pre-built binary R packages via apt
# Supports both amd64 and arm64 (Apple Silicon)
FROM rocker/r2u:24.04
# Install R packages as system packages — pre-compiled, no source compilation
# This works on both amd64 and arm64
RUN apt-get update && apt-get install -y --no-install-recommends \
r-cran-shiny \
r-cran-jsonlite \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 3838
ENTRYPOINT ["/entrypoint.sh"]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 with the apt-installed R libraries on .libPaths(). R package dependencies are baked into the image at build time as r-cran-* apt packages, not installed by the entrypoint.
#!/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:3.12-slim with shiny pre-installed via pip.
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:
# Minimal Python + Shiny image for shinyelectron container strategy
FROM python:3.12-slim
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:appRegistry 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: trueAny 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 three obligations:
-
Listen on the
PORTenv variable (default 3838). -
Use
/appas the working directory. That is where the app is bind-mounted. -
Honor
PORTandHOSTfor the server bind address.
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 it would use (Docker or Podman), where the socket lives, and whether the daemon is reachable. Run it before a release build to catch a missing or stopped engine cheaply.
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: set engine: "podman" in your config, or let auto-detection sort it out.
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.