├── configs ├── odbc │ ├── odbc.ini │ └── odbcinst.ini ├── shinyproxy │ ├── run │ ├── grid-layout │ │ ├── assets │ │ │ └── img │ │ │ │ ├── logo.png │ │ │ │ └── background.png │ │ ├── app.html │ │ └── index.html │ └── application.yml ├── vscode │ ├── run │ └── User │ │ ├── settings.json │ │ └── snippets │ │ └── markdown.json ├── rstudio │ ├── rserver.conf │ └── run ├── krb │ └── krb5.conf └── start.sh ├── .gitattributes ├── samples ├── _apps │ └── Site Usage │ │ ├── ui │ │ ├── header.R │ │ ├── body.R │ │ └── sidebar.R │ │ ├── app.R │ │ └── server │ │ └── server.R └── _docs │ ├── Jupyter Notebook │ ├── convert.sh │ ├── example.py │ └── example.ipynb │ ├── Languages of RStudio │ ├── users.csv │ └── doc.Rmd │ └── ShinyStudio │ └── README.Rmd ├── .gitignore ├── LICENSE ├── Dockerfile └── README.md /configs/odbc/odbc.ini: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /samples/_apps/Site Usage/ui/header.R: -------------------------------------------------------------------------------- 1 | dashboardHeader(titleWidth = 150, title = 'Site Usage') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | .Rhistory 4 | .Rproj.user 5 | .dockerignore 6 | build.* 7 | run.* 8 | content/ -------------------------------------------------------------------------------- /configs/shinyproxy/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | cd /opt/shinyproxy 4 | 5 | java -jar /opt/shinyproxy/shinyproxy.jar 6 | -------------------------------------------------------------------------------- /samples/_docs/Jupyter Notebook/convert.sh: -------------------------------------------------------------------------------- 1 | 2 | jupyter nbconvert example.ipynb --to html -y --template full 3 | 4 | mv example.html index.html 5 | -------------------------------------------------------------------------------- /configs/shinyproxy/grid-layout/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fresh2dev/ShinyStudio-Image/HEAD/configs/shinyproxy/grid-layout/assets/img/logo.png -------------------------------------------------------------------------------- /configs/shinyproxy/grid-layout/assets/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fresh2dev/ShinyStudio-Image/HEAD/configs/shinyproxy/grid-layout/assets/img/background.png -------------------------------------------------------------------------------- /configs/vscode/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | su - $USER -c 'cd ~ && export SHELL=/bin/bash && /usr/local/bin/code-server --allow-http --no-auth --disable-telemetry' 3 | -------------------------------------------------------------------------------- /configs/rstudio/rserver.conf: -------------------------------------------------------------------------------- 1 | # Server Configuration File 2 | 3 | rsession-which-r=/usr/local/bin/R 4 | auth-none=1 5 | auth-minimum-user-id=0 6 | auth-validate-users=0 7 | www-frame-origin=same 8 | 9 | www-address=0.0.0.0 10 | -------------------------------------------------------------------------------- /configs/rstudio/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | ## load /etc/environment vars first: 3 | for line in $( cat /etc/environment ) ; do export $line ; done 4 | exec rserver --server-daemonize=0 --config-file /etc/rstudio/rserver_custom.conf 5 | -------------------------------------------------------------------------------- /samples/_docs/Languages of RStudio/users.csv: -------------------------------------------------------------------------------- 1 | "id","name","email" 2 | "1","Leanne Graham","Sincere@april.biz" 3 | "2","Ervin Howell","Shanna@melissa.tv" 4 | "3","Clementine Bauch","Nathan@yesenia.net" 5 | "4","Patricia Lebsack","Julianne.OConner@kory.org" 6 | "5","Chelsey Dietrich","Lucio_Hettinger@annie.ca" 7 | -------------------------------------------------------------------------------- /samples/_apps/Site Usage/ui/body.R: -------------------------------------------------------------------------------- 1 | dashboardBody(tabItems(tabItem( 2 | tabName = 'tab_home', 3 | fluidRow( 4 | box(title = 'Users', 5 | width = 6, 6 | plotOutput('plot_users')), 7 | box(title = 'Apps', 8 | width = 6, 9 | plotOutput('plot_apps')) 10 | ), 11 | fluidRow(box( 12 | title = 'Data', 13 | width = 12, 14 | DT::dataTableOutput('datatable') 15 | )) 16 | ))) 17 | -------------------------------------------------------------------------------- /samples/_apps/Site Usage/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(shinydashboard) 3 | library(DT) 4 | 5 | library(httr) 6 | library(data.table) 7 | library(magrittr) 8 | library(ggplot2) 9 | library(lubridate) 10 | 11 | ui <- dashboardPage( 12 | header = source('ui/header.R', local = TRUE)$value, 13 | sidebar = source('ui/sidebar.R', local = TRUE)$value, 14 | body = source('ui/body.R', local = TRUE)$value 15 | ) 16 | 17 | server <- source('server/server.R', local=TRUE)$value 18 | 19 | shinyApp(ui, server) 20 | -------------------------------------------------------------------------------- /configs/vscode/User/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.editor.enablePreview": false, 3 | "markdown.extension.toc.updateOnSave": false, 4 | "files.associations": { 5 | "*.Rmd": "markdown" 6 | }, 7 | "python.venvPath": "/pyenv", 8 | "python.jediEnabled": true, 9 | "python.autoUpdateLanguageServer": false, 10 | "python.dataScience.useDefaultConfigForJupyter": false, 11 | "python.dataScience.searchForJupyter": false, 12 | "python.dataScience.changeDirOnImportExport": false, 13 | "files.hotExit": "off" 14 | } -------------------------------------------------------------------------------- /samples/_apps/Site Usage/ui/sidebar.R: -------------------------------------------------------------------------------- 1 | dashboardSidebar(width = 150, collapsed = TRUE, 2 | sidebarMenu( 3 | id = 'sidebar', 4 | menuItem( 5 | 'Home', 6 | tabName = 'tab_home', 7 | icon = icon('home'), 8 | selected = TRUE 9 | ), 10 | menuItem( 11 | actionButton('btn_refresh', 'Refresh', icon=icon('refresh')) 12 | ) 13 | )) 14 | -------------------------------------------------------------------------------- /configs/odbc/odbcinst.ini: -------------------------------------------------------------------------------- 1 | [ODBC Drivers] 2 | Cloudera ODBC Driver for Impala 64-bit=Installed 3 | UsageCount=1 4 | ODBC Driver 17 for SQL Server=Installed 5 | UsageCount=1 6 | PostgreSQL Unicode=Installed 7 | UsageCount=1 8 | 9 | [Cloudera ODBC Driver for Impala 64-bit] 10 | Description=Cloudera ODBC Driver for Impala (64-bit) 11 | Driver=/opt/cloudera/impalaodbc/lib/64/libclouderaimpalaodbc64.so 12 | UsageCount=1 13 | 14 | [ODBC Driver 17 for SQL Server] 15 | Description=Microsoft ODBC Driver 17 for SQL Server 16 | Driver=/opt/microsoft/msodbcsql17/lib64/libmsodbcsql-17.3.so.1.1 17 | UsageCount=1 18 | 19 | [PostgreSQL Unicode] 20 | Description=PostgreSQL ODBC driver (Unicode version) 21 | Driver=psqlodbcw.so 22 | Setup=libodbcpsqlS.so 23 | Debug=0 24 | CommLog=1 25 | UsageCount=1 -------------------------------------------------------------------------------- /configs/vscode/User/snippets/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Print to console": { 3 | "prefix": "FrontMatter", 4 | "body": [ 5 | "---", 6 | "title: ''", 7 | "subtitle: ''", 8 | "abstract: |", 9 | " A fancy doc.", 10 | "output:", 11 | " html_document:", 12 | " fig_caption: no", 13 | " theme: 'spacelab'", 14 | " highlight: 'haddock'", 15 | " keep_md: yes", 16 | " toc: yes", 17 | " toc_depth: 3", 18 | " toc_float:", 19 | " collapsed: no", 20 | " smooth_scroll: yes", 21 | "---\n" 22 | ], 23 | "description": "Sample front matter for RMarkdown documents." 24 | } 25 | } -------------------------------------------------------------------------------- /configs/krb/krb5.conf: -------------------------------------------------------------------------------- 1 | # http://web.mit.edu/kerberOS/www/krb5-1.5/krb5-1.5.4/doc/krb5-admin/Sample-krb5_002econf-File.html 2 | 3 | [libdefaults] 4 | default_realm = ATHENA.MIT.EDU 5 | dns_lookup_kdc = true 6 | dns_lookup_realm = false 7 | 8 | [realms] 9 | # use "kdc = ..." if realm admins haven't put SRV records into DNS 10 | ATHENA.MIT.EDU = { 11 | kdc = kerberos.mit.edu 12 | kdc = kerberos-1.mit.edu 13 | kdc = kerberos-2.mit.edu:750 14 | admin_server = kerberos.mit.edu 15 | master_kdc = kerberos.mit.edu 16 | default_domain = mit.edu 17 | } 18 | EXAMPLE.COM = { 19 | kdc = kerberos.example.com 20 | kdc = kerberos-1.example.com 21 | admin_server = kerberos.example.com 22 | } 23 | 24 | [domain_realm] 25 | .mit.edu = ATHENA.MIT.EDU 26 | mit.edu = ATHENA.MIT.EDU 27 | n 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /samples/_apps/Site Usage/server/server.R: -------------------------------------------------------------------------------- 1 | parse_influx_series <- function(x) { 2 | df <- x$values %>% lapply(unlist) %>% do.call(rbind, .) %>% data.table() 3 | 4 | setnames(df, unlist(x$columns)) 5 | 6 | df[, time := ymd_hms(time)] 7 | 8 | return(df) 9 | } 10 | 11 | function(input, output, session) { 12 | df <- reactive({ 13 | input$btn_refresh 14 | 15 | qry <- 'select * from event' 16 | 17 | resp <- httr::GET('http://influxdb:8086', 18 | path='query', 19 | query=list(db='shinyproxy_usagestats', q=qry)) 20 | 21 | df <- httr::content(resp)$results[[1]]$series %>% 22 | lapply(parse_influx_series) %>% 23 | rbindlist() 24 | 25 | setorder(df, -time) 26 | 27 | return(df) 28 | }) 29 | 30 | output$plot_users <- renderPlot({ 31 | df_users <- df()[type=='ProxyStart', .(count=.N), by='username'] 32 | 33 | ggplot(df_users, aes(x=reorder(username, count), y=count)) + 34 | geom_bar(stat='identity', fill='#3c8dbc') + 35 | coord_flip() + 36 | xlab(NULL) 37 | }) 38 | 39 | output$plot_apps <- renderPlot({ 40 | df_apps <- df()[type=='ProxyStart', .(count=.N), by='data'] 41 | 42 | ggplot(df_apps, aes(x=reorder(data, count), y=count)) + 43 | geom_bar(stat='identity', fill='#3c8dbc') + 44 | coord_flip() + 45 | xlab(NULL) 46 | }) 47 | 48 | output$datatable <- renderDataTable({ 49 | DT::datatable(df(), options = list(pageLength = 25, lengthMenu = c(10, 25, 50, 100), select=FALSE), filter='top') 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /samples/_docs/Jupyter Notebook/example.py: -------------------------------------------------------------------------------- 1 | #%% [markdown] 2 | 3 | # Use VS code to author Python scripts and easily convert them to Jupyter notebooks. 4 | # 5 | # Afterward, conver the Jupyter notebook (.ipynb) to HTML so it is viewable in Shiny Server. 6 | # 7 | # `jupyter nbconvert *.ipynb --to html -y --template full` 8 | # 9 | # See more: https://code.visualstudio.com/docs/python/jupyter-support 10 | 11 | #%% [markdown] 12 | # Below is a quick demo of the Python library, [ezpq](https://github.com/dm3ll3n/ezpq). 13 | 14 | #%% 15 | 16 | import ezpq 17 | import time 18 | import pandas as pd 19 | 20 | #%% 21 | all_output = list() 22 | 23 | # run three different `ezpq` parallel queues, sequentially. 24 | for qid in [1, 2, 3]: 25 | # each queue will process 5 jobs at a time. 26 | with ezpq.Queue(5, qid='queue_' + str(qid)) as Q: 27 | # submit 20 jobs, each taking exactly one second. 28 | for i in range(20): 29 | lane = i % 5 # lanes handle dependent jobs. 30 | Q.put(time.sleep, args=1, 31 | lane=lane, name='Job '+str(i)) 32 | 33 | # wait for all enqueued jobs to complete. 34 | Q.wait() 35 | 36 | # collect job results 37 | all_output.extend( Q.collect() ) 38 | 39 | print('{} job results.'.format(len(all_output))) 40 | 41 | # Peek at results in a dataframe. 42 | pd.DataFrame( all_output )[['qid', 'id', 'lane', 'runtime']].head() 43 | 44 | #%% 45 | 46 | # Plot queue operations. 47 | ezpq.Plot(all_output).build(facet_by='qid', 48 | color_by='lane', 49 | color_pal=['blue', 'orange', 'green', 50 | 'red', 'purple']) 51 | -------------------------------------------------------------------------------- /configs/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # enter pyenv 4 | source "${VIRTUAL_ENV}/bin/activate" 5 | 6 | # setup $USER now 7 | mv -f /etc/cont-init.d/userconf /tmp/userconf.sh 8 | chmod +x /tmp/userconf.sh 9 | source /tmp/userconf.sh 10 | 11 | site_dir="/home/${USER}/__ShinyStudio__" 12 | if [ -d "$site_dir" ]; then 13 | 14 | # if this is a superadmin, set correct site_dir path. 15 | if [ -d "$site_dir/users" ]; then 16 | if [ -z "${SITE_NAME}" ]; then 17 | SITE_NAME="shinystudio" 18 | fi 19 | site_dir="${site_dir}/sites/${SITE_NAME}" 20 | mkdir -p "$site_dir" 21 | fi 22 | 23 | # create site folders, if necessary, and ensure $USERID owns them. 24 | for d in _apps _docs 25 | do 26 | dir="$site_dir/$d" 27 | if [ ! -d "$dir" ] || [ ! -z "$(find ""$dir"" -maxdepth 0 -empty)" ]; then 28 | [ -d "$dir" ] || mkdir -p "$dir" 29 | cp -R "/srv/shiny-server/$d/." "$dir" 30 | chown -R $USERID:$USERID "$dir" 31 | fi 32 | done 33 | 34 | # first launch setup. 35 | if [ -d "/home/${USER}/__Personal__/.vscode" ] && [ ! -f "/home/${USER}/__Personal__/.vscode/User/settings.json" ]; then 36 | su - $USER -c 'cd ~ && export SHELL=/bin/bash && /setup_vscode.sh' 37 | fi 38 | fi 39 | 40 | # Do this to ensure 'SHINYPROXY_USERNAME' and 'SHINYPROXY_GROUPS' 41 | # are available in the rstudio user's environment. 42 | env | grep "SHINYPROXY" > "/home/${USER}/.Renviron" 43 | 44 | # set non-standard port for Jupyter notebook, for use in VS code. 45 | mkdir -p "/home/${USER}/.jupyter" 46 | 47 | echo "c.NotebookApp.ip = '127.0.0.1' 48 | c.NotebookApp.port = 12345 49 | c.NotebookApp.port_retries = 50 50 | c.NotebookApp.token = '' 51 | c.NotebookApp.open_browser = False 52 | c.NotebookApp.disable_check_xsrf = True" > "/home/${USER}/.jupyter/jupyter_notebook_config.py" 53 | 54 | 55 | # setup .gitconfig for this user. 56 | echo "[user] 57 | name = ${USER} 58 | email = none@none.com" > "/home/${USER}/.gitconfig" 59 | 60 | # make shiny-examples available; read-only. 61 | ln -sf /srv/shiny-server "/home/${USER}/shiny-examples" 62 | 63 | # parse arg $1 to define service to run 64 | svc="$1" 65 | 66 | if [ -z "$svc" ]; then 67 | svc='shinyproxy' 68 | fi 69 | 70 | find /etc/services.d/* -type d -not -name "$svc" | xargs rm -rf 71 | 72 | /init 73 | -------------------------------------------------------------------------------- /configs/shinyproxy/grid-layout/app.html: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 |
40 |
Launching ...
41 |
42 | 43 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /samples/_docs/Languages of RStudio/doc.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "The Languages of RStudio" 3 | subtitle: 'A few of many' 4 | output: 5 | html_document: 6 | self_contained: true 7 | fig_caption: no 8 | theme: 'spacelab' 9 | toc: yes 10 | toc_depth: 3 11 | toc_float: 12 | collapsed: no 13 | smooth_scroll: yes 14 | --- 15 | 16 | ```{r setup, include=FALSE} 17 | knitr::opts_chunk$set(echo = TRUE) 18 | ``` 19 | 20 | This document illustrates the versatility of RStudio by performing the same operation in one document with each of R, Python, Bash, and PowerShell. 21 | 22 | Retrieved data is then loaded into SQLite and queried with SQL. 23 | 24 | Lastly, see how to producer "pretty" data tables, and also how interoperability between R and Python can easily be achieved with the `reticulate` package. 25 | 26 | ## R 27 | 28 | ```{r} 29 | df <- read.csv('users.csv') 30 | 31 | df 32 | ``` 33 | 34 | ## Python 35 | 36 | ```{python} 37 | import pandas as pd 38 | 39 | df = pd.read_csv('users.csv') 40 | 41 | df 42 | ``` 43 | 44 | ## Bash 45 | 46 | ```{bash} 47 | input="users.csv" 48 | while IFS=',' read -r f1 f2 f3 49 | do 50 | echo "$f1 $f2 $f3" 51 | done < "$input" 52 | ``` 53 | 54 | [source](https://www.cyberciti.biz/faq/linux-unix-appleosx-bsd-shell-parse-text-file/) 55 | 56 | ## PowerShell 57 | 58 | ```{bash, engine.path='/usr/bin/pwsh'} 59 | Import-Csv 'users.csv' 60 | ``` 61 | 62 | ## SQL 63 | 64 | First, create the SQLite db, then load our dataframe `df` into it. 65 | 66 | ```{r} 67 | library(RSQLite) 68 | 69 | con <- dbConnect(RSQLite::SQLite(), dbname=':memory:') 70 | 71 | dest_table <- 'df_table' 72 | 73 | dbWriteTable(con, name=dest_table, value=df) 74 | ``` 75 | 76 | Query the new table in a SQL code chunk. 77 | 78 | ```{sql, connection=con} 79 | SELECT * 80 | FROM ?dest_table 81 | LIMIT 5 82 | ``` 83 | 84 | ```{r, echo=FALSE} 85 | dbDisconnect(con) 86 | ``` 87 | 88 | SQL code chunks output a pretty HTML table by default. Keep reading to see how to present your R/Python data frame(s) in a pretty table. 89 | 90 | ## Pretty Tables 91 | 92 | The R package `knitr` provides the function `kable` to produce a HTML table from an R data frame. 93 | 94 | Similarly, the R package `DT` provides the function `datatable` as an interface to the popular [DataTables JavaScript library](https://datatables.net/), which presents data frames in an interactive HTML widget. 95 | 96 | ```{r} 97 | library(knitr) # for kable 98 | library(DT) # for datatable 99 | ``` 100 | 101 | ### Kable 102 | 103 | *R* 104 | 105 | ```{r} 106 | knitr::kable(df) 107 | ``` 108 | 109 | *Python* 110 | 111 | > Simply load the `reticulate` package to achieve interoperability between R and Python. Python variables can be accessed from R using py$_var_. 112 | 113 | ```{r} 114 | library(reticulate) 115 | 116 | knitr::kable(py$df) 117 | ``` 118 | 119 | ### DataTables 120 | 121 | *R* 122 | 123 | ```{r} 124 | DT::datatable(df) 125 | ``` 126 | 127 | *Python* 128 | 129 | ```{r} 130 | DT::datatable(py$df) 131 | ``` 132 | -------------------------------------------------------------------------------- /configs/shinyproxy/grid-layout/index.html: -------------------------------------------------------------------------------- 1 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 81 | 82 | 83 |
84 | 85 |
86 |
87 |
88 |
89 |
90 | 91 | 92 | 93 |

