|
| 1 | +--- |
| 2 | +title: "Tuning Our Lab Setup for Production" |
| 3 | +author: "Minna Heim & Matthias Bannert" |
| 4 | +toc: false |
| 5 | +draft: false |
| 6 | +snippet: "We revisit our Shiny + Postgres survey example from the previous post and show how to keep the exact same functionality while cutting image size in half, and reducing the dependencies. By switching to a lighter Alpine-based R image and making dependencies explicit, we turn a teaching prototype into a leaner, more portable production setup that is also suitable for CI/CD." |
| 7 | +cover: ./shiny-survey-lighter.png |
| 8 | +coverAlt: "R Shiny Frontend of an online survey" |
| 9 | +publishDate: "2025-11-26" |
| 10 | +category: "SELF-HOSTED, DevOps, Tutorial" |
| 11 | +tags: [Survey, R, Shiny, Postgres, Docker, docker-compose, Alpine] |
| 12 | +--- |
| 13 | + |
| 14 | +<!-- 2 more introductory sätze --> |
| 15 | +Based on the example from the [previous blogpost](https://h4sci.github.io/blog/self-hosted-shiny-pg), while the postgres image that we use is lean and well suited for our production purposes, the `rocker/shiny` image is quite general and bulky. Hence, we would like to build a custom shiny image instead in this post.. |
| 16 | + |
| 17 | + |
| 18 | +## Breaking down our custom Alpine-Based Image |
| 19 | + |
| 20 | + |
| 21 | +```dockerfile |
| 22 | +# Base image |
| 23 | +FROM devxygmbh/r-alpine:4-3.21 |
| 24 | + |
| 25 | +# Install system dependencies (Alpine package manager) |
| 26 | +RUN apk update && apk add --no-cache \ |
| 27 | + libpq \ |
| 28 | + libxml2 \ |
| 29 | + libcurl \ |
| 30 | + libgit2 \ |
| 31 | + postgresql-client \ |
| 32 | + postgresql-libs \ |
| 33 | + build-base \ |
| 34 | + postgresql-dev \ |
| 35 | + libxml2-dev \ |
| 36 | + libcurl-dev \ |
| 37 | + libgit2-dev |
| 38 | + |
| 39 | +# Install R packages |
| 40 | +RUN R -q -e 'install.packages(c("shiny", "RPostgres", "shinythemes", "shinyjs", "DBI"), repos="https://cloud.r-project.org")' |
| 41 | + |
| 42 | +EXPOSE 3838 |
| 43 | + |
| 44 | +# not actually used, overwritten by `docker-compose.yml`, here just in case the container is used alone, without docker-compose. |
| 45 | +CMD ["R", "-e", "shiny::runApp('/srv/shiny-server/survey', host='0.0.0.0', port=3838)"] |
| 46 | +``` |
| 47 | + |
| 48 | + |
| 49 | +### Key differences: |
| 50 | + |
| 51 | +**Base image**: `devxygmbh/r-alpine:4-3.21` instead of `rocker/shiny`. First of all, alpine is much lighter base than debian or ubuntu. That is, the image size is much smaller and fewer dependencies make maintenance easier, for example monitoring critical vulnerabilities (CVEs). Keep in mind that docker images get pulled often, and a few hundred MB in image size can reduce traffic and build time substantially. |
| 52 | + |
| 53 | +**Explicit system libraries**: because fewer sys-libs are pre-installed we need to add postgres drivers, curl and a few other libraries. |
| 54 | + |
| 55 | +**ARM vs AMD architecture**: DevXY Gmbh provides ARM images to run on modern ARM chipset architecture such as Apple chips from the M1 on, or modern electricity saving servers. Some libraries/binaries are not available for ARM, hence very general prebuilt images from dockerhub (such as `rocker/shiny`) use AMD. For an extended discussion, see below. |
| 56 | + |
| 57 | + |
| 58 | +## Why Care About Image Size? |
| 59 | + |
| 60 | +When you're just getting started, using a convenient base image such as |
| 61 | +`rocker/shiny` is a great choice. It gives you: |
| 62 | + |
| 63 | +- **R + Shiny pre-installed** |
| 64 | +- **System libraries** for many common packages |
| 65 | +- A quick path to “it just works” |
| 66 | + |
| 67 | +However, one trade-off is size. On a laptop with limited disk space, a CI system that |
| 68 | +pulls images frequently, or a small cloud VM, image size starts to matter: |
| 69 | + |
| 70 | +- **Slower pulls and pushes** |
| 71 | +- **Longer cold starts** when new machines spin up |
| 72 | +- **Less space** for other projects and datasets |
| 73 | + |
| 74 | +To showcase this better, here are the sizes for the base images we compare: |
| 75 | + |
| 76 | +**Base images:** |
| 77 | +- `devxygmbh/r-alpine:4-3.21`: **138.3 MB** |
| 78 | +- `rocker/shiny`: **544.9 MB** |
| 79 | + |
| 80 | +**After installing all dependencies and R packages:** |
| 81 | +- **Base example (rocker/shiny)**: **1.68 GB** |
| 82 | +- **Lightweight example (Alpine-based shiny image)**: **789 MB** |
| 83 | + |
| 84 | +That's not just a nice chart for presentations — it’s a practical improvement when |
| 85 | +you rebuild frequently or run on constrained hardware. |
| 86 | + |
| 87 | +Using a lighter base image such as `r-alpine` also means: |
| 88 | + |
| 89 | +- **Adding only the dependencies you need** |
| 90 | +- **Better isolation and maintainability** |
| 91 | +- **Smaller attack surface** |
| 92 | + |
| 93 | +**Exercise:** Check [Docker Hub](https://hub.docker.com/) for these base images to compare contents, and you will see the reason for the size differences. |
| 94 | + |
| 95 | + |
| 96 | +## How to Measure Image Size Yourself |
| 97 | + |
| 98 | +You can reproduce these numbers on your machine with standard Docker commands. |
| 99 | +After building an image, just run: |
| 100 | + |
| 101 | +```bash |
| 102 | +docker images |
| 103 | +``` |
| 104 | + |
| 105 | +Docker will show you the image in a table, including a **SIZE** column. |
| 106 | + |
| 107 | + |
| 108 | +## Why Architecture Matters |
| 109 | + |
| 110 | +Docker images must either: |
| 111 | + |
| 112 | +- be built **for the architecture you're running**, or |
| 113 | + |
| 114 | +- be built as **multi-arch** images. |
| 115 | + |
| 116 | +If an image isn't available for your machine, Docker Desktop will fall back to CPU |
| 117 | +emulation using qemu, which works but is slower. |
| 118 | + |
| 119 | +### Rocker vs Alpine: Platform Support |
| 120 | + |
| 121 | +`rocker/shiny`: |
| 122 | + |
| 123 | +- Official build targets amd64 |
| 124 | + |
| 125 | +- No native arm64 builds |
| 126 | + |
| 127 | +- On Apple Silicon → runs via emulation → slower builds and slower runtime (this is what the previous blogpost did with the `--platform=linux/amd64 rocker/shiny` flag) |
| 128 | + |
| 129 | +- Larger, heavier images |
| 130 | + |
| 131 | +Alpine-based R images (`devxygmbh/r-alpine`): |
| 132 | + |
| 133 | +- Typically provide amd64 + arm64 images |
| 134 | + |
| 135 | +- Faster native performance on Apple Silicon and arm clouds servers (often cheaper) |
| 136 | + |
| 137 | +- Smaller and easier to rebuild locally |
| 138 | + |
| 139 | + |
| 140 | +## APPENDIX: running the fine tuned app |
| 141 | + |
| 142 | +Use the following `docker-compose.yaml` file with the `DOCKERFILE` from above: |
| 143 | + |
| 144 | +```yaml |
| 145 | +services: |
| 146 | + shiny: |
| 147 | + build: |
| 148 | + context: . |
| 149 | + dockerfile: DOCKERFILE |
| 150 | + container_name: fe_shiny |
| 151 | + restart: always |
| 152 | + ports: |
| 153 | + - "3838:3838" |
| 154 | + volumes: |
| 155 | + - "./shiny-data:/srv/shiny-server/survey" |
| 156 | + command: ["R", "--vanilla", "-e", "shiny::runApp('/srv/shiny-server/survey', host='0.0.0.0', port=3838)"] |
| 157 | + |
| 158 | + postgres: |
| 159 | + # a name, e.g., db_container is instrumental to be |
| 160 | + # called as host from the shiny app |
| 161 | + container_name: db_container |
| 162 | + image: postgres:15-alpine |
| 163 | + restart: always |
| 164 | + environment: |
| 165 | + - POSTGRES_USER=postgres |
| 166 | + - POSTGRES_PASSWORD=postgres # Don't use passwords like this in production |
| 167 | + # This port mapping is only necessary to connect from the host, |
| 168 | + # not to let containers talk to each other. |
| 169 | + # port-forwarding: from host port:to docker port -> mapping |
| 170 | + ports: |
| 171 | + - "1111:5432" |
| 172 | + # if container killed, then data is still stored in volume (locally) |
| 173 | + volumes: |
| 174 | + - "./pgdata:/var/lib/postgresql/data" |
| 175 | +``` |
| 176 | +
|
| 177 | +
|
| 178 | +With the assumption that you have followed the instructions of the previous blog post & have gotten it to run, all you have to do is: |
| 179 | +
|
| 180 | +1. Start the full stack: |
| 181 | +
|
| 182 | + ```bash |
| 183 | + docker-compose up -d |
| 184 | + ``` |
| 185 | + |
| 186 | +2. Once the containers are up, visit: |
| 187 | + |
| 188 | + - Shiny app: `http://localhost:3838` |
| 189 | + |
| 190 | + |
| 191 | +From here on your survey behaves exactly as in the original tutorial, only the |
| 192 | +containers are **smaller and more explicit** in their dependencies. |
| 193 | + |
| 194 | + |
| 195 | + |
| 196 | +If you want to see this **Example in action**, visit the [github.com/h4sci/h4sci-poll](https://github.com/h4sci/h4sci-poll) directory! |
0 commit comments