Skip to content

Commit de88913

Browse files
authored
Merge pull request #1 from SumedhSankhe/docker-workflow
Docker workflow
2 parents 2a2ad65 + e66193f commit de88913

9 files changed

Lines changed: 1683 additions & 18 deletions

File tree

.Rprofile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
source("renv/activate.R")
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: Build and Push Docker Images
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- develop
8+
tags:
9+
- 'v*'
10+
pull_request:
11+
branches:
12+
- main
13+
workflow_dispatch:
14+
15+
env:
16+
REGISTRY: ghcr.io
17+
IMAGE_NAME: ${{ github.repository }}
18+
19+
jobs:
20+
build-and-push:
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
packages: write
25+
26+
strategy:
27+
fail-fast: false # Continue running other builds even if one fails
28+
matrix:
29+
dockerfile:
30+
- name: single-stage
31+
file: Dockerfile.single-stage
32+
description: "Baseline single-stage build"
33+
- name: two-stage
34+
file: Dockerfile.multistage
35+
description: "Optimized two-stage build"
36+
- name: three-stage
37+
file: Dockerfile.three-stage
38+
description: "Advanced three-stage build with base layer"
39+
40+
steps:
41+
- name: Checkout repository
42+
uses: actions/checkout@v4
43+
44+
- name: Set up Docker Buildx
45+
uses: docker/setup-buildx-action@v3
46+
47+
- name: Log in to GitHub Container Registry
48+
if: github.event_name != 'pull_request'
49+
uses: docker/login-action@v3
50+
with:
51+
registry: ${{ env.REGISTRY }}
52+
username: ${{ github.actor }}
53+
password: ${{ secrets.GITHUB_TOKEN }}
54+
55+
- name: Extract metadata (tags, labels) for Docker
56+
id: meta
57+
uses: docker/metadata-action@v5
58+
with:
59+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
60+
tags: |
61+
type=ref,event=branch,suffix=-${{ matrix.dockerfile.name }}
62+
type=ref,event=pr,suffix=-${{ matrix.dockerfile.name }}
63+
type=semver,pattern={{version}},suffix=-${{ matrix.dockerfile.name }}
64+
type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.dockerfile.name }}
65+
type=sha,prefix=${{ matrix.dockerfile.name }}-
66+
type=raw,value=${{ matrix.dockerfile.name }}-latest,enable={{is_default_branch}}
67+
labels: |
68+
org.opencontainers.image.title=Shiny Docker Optimization - ${{ matrix.dockerfile.name }}
69+
org.opencontainers.image.description=${{ matrix.dockerfile.description }}
70+
71+
- name: Build and push Docker image
72+
uses: docker/build-push-action@v5
73+
with:
74+
context: .
75+
file: ${{ matrix.dockerfile.file }}
76+
push: ${{ github.event_name != 'pull_request' }}
77+
tags: ${{ steps.meta.outputs.tags }}
78+
labels: ${{ steps.meta.outputs.labels }}
79+
cache-from: type=gha
80+
cache-to: type=gha,mode=max
81+
platforms: linux/amd64
82+
83+
- name: Generate build summary
84+
if: github.event_name != 'pull_request'
85+
run: |
86+
echo "## Docker Image Build Summary - ${{ matrix.dockerfile.name }}" >> $GITHUB_STEP_SUMMARY
87+
echo "" >> $GITHUB_STEP_SUMMARY
88+
echo "**Dockerfile:** \`${{ matrix.dockerfile.file }}\`" >> $GITHUB_STEP_SUMMARY
89+
echo "" >> $GITHUB_STEP_SUMMARY
90+
echo "**Description:** ${{ matrix.dockerfile.description }}" >> $GITHUB_STEP_SUMMARY
91+
echo "" >> $GITHUB_STEP_SUMMARY
92+
echo "### Tags Created" >> $GITHUB_STEP_SUMMARY
93+
echo '```' >> $GITHUB_STEP_SUMMARY
94+
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
95+
echo '```' >> $GITHUB_STEP_SUMMARY
96+
echo "" >> $GITHUB_STEP_SUMMARY
97+
echo "### Pull Command" >> $GITHUB_STEP_SUMMARY
98+
echo '```bash' >> $GITHUB_STEP_SUMMARY
99+
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.dockerfile.name }}-latest" >> $GITHUB_STEP_SUMMARY
100+
echo '```' >> $GITHUB_STEP_SUMMARY
101+
102+
compare-sizes:
103+
runs-on: ubuntu-latest
104+
needs: build-and-push
105+
if: success() && github.event_name != 'pull_request' # Only run if ALL builds succeed
106+
permissions:
107+
contents: read
108+
packages: read
109+
110+
steps:
111+
- name: Log in to GitHub Container Registry
112+
uses: docker/login-action@v3
113+
with:
114+
registry: ${{ env.REGISTRY }}
115+
username: ${{ github.actor }}
116+
password: ${{ secrets.GITHUB_TOKEN }}
117+
118+
- name: Pull all images and compare sizes
119+
run: |
120+
echo "## Docker Image Size Comparison" >> $GITHUB_STEP_SUMMARY
121+
echo "" >> $GITHUB_STEP_SUMMARY
122+
echo "| Variant | Size | Optimization |" >> $GITHUB_STEP_SUMMARY
123+
echo "|---------|------|--------------|" >> $GITHUB_STEP_SUMMARY
124+
125+
# Pull and get sizes
126+
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:single-stage-latest || true
127+
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:two-stage-latest || true
128+
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:three-stage-latest || true
129+
130+
# Get sizes
131+
SINGLE_SIZE=$(docker images --format "{{.Size}}" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:single-stage-latest 2>/dev/null || echo "N/A")
132+
TWO_SIZE=$(docker images --format "{{.Size}}" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:two-stage-latest 2>/dev/null || echo "N/A")
133+
THREE_SIZE=$(docker images --format "{{.Size}}" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:three-stage-latest 2>/dev/null || echo "N/A")
134+
135+
echo "| Single-stage | ${SINGLE_SIZE} | Baseline |" >> $GITHUB_STEP_SUMMARY
136+
echo "| Two-stage | ${TWO_SIZE} | ~40% smaller |" >> $GITHUB_STEP_SUMMARY
137+
echo "| Three-stage | ${THREE_SIZE} | ~40% smaller + better caching |" >> $GITHUB_STEP_SUMMARY

