Skip to content

Commit b77a5ee

Browse files
authored
Merge pull request #19 from h4sci/blog-alpine
Added Fine Tuned Alpine Blogpost
2 parents 964011f + 98721ca commit b77a5ee

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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!
77.1 KB
Loading

0 commit comments

Comments
 (0)