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. 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:
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 reads the engine from the baked configuration (set via
engine:in_shinyelectron.yml; default isdocker). - 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/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:appSystem 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-devRegistry 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 four obligations:
-
Mount point at
/app. shinyelectron bind-mounts the app directory to/app. SetWORKDIR /appin your Dockerfile and read app files from there. -
Serve on
$HOST:$PORT. shinyelectron setsPORT(default3838) andHOST(default0.0.0.0). Your server command must honor both env vars. -
Expose the port. Add
EXPOSE 3838to the Dockerfile so the container engine maps the port correctly. -
Pass-through config. Any
container.enventries in_shinyelectron.ymlare forwarded as-eflags todocker run; anycontainer.volumesentries are forwarded as-vflags. 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.