Dockerfile.multistage

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# ============================================================================
1313
# STAGE 1: Builder - Install dependencies
1414
# ============================================================================
15-
FROM rocker/r-ver:4.3.2 AS builder
15+
FROM rocker/r-ver:4.4.3 AS builder
1616

1717
# Install system dependencies needed for package compilation
1818
RUN apt-get update && apt-get install -y \
@@ -49,20 +49,21 @@ COPY app.R app.R
4949
# ============================================================================
5050
# STAGE 2: Runtime - Minimal production image
5151
# ============================================================================
52-
FROM rocker/r-ver:4.3.2
52+
FROM rocker/r-ver:4.4.3
5353

5454
# Install ONLY runtime system dependencies (no build tools)
55-
RUN apt-get update && apt-get install -y \
55+
# Note: Using minimal runtime libraries without -dev packages
56+
RUN apt-get update && apt-get install -y --no-install-recommends \
5657
libcurl4 \
5758
libssl3 \
5859
libxml2 \
5960
libfontconfig1 \
6061
libharfbuzz0b \
6162
libfribidi0 \
6263
libfreetype6 \
63-
libpng16-16 \
64-
libtiff5 \
65-
libjpeg62-turbo \
64+
libpng16-16t64 \
65+
libtiff6 \
66+
libjpeg-turbo8 \
6667
&& rm -rf /var/lib/apt/lists/*
6768

6869
WORKDIR /app

Dockerfile.single-stage

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# 4. Larger final image size (~2GB+)
99
# ============================================================================
1010

11-
FROM rocker/r-ver:4.3.2
11+
FROM rocker/r-ver:4.4.3
1212

1313
# Install system dependencies
1414
RUN apt-get update && apt-get install -y \

Dockerfile.three-stage

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# ============================================================================
2+
# THREE-STAGE DOCKERFILE (ADVANCED OPTIMIZATION)
3+
# ============================================================================
4+
# Architecture based on change frequency and package sources:
5+
#
6+
# BASE: CRAN packages (stable, rarely change) + build tools
7+
# BUILDER: Custom/private packages (change independently from CRAN)
8+
# RUNTIME: Pure application + runtime libraries only (NO build tools)
9+
#
10+
# Key advantages:
11+
# 1. CRAN packages cached separately from custom packages
12+
# 2. Custom package changes don't invalidate CRAN package layer
13+
# 3. Runtime image is completely clean - no build tools whatsoever
14+
# 4. Optimal layer caching based on change frequency
15+
# ============================================================================
16+
17+
# ============================================================================
18+
# STAGE 1: Base - CRAN packages and build environment
19+
# ============================================================================
20+
FROM rocker/r-ver:4.4.3 AS base
21+
22+
# Install system dependencies needed for compilation AND runtime
23+
# Build tools are needed here to compile CRAN packages
24+
RUN apt-get update && apt-get install -y \
25+
# Build tools (needed for CRAN package compilation)
26+
libcurl4-openssl-dev \
27+
libssl-dev \
28+
libxml2-dev \
29+
libfontconfig1-dev \
30+
libharfbuzz-dev \
31+
libfribidi-dev \
32+
libfreetype6-dev \
33+
libpng-dev \
34+
libtiff5-dev \
35+
libjpeg-dev \
36+
&& rm -rf /var/lib/apt/lists/*
37+
38+
WORKDIR /base
39+
40+
# OPTIMIZATION 1: Copy only dependency files first
41+
# This creates a cache-friendly layer that only rebuilds when CRAN dependencies change
42+
COPY renv.lock renv.lock
43+
COPY .Rprofile .Rprofile
44+
COPY renv/activate.R renv/activate.R
45+
COPY renv/settings.json renv/settings.json
46+
47+
# OPTIMIZATION 2: Install renv and restore CRAN packages
48+
# This layer is cached unless renv.lock changes
49+
# CRAN packages change rarely, so this provides excellent caching
50+
RUN R -e "install.packages('renv', repos = 'https://cloud.r-project.org')"
51+
RUN R -e "renv::restore()"
52+
53+
# ============================================================================
54+
# STAGE 2: Builder - Custom/private packages
55+
# ============================================================================
56+
FROM base AS builder
57+
58+
# Inherit all CRAN packages and build tools from base
59+
# Add any additional build dependencies for custom packages here if needed
60+
# RUN apt-get update && apt-get install -y \
61+
# <additional-build-deps> \
62+
# && rm -rf /var/lib/apt/lists/*
63+
64+
WORKDIR /build
65+
66+
# Copy and build custom packages (not on CRAN)
67+
# These might be:
68+
# - Private packages from GitHub/GitLab
69+
# - Internal company packages
70+
# - Packages built from local source
71+
#
72+
# Example (uncomment and modify as needed):
73+
# COPY custom_packages/ custom_packages/
74+
# RUN R CMD INSTALL custom_packages/mypackage
75+
76+
# For now, just inherit from base
77+
# In a real scenario, you'd install custom packages here:
78+
# RUN R -e "remotes::install_github('yourorg/yourpackage')"
79+
80+
# Copy application code
81+
# This is the most frequently changing layer
82+
COPY app.R app.R
83+
84+
# ============================================================================
85+
# STAGE 3: Runtime - Pure application (NO build tools)
86+
# ============================================================================
87+
FROM rocker/r-ver:4.4.3
88+
89+
# Install ONLY runtime system dependencies (no -dev packages, no build tools)
90+
# Note: Using minimal runtime libraries without -dev packages
91+
RUN apt-get update && apt-get install -y --no-install-recommends \
92+
libcurl4 \
93+
libssl3 \
94+
libxml2 \
95+
libfontconfig1 \
96+
libharfbuzz0b \
97+
libfribidi0 \
98+
libfreetype6 \
99+
libpng16-16t64 \
100+
libtiff6 \
101+
libjpeg-turbo8 \
102+
&& rm -rf /var/lib/apt/lists/*
103+
104+
WORKDIR /app
105+
106+
# Copy CRAN packages from base stage
107+
COPY --from=base /base/renv /app/renv
108+
COPY --from=base /base/.Rprofile /app/.Rprofile
109+
COPY --from=base /base/renv.lock /app/renv.lock
110+
111+
# Copy custom packages from builder stage (if any were installed)
112+
# COPY --from=builder /usr/local/lib/R/site-library/mypackage /usr/local/lib/R/site-library/mypackage
113+
114+
# Copy application code from builder
115+
COPY --from=builder /build/app.R /app/app.R
116+
117+
# Expose Shiny port
118+
EXPOSE 3838
119+
120+
# Run the application
121+
CMD ["R", "-e", "shiny::runApp('/app', host = '0.0.0.0', port = 3838)"]
122+
123+
# ============================================================================
124+
# Layer Optimization Strategy:
125+
# ============================================================================
126+
#
127+
# CRAN packages (base): Changes LEAST frequently → Cached most aggressively
128+
# Custom packages (builder): Changes MORE frequently → Independent cache layer
129+
# Application code (runtime): Changes MOST frequently → Doesn't invalidate above
130+
#
131+
# Build flow:
132+
# 1. Base layer compiles CRAN packages (cached unless renv.lock changes)
133+
# 2. Builder adds custom packages (cached unless custom package sources change)
134+
# 3. Runtime copies everything and adds application code
135+
# 4. Code changes only invalidate the final COPY app.R step
136+
#
137+
# ============================================================================
138+
# Build and Run Commands:
139+
# ============================================================================
140+
# Build (three-stage):
141+
# docker build -f Dockerfile.three-stage -t shiny-app:three-stage .
142+
#
143+
# Run:
144+
# docker run -p 3838:3838 shiny-app:three-stage
145+
#
146+
# Verify no build tools in runtime:
147+
# docker run --rm shiny-app:three-stage dpkg -l | grep -E "dev|gcc|g\+\+"
148+
# (should return nothing)
149+
#
150+
# Compare sizes:
151+
# docker images | grep shiny-app
152+
# ============================================================================

docker-compose.yml

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: '3.8'
22

33
services:
4-
# Single-stage build (for comparison)
4+
# Single-stage build (baseline)
55
shiny-single:
66
build:
77
context: .
@@ -11,23 +11,37 @@ services:
1111
ports:
1212
- "3838:3838"
1313
restart: unless-stopped
14-
# Uncomment to run this version
15-
# profiles:
16-
# - single
14+
profiles:
15+
- single
1716

18-
# Multistage build (optimized)
19-
shiny-optimized:
17+
# Two-stage build (optimized)
18+
shiny-two-stage:
2019
build:
2120
context: .
2221
dockerfile: Dockerfile.multistage
23-
image: shiny-app:optimized
24-
container_name: shiny-optimized
22+
image: shiny-app:two-stage
23+
container_name: shiny-two-stage
24+
ports:
25+
- "3838:3838"
26+
restart: unless-stopped
27+
profiles:
28+
- two-stage
29+
30+
# Three-stage build (advanced optimization)
31+
shiny-three-stage:
32+
build:
33+
context: .
34+
dockerfile: Dockerfile.three-stage
35+
image: shiny-app:three-stage
36+
container_name: shiny-three-stage
2537
ports:
2638
- "3838:3838"
2739
restart: unless-stopped
2840
# This is the default service to run
2941

3042
# Usage:
31-
# docker-compose up # Runs optimized version
32-
# docker-compose up shiny-single # Runs single-stage version
33-
# docker-compose down # Stop and remove containers
43+
# docker-compose up # Runs three-stage version (default)
44+
# docker-compose up shiny-single # Runs single-stage version
45+
# docker-compose up shiny-two-stage # Runs two-stage version
46+
# docker-compose up shiny-three-stage # Runs three-stage version (explicit)
47+
# docker-compose down # Stop and remove containers

0 commit comments

Comments
 (0)