94 | 95 |

96 | 97 |

98 |
99 |
100 |
101 |
102 |
103 |
104 | 105 | 106 | -------------------------------------------------------------------------------- /configs/shinyproxy/application.yml: -------------------------------------------------------------------------------- 1 | # BASIC AUTH 2 | proxy: 3 | ### PERSONALIZATION ### 4 | title: ShinyStudio 5 | hide-navbar: false 6 | logo-url: file:///opt/shinyproxy/templates/grid-layout/assets/img/logo.png 7 | favicon-path: /opt/shinyproxy/templates/grid-layout/assets/img/logo.png 8 | template-path: ./templates/grid-layout 9 | ### AUTHENTICATION ### 10 | admin-groups: ['admins'] 11 | authentication: simple 12 | users: 13 | - name: ${USER} 14 | password: ${PASSWORD} 15 | groups: admins 16 | ### DANGER ZONE ### 17 | port: 8080 # don't change! 18 | landing-page: / 19 | container-wait-time: 30000 20 | heartbeat-rate: 15000 21 | heartbeat-timeout: 120000 22 | docker: 23 | internal-networking: true 24 | specs: 25 | - id: reports 26 | display-name: Apps & Reports 27 | logo-url: 'fas fa-chart-line' 28 | container-image: dm3ll3n/shinystudio 29 | container-cmd: ["/start.sh", "shiny-server"] 30 | container-network: shinystudio-net 31 | container-volumes: 32 | - "${CONTENT_PATH}/sites/${SITE_NAME}/_apps:/srv/shiny-server:z" 33 | - "${SITE_NAME}_r_libraries:/r-libs" 34 | - "${SITE_NAME}_py_environment:/pyenv" 35 | - "${SITE_NAME}_pwsh_modules:/home/#{proxy.userId}/.local/share/powershell/Modules" 36 | access-groups: [ 'superadmins', 'admins', 'readers' ] 37 | container-env: 38 | USER: "#{proxy.userId}" 39 | USERID: ${USERID} 40 | - id: documents 41 | display-name: Documents 42 | logo-url: 'fas fa-file-alt' 43 | container-image: dm3ll3n/shinystudio 44 | container-cmd: ["/start.sh", "shiny-server"] 45 | container-network: shinystudio-net 46 | container-volumes: 47 | - "${CONTENT_PATH}/sites/${SITE_NAME}/_docs:/srv/shiny-server:z" 48 | - "${SITE_NAME}_r_libraries:/r-libs" 49 | - "${SITE_NAME}_py_environment:/pyenv" 50 | - "${SITE_NAME}_pwsh_modules:/home/#{proxy.userId}/.local/share/powershell/Modules" 51 | access-groups: [ 'superadmins', 'admins', 'readers' ] 52 | container-env: 53 | USER: "#{proxy.userId}" 54 | USERID: ${USERID} 55 | - id: personal 56 | display-name: Personal 57 | logo-url: 'far fa-folder-open' 58 | container-image: dm3ll3n/shinystudio 59 | container-cmd: ["/start.sh", "shiny-server"] 60 | container-network: shinystudio-net 61 | container-volumes: 62 | - "${CONTENT_PATH}/users/#{proxy.userId}:/srv/shiny-server:z" 63 | - "${SITE_NAME}_r_libraries:/r-libs" 64 | - "${SITE_NAME}_py_environment:/pyenv" 65 | - "${SITE_NAME}_pwsh_modules:/home/#{proxy.userId}/.local/share/powershell/Modules" 66 | access-groups: [ 'superadmins', 'admins', 'readers' ] 67 | container-env: 68 | USER: "#{proxy.userId}" 69 | USERID: ${USERID} 70 | - id: rstudio 71 | display-name: RStudio 72 | logo-url: 'fab fa-r-project' 73 | container-image: dm3ll3n/shinystudio 74 | container-cmd: ["/start.sh", "rstudio"] 75 | container-network: shinystudio-net 76 | container-volumes: 77 | - "${CONTENT_PATH}/sites/${SITE_NAME}:/home/#{proxy.userId}/__ShinyStudio__:z" 78 | - "${CONTENT_PATH}/users/#{proxy.userId}:/home/#{proxy.userId}/__Personal__:z" 79 | - "${CONTENT_PATH}/users/#{proxy.userId}/.rstudio:/home/#{proxy.userId}/.rstudio/monitored/user-settings:z" 80 | - "${SITE_NAME}_r_libraries:/r-libs" 81 | - "${SITE_NAME}_py_environment:/pyenv" 82 | - "${SITE_NAME}_pwsh_modules:/home/#{proxy.userId}/.local/share/powershell/Modules" 83 | container-env: 84 | USER: "#{proxy.userId}" 85 | USERID: ${USERID} 86 | description: Full Screen 87 | port: 8787 88 | access-groups: [ 'admins' ] 89 | - id: vscode 90 | display-name: Visual Studio Code 91 | logo-url: 'fas fa-terminal' 92 | container-image: dm3ll3n/shinystudio 93 | container-cmd: ["/start.sh", "vscode"] 94 | container-network: shinystudio-net 95 | container-volumes: 96 | - "${CONTENT_PATH}/sites/${SITE_NAME}:/home/#{proxy.userId}/__ShinyStudio__:z" 97 | - "${CONTENT_PATH}/users/#{proxy.userId}:/home/#{proxy.userId}/__Personal__:z" 98 | - "${CONTENT_PATH}/users/#{proxy.userId}/.vscode:/home/#{proxy.userId}/.local/share/code-server:z" 99 | - "${SITE_NAME}_r_libraries:/r-libs" 100 | - "${SITE_NAME}_py_environment:/pyenv" 101 | - "${SITE_NAME}_pwsh_modules:/home/#{proxy.userId}/.local/share/powershell/Modules" 102 | container-env: 103 | USER: "#{proxy.userId}" 104 | USERID: ${USERID} 105 | description: Full Screen 106 | port: 8443 107 | access-groups: [ 'admins' ] 108 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rocker/verse:3.6.1 2 | 3 | LABEL maintainer="dm3ll3n@gmail.com" 4 | 5 | # essential vars 6 | ENV DISABLE_AUTH true 7 | ENV R_LIBS_USER /r-libs 8 | ENV APPLICATION_LOGS_TO_STDOUT false 9 | 10 | # add shiny immediately and expose port 3838. 11 | RUN export ADD=shiny && bash /etc/cont-init.d/add 12 | 13 | RUN apt-get update && \ 14 | apt-get install -y apt-transport-https && \ 15 | apt-get install -y curl nano 16 | 17 | # install Java 8 and ShinyProxy 18 | RUN apt-get install -y openjdk-8-jdk-headless && \ 19 | mkdir -p /opt/shinyproxy && \ 20 | wget https://www.shinyproxy.io/downloads/shinyproxy-2.3.0.jar -O /opt/shinyproxy/shinyproxy.jar 21 | 22 | COPY configs/shinyproxy/grid-layout /opt/shinyproxy/templates/grid-layout 23 | COPY configs/shinyproxy/application.yml /opt/shinyproxy/application.yml 24 | 25 | # create shared /r-libs directory and ensure it's writeable by all. 26 | RUN mkdir /r-libs && \ 27 | echo ".libPaths( c( '/r-libs', .libPaths() ) )" >> /usr/local/lib/R/etc/Rprofile.site 28 | 29 | # install R packages 30 | # rmarkdown 1.12 does not display floating TOC; downgrade to 1.11. 31 | RUN R -e "install.packages(c('reticulate', 'png', 'DBI', 'odbc', 'shinydashboard', 'flexdashboard', 'shinycssloaders', 'DT', 'visNetwork', 'networkD3'))" && \ 32 | R -e "install.packages('https://cran.r-project.org/src/contrib/Archive/rmarkdown/rmarkdown_1.11.tar.gz', repos=NULL)" 33 | 34 | COPY samples /srv/shiny-server 35 | RUN mkdir -p /srv/shiny-server/_apps && \ 36 | git clone https://github.com/dm3ll3n/Shiny-GEM /srv/shiny-server/_apps/Shiny-GEM && \ 37 | Rscript '/srv/shiny-server/_apps/Shiny-GEM/install-requirements.R' && \ 38 | chmod -R 777 /r-libs 39 | 40 | # setup python 41 | ENV VIRTUAL_ENV /pyenv 42 | RUN apt-get update && \ 43 | apt-get install -y python3-pip python3-venv libpython-dev libpython3-dev python-dev python3-dev && \ 44 | python3 -m venv "${VIRTUAL_ENV}" && \ 45 | chmod -R 777 "${VIRTUAL_ENV}" && \ 46 | "${VIRTUAL_ENV}/bin/activate" 47 | 48 | # install python packages 49 | ENV PATH "${VIRTUAL_ENV}/bin:${PATH}" 50 | RUN echo "export PATH=\"${VIRTUAL_ENV}/bin:\${PATH}\"" >> /etc/profile && \ 51 | pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org --upgrade pip && \ 52 | pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org wheel && \ 53 | pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org \ 54 | Cython numpy matplotlib pandas tqdm ezpq paramiko requests pylint jupyter && \ 55 | apt-get install -y python3-tk && \ 56 | pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org plotnine 57 | 58 | # install pwsh 59 | # https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux 60 | RUN apt-get install -y libc6 libgcc1 libgssapi-krb5-2 liblttng-ust0 libstdc++6 libcurl3 libunwind8 libuuid1 zlib1g libssl1.0.2 libicu57 && \ 61 | wget https://github.com/PowerShell/PowerShell/releases/download/v6.2.3/powershell_6.2.3-1.debian.9_amd64.deb -O /tmp/pwsh.deb && \ 62 | dpkg -i /tmp/pwsh.deb && \ 63 | rm -f /tmp/pwsh.deb && \ 64 | pwsh -c "Install-Module SqlServer -Force" 65 | 66 | # install VS code-server 67 | RUN wget https://github.com/cdr/code-server/releases/download/1.1156-vsc1.33.1/code-server1.1156-vsc1.33.1-linux-x64.tar.gz -O /tmp/vs-code-server.tar.gz && \ 68 | mkdir /tmp/vs-code-server && \ 69 | tar -xzf /tmp/vs-code-server.tar.gz --strip 1 --directory /tmp/vs-code-server && \ 70 | mv -f /tmp/vs-code-server/code-server /usr/local/bin/code-server && \ 71 | rm -rf /tmp/vs-code-server.tar.gz && \ 72 | mkdir /code-server-template && \ 73 | code-server --user-data-dir /code-server-template --install-extension ms-python.python && \ 74 | code-server --user-data-dir /code-server-template --install-extension ms-vscode.powershell && \ 75 | # code-server --user-data-dir /code-server-template --install-extension ms-mssql.mssql && \ 76 | code-server --user-data-dir /code-server-template --install-extension yzhang.markdown-all-in-one && \ 77 | echo '#!/usr/bin/env bash' > '/setup_vscode.sh' && \ 78 | echo 'cp -Rn /code-server-template/* ~/.local/share/code-server' >> '/setup_vscode.sh' && \ 79 | chmod 555 '/setup_vscode.sh' && \ 80 | # unsure why this is necessary, but it solves a fatal 'file not found' error. 81 | mkdir -p /src/packages/server/build/web && \ 82 | echo '' > /src/packages/server/build/web/index.html 83 | 84 | COPY configs/vscode/User/settings.json /code-server-template/User/settings.json 85 | COPY configs/vscode/User/snippets /code-server-template/User/snippets 86 | 87 | # install kerberos 88 | RUN export DEBIAN_FRONTEND=noninteractive && \ 89 | apt-get install -y krb5-user 90 | 91 | # install SQL Server odbc driver 92 | RUN apt-get install -y unixodbc && \ 93 | wget https://packages.microsoft.com/debian/9/prod/pool/main/m/msodbcsql17/msodbcsql17_17.3.1.1-1_amd64.deb -O /tmp/msodbcsql.deb && \ 94 | ACCEPT_EULA=Y dpkg -i /tmp/msodbcsql.deb && \ 95 | rm -f /tmp/msodbcsql.deb 96 | 97 | # install PostgreSQL odbc driver 98 | RUN apt-get install -y odbc-postgresql 99 | 100 | # install cloudera odbc driver 101 | RUN wget https://downloads.cloudera.com/connectors/ClouderaImpala_ODBC_2.6.2.1002/Debian/clouderaimpalaodbc_2.6.2.1002-2_amd64.deb -O /tmp/clouderaimpalaodbc_amd64.deb && \ 102 | dpkg -i /tmp/clouderaimpalaodbc_amd64.deb && \ 103 | rm -f /tmp/clouderaimpalaodbc_amd64.deb 104 | 105 | # custom configs 106 | COPY configs/rstudio/rserver.conf /etc/rstudio/rserver_custom.conf 107 | 108 | COPY configs/odbc/odbcinst.ini /etc/odbcinst.ini 109 | COPY configs/odbc/odbc.ini /etc/odbc.ini 110 | 111 | COPY configs/krb/krb5.conf /etc/krb5.conf 112 | ENV KRB5_CONFIG /etc/krb5.conf 113 | 114 | # copy custom run commands. 115 | COPY configs/rstudio/run /etc/services.d/rstudio/run 116 | COPY configs/vscode/run /etc/services.d/vscode/run 117 | COPY configs/shinyproxy/run /etc/services.d/shinyproxy/run 118 | 119 | # copy custom start command and make it executable. 120 | COPY configs/start.sh /start.sh 121 | RUN chmod +x /start.sh 122 | 123 | CMD [ "/start.sh", "shinyproxy" ] 124 | -------------------------------------------------------------------------------- /samples/_docs/ShinyStudio/README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: ShinyStudio 3 | subtitle: 'ShinyProxy + RStudio + Shiny + VS Code for teams, in a browser.' 4 | output: 5 | html_document: 6 | self_contained: true 7 | fig_caption: no 8 | theme: 'spacelab' 9 | highlight: 'haddock' 10 | toc: yes 11 | toc_depth: 3 12 | toc_float: 13 | collapsed: no 14 | smooth_scroll: yes 15 | md_document: 16 | variant: gfm 17 | toc: true 18 | toc_depth: 3 19 | --- 20 | 21 | ## Overview 22 | 23 | ![](https://i.imgur.com/rtd29qCh.png) 24 | 25 | The ShinyStudio project is an orchestration of various open-source solutions with the goal of providing: 26 | 27 | * a secured, collaborative development environment for R, Python, PowerShell, and more. 28 | * a secured, convenient way to share apps and documents written in Shiny, RMarkdown, plain Markdown, or HTML. 29 | * easily reproducible, cross-platform setup leveraging Docker for all components. 30 | 31 | ![](https://i.imgur.com/qc7bL1I.gif) 32 | 33 | ![](https://i.imgur.com/PRDW25E.png) 34 | 35 | There are two distributions of ShinyStudio, the *image* and the *stack*, explained below. 36 | 37 | ### ShinyStudio Image 38 | 39 | The ShinyStudio image, hosted on [DockerHub](https://hub.docker.com/r/dm3ll3n/shinystudio), builds upon the [Rocker project](https://www.rocker-project.org/) to include: 40 | 41 | - [ShinyProxy](https://www.shinyproxy.io/) 42 | - [RStudio Server](https://www.rstudio.com/) 43 | - [VS Code](https://code.visualstudio.com/), modified by [Coder.com](https://coder.com/) 44 | - [Shiny Server](https://shiny.rstudio.com/) 45 | 46 | The image is great for a personal instance, a quick demo, or the building blocks for a very customized setup. 47 | 48 | [Get Started with the Image](#image) 49 | 50 | ![ShinyStudio](https://i.imgur.com/FIzE0d7.png) 51 | 52 | ### ShinyStudio Stack 53 | 54 | The ShinyStudio stack builds upon the image to incorporate: 55 | 56 | - [NGINX](https://www.nginx.com/) with HTTPS enabled. 57 | - [InfluxDB](https://www.influxdata.com/) for monitoring site usage. 58 | 59 | Each component of the stack is run in a Docker container for reproducibility, scalability, and security. Only the NGINX port is exposed on the host system; all communication between ShinyProxy and other components happens inside an isolated Docker network. 60 | 61 | [Get Started with the Stack](#stack) 62 | 63 | ![](https://i.imgur.com/RsLeueG.png) 64 | 65 | ## Getting Started 66 | 67 | The setup has been verified to work on each of [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) (for Linux) and [Docker Desktop](https://www.docker.com/products/docker-desktop) (for Mac and Windows). 68 | 69 | > Note: when upgrading ShinyStudio, please setup from scratch and migrate existing content/settings afterward. 70 | 71 | > Note: Setup must be run as a non-root user. 72 | 73 | ### Image 74 | 75 | To download and run the ShinyStudio image from [DockerHub](https://hub.docker.com/r/dm3ll3n/shinystudio), first, create a docker network named `shinystudio-net`: 76 | 77 | ```text 78 | docker network create shinystudio-net 79 | ``` 80 | 81 | Then, execute `docker run` in the terminal for your OS: 82 | 83 | * Bash (Linux/Mac) 84 | 85 | ``` text 86 | docker run -d --restart always --name shinyproxy \ 87 | --network shinystudio-net \ 88 | -v /var/run/docker.sock:/var/run/docker.sock \ 89 | -e USERID=$USERID \ 90 | -e USER=$USER \ 91 | -e PASSWORD=password \ 92 | -e CONTENT_PATH="${HOME}/ShinyStudio" \ 93 | -e SITE_NAME=shinystudio \ 94 | -p 8080:8080 \ 95 | dm3ll3n/shinystudio 96 | ``` 97 | 98 | * PowerShell (Windows) 99 | 100 | ```text 101 | docker run -d --restart always --name shinyproxy ` 102 | --network shinystudio-net ` 103 | -v /var/run/docker.sock:/var/run/docker.sock ` 104 | -e USERID=1000 ` 105 | -e USER=$env:USERNAME ` 106 | -e PASSWORD=password ` 107 | -e CONTENT_PATH="/host_mnt/c/Users/$env:USERNAME/ShinyStudio" ` 108 | -e SITE_NAME=shinystudio ` 109 | -p 8080:8080 ` 110 | dm3ll3n/shinystudio 111 | ``` 112 | 113 | > Notice the unique form of the path for the `CONTENT_PATH` variable in the Windows setup. 114 | 115 | Once complete, open a web browser and navigate to `http://:8080`. Log in with your username and the password `password`. 116 | 117 | ### Stack 118 | 119 | The *stack* distribution of ShinyStudio is delivered through the [GitHub repo](https://github.com/dm3ll3n/ShinyStudio) and introduces two additional requirements: 120 | 121 | * [docker-compose](https://docs.docker.com/compose/install/) (ships with Docker Desktop) 122 | * [Git](https://git-scm.com/downloads) 123 | 124 | HTTPS is configured by default, so SSL/TLS certs are required in order for the stack to operate. Use the provided script `certify.sh` (`certify.ps1` for Windows) to create a self-signed certificate, or to request one from LetsEncrypt (more on that). 125 | 126 | #### Minimal setup: 127 | 128 | ```text 129 | # copy the setup files. 130 | git clone https://github.com/dm3ll3n/ShinyStudio 131 | 132 | # enter the directory. 133 | cd ShinyStudio 134 | 135 | # run certify to generate self-signed cert. 136 | ./certify.[sh/ps1] 137 | ``` 138 | 139 | Now, browse to `http://` (e.g., `http://localhost`) to access ShinyStudio. On first launch, you will need to accept the warning about an untrusted certificate. See the customized setup to see how to request a trusted cert from LetsEncrypt. 140 | 141 | The default logins are below. See the customized setup to see how to add/remove accounts. 142 | 143 | | **username** | **password** | 144 | |:------------:|:------------:| 145 | | user | user | 146 | | admin | admin | 147 | | superadmin | superadmin | 148 | 149 | 150 | #### Customized setup: 151 | 152 | There are three files essential to a customized configuration: 153 | 154 | 1. `.env` 155 | 156 | > The docker-compose environment file. The project name, content path, and HTTP ports can be changed here. 157 | 158 | Note that Docker volume names are renamed along with the project name, so be prepared to migrate or recreate data stored in Docker volumes when changing the project name. 159 | 160 | 2. `application.yml` 161 | 162 | > The ShinyProxy config file. Users can be added/removed here. Other configurations are available too, such as the site title and the ability to provide a non-standard landing page. 163 | 164 | Using the provided template, you can assign users to the following groups with tiered access: 165 | 166 | - **readers**: can only view content from "Apps & Reports", "Documents", and "Personal". 167 | - **admins**: can view all site content and develop content with RStudio and VS Code. 168 | - **superadmins**: can view and develop site content across multiple instances of ShinyStudio. Can also manage *all* user files. 169 | 170 | Review the [ShinyProxy configuration documentation](https://www.shinyproxy.io/configuration/) for all options. 171 | 172 | 3. `nginx.conf` 173 | 174 | > The NGINX config file. Defines the accepted site name and what ports to listen on. 175 | 176 | If you change the ports here, you must also change the ports defined in the `.env` file. Also, if you change the domain name, you must provide/generate a new certificate for it. 177 | 178 | 4. `certify.[sh/ps1]` 179 | 180 | > The script used to generate a self-signed cert, or to request a trusted cert from LetsEncrypt. 181 | 182 | With no parameters, `certify` generates a self-signed cert for `example.com` (the default domain name defined in `nginx.conf`). 183 | 184 | To generate a self-signed cert with another domain name, first edit the domain name in `nginx.conf`. Afterward, generate a new cert with: 185 | 186 | ``` 187 | ./certify.sh 188 | 189 | # e.g., ./certify.sh www.shinystudio.com 190 | ``` 191 | 192 | If your server is accessible from the web, you can request a trusted certificate from LetsEncrypt. First, edit `nginx.conf` with your domain name, then request a new cert from LetsEncrypt like so: 193 | 194 | ``` 195 | ./certify.sh 196 | 197 | # e.g., ./certify.sh www.shinystudio.com donald@email.com 198 | ``` 199 | 200 | CertBot, included in the stack, will automatically renew your LetsEncrypt certificate. 201 | 202 | To manage the services in the stack, use the native docker-compose commands, e.g.: 203 | 204 | ``` 205 | # stop all services. 206 | docker-compose down 207 | 208 | # start all services. 209 | docker-compose up -d 210 | ``` 211 | 212 | ## Develop 213 | 214 | Open either RStudio or VS Code and notice two important directories: 215 | 216 | - \_\_ShinyStudio\_\_ 217 | - \_\_Personal\_\_ 218 | 219 | > Files must be saved in either of these two directories in order to persist between sessions. 220 | 221 | ![](https://i.imgur.com/ac7iKDHh.png) 222 | 223 | These two folders are shared between instances RStudio, VS Code, and Shiny Server. So, creating new content is as simple as saving a file to the appropriate directory. 224 | 225 | ![](https://i.imgur.com/lAuTMgBh.png) 226 | 227 | ## Tools 228 | 229 | The ShinyStudio image comes with... 230 | 231 | - R 232 | - Python 3 233 | - PowerShell 234 | 235 | ...and ODBC drivers for: 236 | 237 | - SQL Server 238 | - PostgresSQL 239 | - Cloudera Impala. 240 | 241 | These are persistent because they are built into the image. 242 | 243 | | | Persistent | 244 | |----------------------------:|:----------:| 245 | | \_\_ShinyStudio__ directory | Yes | 246 | | \_\_Personal__ directory | Yes | 247 | | Other directories | **No** | 248 | | R Libraries | Yes | 249 | | Python Packages | Yes | 250 | | PowerShell Modules | Yes | 251 | | RStudio User Settings | Yes | 252 | | VS Code User Settings | Yes | 253 | | Installed Apps | **No** | 254 | | Installed Drivers | **No** | 255 | 256 | 257 | ## References 258 | 259 | * https://www.shinyproxy.io/ 260 | * https://www.rocker-project.org/ 261 | * https://telethonkids.wordpress.com/2019/02/08/deploying-an-r-shiny-app-with-docker/ 262 | * https://appsilon.com/alternatives-to-scaling-shiny 263 | * https://github.com/wmnnd/nginx-certbot 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShinyStudio 2 | 3 | ## *A Docker orchestration of open-source solutions to facilitate secure, collaborative development.* 4 | 5 | - [Overview](#overview) 6 | - [ShinyStudio Image](#shinystudio-image) 7 | - [ShinyStudio Stack](#shinystudio-stack) 8 | - [Getting Started](#getting-started) 9 | - [Image](#image) 10 | - [Stack](#stack) 11 | - [Develop](#develop) 12 | - [Tools](#tools) 13 | - [References](#references) 14 | 15 | ## Overview 16 | 17 | ![](https://i.imgur.com/rtd29qCh.png) 18 | 19 | The ShinyStudio project is an orchestration of various open-source 20 | solutions with the goal of providing: 21 | 22 | - a secured, collaborative development environment for R, Python, 23 | PowerShell, and more. 24 | - a secured, convenient way to share apps and documents written in 25 | Shiny, RMarkdown, plain Markdown, or HTML. 26 | - easily reproducible, cross-platform setup leveraging Docker for all 27 | components. 28 | 29 | ![](https://i.imgur.com/qc7bL1I.gif) 30 | 31 | ![](https://i.imgur.com/PRDW25E.png) 32 | 33 | There are two distributions of ShinyStudio, the *image* and the *stack*, 34 | explained below. 35 | 36 | ### ShinyStudio Image 37 | 38 | The ShinyStudio image, hosted on 39 | [DockerHub](https://hub.docker.com/r/dm3ll3n/shinystudio), builds upon 40 | the [Rocker project](https://www.rocker-project.org/) to include: 41 | 42 | - [ShinyProxy](https://www.shinyproxy.io/) 43 | - [RStudio Server](https://www.rstudio.com/) 44 | - [VS Code](https://code.visualstudio.com/), modified by 45 | [Coder.com](https://coder.com/) 46 | - [Shiny Server](https://shiny.rstudio.com/) 47 | 48 | The image is great for a personal instance, a quick demo, or the 49 | building blocks for a very customized setup. 50 | 51 | [Get Started with the Image](#image) 52 | 53 | ![ShinyStudio](https://i.imgur.com/FIzE0d7.png) 54 | 55 | ### ShinyStudio Stack 56 | 57 | The ShinyStudio stack builds upon the image to incorporate: 58 | 59 | - [NGINX](https://www.nginx.com/) with HTTPS enabled. 60 | - [InfluxDB](https://www.influxdata.com/) for monitoring site usage. 61 | 62 | Each component of the stack is run in a Docker container for 63 | reproducibility, scalability, and security. Only the NGINX port is 64 | exposed on the host system; all communication between ShinyProxy and 65 | other components happens inside an isolated Docker network. 66 | 67 | [Get Started with the Stack](#stack) 68 | 69 | ![](https://i.imgur.com/RsLeueG.png) 70 | 71 | ## Getting Started 72 | 73 | The setup has been verified to work on each of 74 | [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) (for 75 | Linux) and [Docker 76 | Desktop](https://www.docker.com/products/docker-desktop) (for Mac and 77 | Windows). 78 | 79 | > Note: when upgrading ShinyStudio, please setup from scratch and 80 | > migrate existing content/settings afterward. 81 | 82 | > Note: Setup must be run as a non-root user. 83 | 84 | ### Image 85 | 86 | To download and run the ShinyStudio image from 87 | [DockerHub](https://hub.docker.com/r/dm3ll3n/shinystudio), first, create 88 | a docker network named `shinystudio-net`: 89 | 90 | ``` text 91 | docker network create shinystudio-net 92 | ``` 93 | 94 | Then, execute `docker run` in the terminal for your OS: 95 | 96 | - Bash (Linux/Mac) 97 | 98 | 99 | 100 | ``` text 101 | docker run -d --restart always --name shinyproxy \ 102 | --network shinystudio-net \ 103 | -v /var/run/docker.sock:/var/run/docker.sock \ 104 | -e USERID=$USERID \ 105 | -e USER=$USER \ 106 | -e PASSWORD=password \ 107 | -e CONTENT_PATH="${HOME}/ShinyStudio" \ 108 | -e SITE_NAME=shinystudio \ 109 | -p 8080:8080 \ 110 | dm3ll3n/shinystudio 111 | ``` 112 | 113 | - PowerShell (Windows) 114 | 115 | 116 | 117 | ``` text 118 | docker run -d --restart always --name shinyproxy ` 119 | --network shinystudio-net ` 120 | -v /var/run/docker.sock:/var/run/docker.sock ` 121 | -e USERID=1000 ` 122 | -e USER=$env:USERNAME ` 123 | -e PASSWORD=password ` 124 | -e CONTENT_PATH="/host_mnt/c/Users/$env:USERNAME/ShinyStudio" ` 125 | -e SITE_NAME=shinystudio ` 126 | -p 8080:8080 ` 127 | dm3ll3n/shinystudio 128 | ``` 129 | 130 | > Notice the unique form of the path for the `CONTENT_PATH` variable in 131 | > the Windows setup. 132 | 133 | Once complete, open a web browser and navigate to 134 | `http://:8080`. Log in with your username and the password 135 | `password`. 136 | 137 | ### Stack 138 | 139 | The *stack* distribution of ShinyStudio is delivered through the [GitHub 140 | repo](https://github.com/dm3ll3n/ShinyStudio) and introduces two 141 | additional requirements: 142 | 143 | - [docker-compose](https://docs.docker.com/compose/install/) (ships 144 | with Docker Desktop) 145 | - [Git](https://git-scm.com/downloads) 146 | 147 | HTTPS is configured by default, so SSL/TLS certs are required in order 148 | for the stack to operate. Use the provided script `certify.sh` 149 | (`certify.ps1` for Windows) to create a self-signed certificate, or to 150 | request one from LetsEncrypt (more on that). 151 | 152 | #### Minimal setup: 153 | 154 | ``` text 155 | # copy the setup files. 156 | git clone https://github.com/dm3ll3n/ShinyStudio 157 | 158 | # enter the directory. 159 | cd ShinyStudio 160 | 161 | # run certify to generate self-signed cert. 162 | ./certify.[sh/ps1] 163 | ``` 164 | 165 | Now, browse to `http://` (e.g., `http://localhost`) to access 166 | ShinyStudio. On first launch, you will need to accept the warning about 167 | an untrusted certificate. See the customized setup to see how to request 168 | a trusted cert from LetsEncrypt. 169 | 170 | The default logins are below. See the customized setup to see how to 171 | add/remove accounts. 172 | 173 | | **username** | **password** | 174 | | :----------: | :----------: | 175 | | user | user | 176 | | admin | admin | 177 | | superadmin | superadmin | 178 | 179 | #### Customized setup: 180 | 181 | There are three files essential to a customized configuration: 182 | 183 | 1. `.env` 184 | 185 | > The docker-compose environment file. The project name, content path, 186 | > and HTTP ports can be changed here. 187 | 188 | Note that Docker volume names are renamed along with the project name, 189 | so be prepared to migrate or recreate data stored in Docker volumes when 190 | changing the project name. 191 | 192 | 2. `application.yml` 193 | 194 | > The ShinyProxy config file. Users can be added/removed here. Other 195 | > configurations are available too, such as the site title and the 196 | > ability to provide a non-standard landing page. 197 | 198 | Using the provided template, you can assign users to the following 199 | groups with tiered access: 200 | 201 | - **readers**: can only view content from “Apps & Reports”, 202 | “Documents”, and “Personal”. 203 | - **admins**: can view all site content and develop content with 204 | RStudio and VS Code. 205 | - **superadmins**: can view and develop site content across multiple 206 | instances of ShinyStudio. Can also manage *all* user files. 207 | 208 | Review the [ShinyProxy configuration 209 | documentation](https://www.shinyproxy.io/configuration/) for all 210 | options. 211 | 212 | 3. `nginx.conf` 213 | 214 | > The NGINX config file. Defines the accepted site name and what ports 215 | > to listen on. 216 | 217 | If you change the ports here, you must also change the ports defined in 218 | the `.env` file. Also, if you change the domain name, you must 219 | provide/generate a new certificate for it. 220 | 221 | 4. `certify.[sh/ps1]` 222 | 223 | > The script used to generate a self-signed cert, or to request a 224 | > trusted cert from LetsEncrypt. 225 | 226 | With no parameters, `certify` generates a self-signed cert for 227 | `example.com` (the default domain name defined in `nginx.conf`). 228 | 229 | To generate a self-signed cert with another domain name, first edit the 230 | domain name in `nginx.conf`. Afterward, generate a new cert with: 231 | 232 | ./certify.sh 233 | 234 | # e.g., ./certify.sh www.shinystudio.com 235 | 236 | If your server is accessible from the web, you can request a trusted 237 | certificate from LetsEncrypt. First, edit `nginx.conf` with your domain 238 | name, then request a new cert from LetsEncrypt like so: 239 | 240 | ./certify.sh 241 | 242 | # e.g., ./certify.sh www.shinystudio.com donald@email.com 243 | 244 | CertBot, included in the stack, will automatically renew your 245 | LetsEncrypt certificate. 246 | 247 | To manage the services in the stack, use the native docker-compose 248 | commands, e.g.: 249 | 250 | # stop all services. 251 | docker-compose down 252 | 253 | # start all services. 254 | docker-compose up -d 255 | 256 | ## Develop 257 | 258 | Open either RStudio or VS Code and notice two important directories: 259 | 260 | - \_\_ShinyStudio\_\_ 261 | - \_\_Personal\_\_ 262 | 263 | > Files must be saved in either of these two directories in order to 264 | > persist between sessions. 265 | 266 | ![](https://i.imgur.com/ac7iKDHh.png) 267 | 268 | These two folders are shared between instances RStudio, VS Code, and 269 | Shiny Server. So, creating new content is as simple as saving a file to 270 | the appropriate directory. 271 | 272 | ![](https://i.imgur.com/lAuTMgBh.png) 273 | 274 | ## Tools 275 | 276 | The ShinyStudio image comes with… 277 | 278 | - R 279 | - Python 3 280 | - PowerShell 281 | 282 | …and ODBC drivers for: 283 | 284 | - SQL Server 285 | - PostgresSQL 286 | - Cloudera Impala. 287 | 288 | These are persistent because they are built into the image. 289 | 290 | | | Persistent | 291 | | ----------------------------: | :--------: | 292 | | \_\_ShinyStudio\_\_ directory | Yes | 293 | | \_\_Personal\_\_ directory | Yes | 294 | | Other directories | **No** | 295 | | R Libraries | Yes | 296 | | Python Packages | Yes | 297 | | PowerShell Modules | Yes | 298 | | RStudio User Settings | Yes | 299 | | VS Code User Settings | Yes | 300 | | Installed Apps | **No** | 301 | | Installed Drivers | **No** | 302 | 303 | ## References 304 | 305 | - 306 | - 307 | - 308 | - 309 | - 310 | -------------------------------------------------------------------------------- /samples/_docs/Jupyter Notebook/example.ipynb: -------------------------------------------------------------------------------- 1 | {"cells":[{"cell_type":"markdown","metadata":{},"source":[" Use VS code to author Python scripts and easily convert them to Jupyter notebooks.\n","\n"," Afterward, conver the Jupyter notebook (.ipynb) to HTML so it is viewable in Shiny Server.\n","\n"," `jupyter nbconvert *.ipynb --to html -y --template full`\n","\n"," See more: https://code.visualstudio.com/docs/python/jupyter-support"]},{"cell_type":"markdown","metadata":{},"source":[" Below is a quick demo of the Python library, [ezpq](https://github.com/dm3ll3n/ezpq)."]},{"cell_type":"code","execution_count":4,"metadata":{},"outputs":[],"source":["\n","import ezpq\n","import time\n","import pandas as pd\n",""]},{"cell_type":"code","execution_count":5,"metadata":{},"outputs":[{"name":"stdout","output_type":"stream","text":"60 job results.\n"},{"data":{"text/html":"
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
qididlaneruntime
0queue_1101.010010
1queue_1211.022951
2queue_1321.036379
3queue_1431.030257
4queue_1541.017502
\n
","text/plain":" qid id lane runtime\n0 queue_1 1 0 1.010010\n1 queue_1 2 1 1.022951\n2 queue_1 3 2 1.036379\n3 queue_1 4 3 1.030257\n4 queue_1 5 4 1.017502"},"execution_count":5,"metadata":{},"output_type":"execute_result"}],"source":["all_output = list()\n","\n","# run three different `ezpq` parallel queues, sequentially.\n","for qid in [1, 2, 3]:\n"," # each queue will process 5 jobs at a time.\n"," with ezpq.Queue(5, qid='queue_' + str(qid)) as Q:\n"," # submit 20 jobs, each taking exactly one second.\n"," for i in range(20):\n"," lane = i % 5 # lanes handle dependent jobs.\n"," Q.put(time.sleep, args=1,\n"," lane=lane, name='Job '+str(i))\n","\n"," # wait for all enqueued jobs to complete.\n"," Q.wait()\n"," \n"," # collect job results\n"," all_output.extend( Q.collect() )\n","\n","print('{} job results.'.format(len(all_output)))\n","\n","# Peek at results in a dataframe.\n","pd.DataFrame( all_output )[['qid', 'id', 'lane', 'runtime']].head()\n",""]},{"cell_type":"code","execution_count":6,"metadata":{},"outputs":[{"data":{"image/png":"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\n","text/plain":"
"},"metadata":{},"output_type":"display_data"},{"data":{"text/plain":""},"execution_count":6,"metadata":{},"output_type":"execute_result"}],"source":["\n","# Plot queue operations.\n","ezpq.Plot(all_output).build(facet_by='qid',\n"," color_by='lane',\n"," color_pal=['blue', 'orange', 'green',\n"," 'red', 'purple'])\n",""]},{"cell_type":"code","execution_count":7,"metadata":{},"outputs":[],"source":""}],"nbformat":4,"nbformat_minor":2,"metadata":{"language_info":{"name":"python","codemirror_mode":{"name":"ipython","version":3}},"orig_nbformat":2,"file_extension":".py","mimetype":"text/x-python","name":"python","npconvert_exporter":"python","pygments_lexer":"ipython3","version":3}} --------------------------------------------------------------------------------