UFRC PubApps: Deploying Shiny Apps with Podman
Overview
UFRC PubApps is a virtual machine managed by UF Research Computing that allows us to host containerized web applications accessible via public URLs. We use Podman (a daemonless, rootless alternative to Docker) to build and run containers on this VM. All of our currently deployed apps are R Shiny apps, though the same workflow applies to other app types with some differences noted below.
Signing In
PubApps is only accessible by SSHing from HiPerGator. You cannot SSH in directly from your local machine. Everyone logs in as PubApps user jobrant, but access is managed through your hipergator account.
ssh jobrant@pubjobrant1
If you are prompted for a password, it is not working — access is by SSH key only. If you cannot connect, you need to be added as an authorized user (see Adding a New User).
Directory Structure
The top-level structure on the VM looks like this:
~/
├── bcb-sr/
│ ├── prod/
│ │ ├── apps/ # Production app code (bind-mounted at runtime)
│ │ └── web/
│ │ └── index.html # Production landing page
│ ├── dev/
│ │ ├── apps/ # Development app code (bind-mounted at runtime)
│ │ └── web/
│ │ └── index.html # Dev landing page
│ └── tests/
├── Dockerfiles/ # Archived/misc Dockerfiles not in active deployment
├── port_info.csv # Record of all port assignments and URLs
└── README.md
When starting a new app, create its directories manually or via git clone (see Code Development Workflow). :
mkdir -p ~/bcb-sr/prod/apps/<your-app-name>
mkdir -p ~/bcb-sr/dev/apps/<your-app-name>
Production-ready code goes in prod/apps/, work-in-progress in dev/apps/.
The web index pages (web/index.html) are where the landing pages listing
available apps are maintained — update these when adding a new app with a live URL.
(This directory schema is just for organizational purposes and does not affect the running containers or URLs; any container can bind-mount any filepath and any port can be mapped to any URL.)
Code Development Workflow
Avoid writing or editing code directly on the PubApps VM. The VM has no UI and is not set up as a development environment. The best workflow is:
- Develop and test your app on HiPerGator and/or locally
- Push changes to GitHub
- On the VM, clone the repo once into the appropriate
apps/directory:
cd ~/bcb-sr/prod/apps/
git clone <your-repo-url> <your-app-name>
- For subsequent updates, pull changes:
cd ~/bcb-sr/prod/apps/<your-app-name>
git pull origin main
The VM is a deployment target only. All development should happen elsewhere.
Building a Container Image with Podman
The Dockerfile
Each app needs a Dockerfile that defines its environment. Below is a minimal starting point for an R Shiny app — only include the packages your app actually needs. Adding unnecessary packages increases image size and build time, and makes the environment harder to maintain.
# Use the official Rocker Shiny base image
FROM rocker/shiny:latest
# Install system dependencies
# These are common ones — remove any your app does not need
RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
&& rm -rf /var/lib/apt/lists/*
# Install only the CRAN packages your app actually uses
RUN install2.r --error \
shiny \
<package1> \
<package2>
# If your app uses Bioconductor packages, install BiocManager first,
# then only the packages you need
RUN R -e "install.packages('BiocManager', repos='https://cran.rstudio.com/')"
RUN R -e "BiocManager::install(c('<BiocPackage1>', '<BiocPackage2>'), update = FALSE)"
# Expose the Shiny port
EXPOSE 3838
# Start the Shiny app
CMD ["R", "-e", "shiny::runApp('/srv/shiny-server', host='0.0.0.0', port=3838)"]
Best practice: Think of the image as the environment, not the app. It should contain R, system libraries, and R packages — nothing else. In most cases, app code can just be bind-mounted at runtime (see below), which means you can update the app without rebuilding the image.
Image size and storage
Image size is determined entirely at build time by what you install. A common
source of unexpectedly large images is installing packages "just in case" rather
than only what the app requires. Once built, the image itself does not grow.
However, if a running container writes files to paths inside the container that
are not bind-mounted to the host, those writes accumulate in the container's
writable layer. Any files your app generates at runtime (e.g. user uploads,
temporary outputs) should be written to /tmp inside the container, or to a
bind-mounted directory on the host.
Non-Shiny apps
The same Podman workflow applies to non-Shiny apps (e.g. Python/Flask, FastAPI,
Streamlit). The main differences are the base image (python:3.x-slim or
similar instead of rocker/shiny), the internal port (varies by framework),
and the CMD. The port mapping and deployment steps are otherwise identical.
Building the image
Run this from the directory containing your Dockerfile:
podman build -t <image-name>:latest .
You can also specify a Dockerfile by name if it is not called Dockerfile:
podman build -t <image-name>:latest -f <Dockerfile-name> .
Use a clear, consistent image name (e.g. myapp-prod, batchcorrector).
You will reference it by name when running containers.
Inspecting images
# List all local images
podman images
# Inspect a specific image
podman inspect <image-name>
Running Containers
Port assignment: "prod is odd"
Each app gets two ports — one for production, one for dev. Odd ports map to
production (bcb-sr.rc.ufl.edu) and even ports map to dev
(devbcb-sr.rc.ufl.edu). Ports are assigned in sequence. To find the next
available pair:
- Check
port_info.csvfor the highest assigned port - Also run
podman psto catch any running containers not yet recorded - Take the next odd number as your prod port and the following even number as dev
Note: a container could be stopped and not appear in
podman psbut still have its port recorded inport_info.csv— check both.
Running a container
podman run -d \
--name <container-name> \
-p <PORT>:3838 \
-v /pubapps/jobrant/bcb-sr/prod/apps/<your-app-name>:/srv/shiny-server \
localhost/<image-name>:latest
The -v flag bind-mounts your app code directory into the container at the
path Shiny expects. This means you can update the app by doing git pull and
restarting the container without rebuilding the image, and the image stays lean
and reusable across app versions.
Alternatively, you can bake the code into the image using a COPY
instruction in the Dockerfile. This makes the image self-contained and
portable, but means you must rebuild and redeploy the image every time the
app code changes. For active development, the bind-mount approach is strongly
preferred.
The --replace flag
If a container with that name already exists, add --replace to stop and
replace it in one command:
podman run -d --replace \
--name <container-name> \
-p <PORT>:3838 \
-v /pubapps/jobrant/bcb-sr/prod/apps/<your-app-name>:/srv/shiny-server \
localhost/<image-name>:latest
Managing Running Containers
# List all running containers
podman ps
# List all containers including stopped ones
podman ps -a
# Stop a container
podman stop <container-name>
# Start a stopped container
podman start <container-name>
# Restart a container
podman restart <container-name>
# View logs (useful for debugging)
podman logs <container-name>
podman logs --tail 50 <container-name>
Checking storage and pruning
Unused images and stopped containers accumulate and consume disk space. Clean up periodically:
# Check disk usage by images, containers, and volumes
podman system df
# Remove all stopped containers
podman container prune
# Remove all unused images (not referenced by any container)
podman image prune
# Remove everything unused at once (containers, images, volumes)
podman system prune
Caution:
podman system prunewill remove stopped containers and unused images. Make sure you do not need them before running it — images take time to rebuild.
Requesting a URL (Reverse Proxy)
Once your containers are running, submit a ticket to UFRC to assign URLs:
URL: https://support.rc.ufl.edu/enter_bug.cgi
Use this template:
Subject: URL assignment for jobrant@pubjobrant1
Hello,
Can you please assign the following URLs:
<PROD PORT> -> https://bcb-sr.rc.ufl.edu/<your-app-name>
<DEV PORT> -> https://devbcb-sr.rc.ufl.edu/<your-app-name>
Thank you!
You can test that the container is responding before submitting the ticket:
curl http://localhost:<PORT>
Updating port_info.csv
Every deployed container must be recorded in port_info.csv. Add a row for
each container (prod and dev) with all fields populated:
port,container,volume,image,URL
21123,myapp-prod,/pubapps/jobrant/bcb-sr/prod/apps/myapp:/srv/shiny-server,localhost/myapp:latest,https://bcb-sr.rc.ufl.edu/myapp/
21124,myapp-dev,/pubapps/jobrant/bcb-sr/dev/apps/myapp:/srv/shiny-server,localhost/myapp:latest,https://devbcb-sr.rc.ufl.edu/myapp/
This file is the authoritative record of what is deployed and where. Keep it up to date — it is the first place to check when troubleshooting or adding a new app.
Adding a New User
To give a HiPerGator user SSH access to the PubApps VM:
- Ask them to share their public SSH key from their
~/.ssh/directory on HiPerGator - Append that key to
~/.ssh/authorized_keysonpubjobrant1:
echo "<their public key>" >> ~/.ssh/authorized_keys
They can then SSH in from HiPerGator. If they are prompted for a password, the key was not added correctly.