Ansible Role linuxfabrik.lfops.shiny_server¶
This role installs and configures Shiny Server Open Source, the Node.js server from Posit that hosts R Shiny applications, and makes it multi-tenant: one Apache httpd vHost per tenant, HTTP basic authentication per tenant, one R worker and one system account per tenant application.
Shiny Server Open Source knows no users, no authentication and no roles. The separation therefore does not happen inside the application but in front of it, in the reverse proxy, and below it, in separate application instances and system accounts.
Available in the next LFOps release.
How the Role Behaves¶
- Posit publishes one RPM for the whole Red Hat family, built against CentOS 8. It declares no dependencies at all and carries its own Node.js runtime, which is why the same file runs on RHEL 8, 9 and 10. The package is downloaded on the Ansible controller and copied to the target, so targets without Internet access can be provisioned; the controller needs outbound access to
download3.rstudio.org. It is fetched only when the installed version differs fromshiny_server__version. - Updating Shiny Server means raising
shiny_server__versionand running the role again. Posit documents no separate upgrade procedure: the Admin Guide installs the RPM of the wanted version and stops there, which is exactly what the role does, and the package manager resolves it as an upgrade of the installed one. Lowering the version works the same way and downgrades. The package restarts the service as part of its own installation, so an update is a brief outage and takes every running R worker with it. The configuration, the systemd drop-in and the role's logrotate file survive it: the package writesshiny-server.confonly when none exists, and it replaces the unit and its own/etc/logrotate.d/shiny-serverrather than anything belowshiny-server.service.d/. - The configuration is deployed before the package. The package writes its own
shiny-server.confonly when none exists, and then enables and starts the service immediately, so without this the service would come up once listening on every interface and serving/srv/shiny-serverwithout any access control. - Shiny Server listens on
127.0.0.1only. It passes every client header straight through to the R worker, so on a listener reachable from the network a client bypasses the proxy and setsShiny-Server-Credentialsitself, which is exactly the value an application reads assession$user. The role refuses to run when that header is enabled on a non-loopback listener. - A configuration change is applied with a reload. The service maps
ExecReloadtoSIGHUP, on which Shiny Server re-reads its configuration, so no session is dropped. This also meanslfops__skip_restart_handlersdoes not defer a configuration change. - The package overwrites
/etc/systemd/system/shiny-server.serviceand/etc/logrotate.d/shiny-serveron every update. The role therefore never touches either: unit settings go into a drop-in under/etc/systemd/system/shiny-server.service.d/, and the rotation of the per-session application logs into a separate/etc/logrotate.d/shiny-server-apps. The vendor's own file keeps rotating the daemon log/var/log/shiny-server.log. - Every application gets its own
location, and therefore its own R worker. This is the reason to give each tenant its own entry even when two tenants run the same code: the default scheduler starts a single, single-threaded R worker per application, shared by all its visitors, so two tenants on one instance block each other while one of them computes. - The role creates the directory structure, the system accounts and the log directories. It does not deploy application code. Put the shared code below
shiny_server__shared_dirand the per-tenant configuration and data belowshiny_server__tenants_dir, using symlinks to the shared files rather than copies. Link the individual files, not the whole directory: the working directory of the R process stays the tenant directory that way, so relative paths in the application code find the right tenant's data. - A tenant account is created with its own primary group and a secondary membership in
shiny_server__shared_group. The primary group keeps it out of the other tenants' directories, the secondary one lets it read the shared code tree. That shared group is deliberately not the group of the service account: the latter owns the application directory of every location left at the defaultrun_as, so using it for both jobs would make those directories readable by all tenants. The account gets no login shell, which does not hinder Shiny Server: it starts the worker as root withsu -s /bin/bash, which overrides the shell of the account. It does need a home directory, because a worker whose account has none is not started at all. - An R worker inherits nothing from the service environment. Shiny Server hands it
HOME,LANGandPATH, and thesu --loginit goes through then discards those again in favour of the login environment.TMPDIR,R_LIBS,RETICULATE_PYTHONand a full locale therefore belong inRenviron.siteorRprofile.site, which therrole manages. - Retiring a location with
state: 'absent'removes what the role created for it: thelocationfrom the configuration, the application directory with the tenant's data in it, the log directory, and the account the application ran as together with its group and home directory. Anything a still-active location shares is kept, so retiring one of a tenant's two applications leaves the account, the log directory and the tenant directory in place. The tenant directory itself goes only once it is empty. - Deleting a location's entry from the inventory is not the same as retiring it. The entry simply disappears from the merged list, so the location stops being served and nothing on disk is touched. Only an explicit
state: 'absent'deletes, which is what keeps a mistypedpathfrom destroying data. - The role does not manage TLS. The generated vHosts listen on port 80, for the topology where a reverse proxy in front terminates TLS and the network restricts who may reach this host. Where users connect to this Apache directly, TLS is mandatory, because basic authentication sends the credentials on every request; set
virtualhost_port: 443per vHost and add the certificate directives through itsrawkey.
Known Limitations¶
- Shiny Server Open Source has no authentication, no session management and no per-application resource limits. Basic authentication in front of it cannot log out, expire a session, lock an account after failed attempts or offer a second factor. Where that is needed, Posit Connect or ShinyProxy with one container per user is the answer.
- The separation inside a single application is guaranteed by the application code, not by the platform. All visitors of one application share one R process; everything inside
server <- function(input, output, session)is per session, everything inglobal.Ror aboveserver()is shared by all sessions of that application. - The memory limit in the systemd drop-in protects the host, not the individual session. All sessions of all tenants share the budget, and the OOM killer picks a process inside the control group when it is exceeded.
- Only the x86_64 package is supported. Posit publishes no aarch64 build of Shiny Server.
Dependent Roles¶
Any LFOps playbook that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables.
- R and the CRAN packages a Shiny application needs must be installed (role: linuxfabrik.lfops.r). This role injects
shiny,rmarkdown,knitrandhtmltoolsinto it, plus theTMPDIRthe uploads land in. - The reverse proxy in front of the tenants must be configured (role: linuxfabrik.lfops.apache_httpd). This role injects one vHost per tenant, the
.htpasswdentries, and theproxy_httpandproxy_wstunnelmodules that role leaves disabled by default. - The
python3-passliblibrary must be installed (role: linuxfabrik.lfops.python), because the.htpasswdfiles are written with it. - On RHEL-compatible systems, the
httpd_can_network_connectSELinux boolean must be enabled (roles: linuxfabrik.lfops.policycoreutils, linuxfabrik.lfops.selinux). Without it Apache cannot reach the Shiny Server port.
Requirements¶
- Outbound HTTPS access from the Ansible controller to
download3.rstudio.org.
Manual steps:
- Look up the current version on the GitHub Tags page and pin it in
shiny_server__version. - Deploy the application code below
shiny_server__shared_dirand the per-tenant files belowshiny_server__tenants_diryourself, from Git or with the files role. - Optional: where users reach this Apache directly rather than through a proxy in front, obtain a certificate per tenant hostname (role: linuxfabrik.lfops.acme_sh) and add the TLS directives to the vHost through its
rawkey.
Multi-Tenant Setup¶
Two tenants with two applications each need two vHosts, not four: the applications become location blocks inside the tenant's vHost, and one password per tenant covers all of them.
1. Declare one Shiny Server location per tenant application¶
Each entry becomes a location in shiny-server.conf and gets its own R worker and its own account.
shiny_server__locations__group_var:
- path: '/acme/app1'
app_dir: '/srv/shiny-server/tenants/acme/app1'
log_dir: '/var/log/shiny-server/acme'
run_as: 'shiny-acme'
- path: '/acme/app2'
app_dir: '/srv/shiny-server/tenants/acme/app2'
log_dir: '/var/log/shiny-server/acme'
run_as: 'shiny-acme'
2. Declare one vHost per tenant¶
locations maps the URL path inside the vHost to the Shiny Server location behind it, so the tenant reaches its applications at /app1 and /app2 without the tenant name in the URL.
shiny_server__vhosts__group_var:
- conf_server_name: 'shiny-acme.example.com'
htpasswd:
- username: 'firstname.lastname'
password: 'linuxfabrik'
locations:
- shiny_location: '/acme/app1'
path: '/app1'
- shiny_location: '/acme/app2'
path: '/app2'
3. Lay out the application code¶
The role creates the directories; the files are yours to deploy. Symlink the shared code file by file so that the working directory of the R process stays the tenant directory:
/srv/shiny-server/shared/app1/app.R
/srv/shiny-server/tenants/acme/app1/app.R -> ../../../shared/app1/app.R
/srv/shiny-server/tenants/acme/app1/config.R (tenant specific)
/srv/shiny-server/tenants/acme/app1/data/ (tenant specific)
Tags¶
shiny_server
- Installs Shiny Server.
- Creates the service account, the tenant accounts and the directory structure.
- Deploys the configuration, the systemd drop-in and the logrotate configuration.
- Ensures the service is in the desired state.
- Triggers: shiny-server.service reload, shiny-server.service restart.
shiny_server:configure
- Creates the directory structure and deploys the configuration and the systemd drop-in.
- Triggers: shiny-server.service reload, shiny-server.service restart.
shiny_server:logrotate
- Deploys the logrotate configuration of the application logs.
- Triggers: none.
shiny_server:state
- Manages the service state (start, stop, enable, disable).
- Triggers: none.
shiny_server:users
- Creates the accounts the applications run as.
- Triggers: none.
Mandatory Role Variables¶
shiny_server__version
- The version of Shiny Server to install, as it appears in the package filename. Look it up on the GitHub Tags page.
- Type: String.
Example:
# mandatory
shiny_server__version: '1.5.23.1030'
Optional Role Variables¶
shiny_server__conf_allow_app_override
- Allow an application directory to override server settings through a
.shiny_app.confnext to the application code. - Type: Bool.
- Default:
false - Deviates from the upstream default
true: on a host where tenants own their application directories, this lets a tenant raise its own request limit, turn error sanitizing off, make its log files world readable and choose its own Python interpreter.
shiny_server__conf_app_idle_timeout
- Seconds an R process without a connection keeps running.
0disables the cleanup. Keep it low for memory-hungry applications, otherwise abandoned sessions hold memory indefinitely. - Type: Number.
- Default:
5
shiny_server__conf_app_init_timeout
- Seconds an application is given to start.
- Type: Number.
- Default:
60
shiny_server__conf_frame_options
X-Frame-Optionsheader sent on URLs served from Shiny applications, as a mitigation against clickjacking.- Type: String. One of
allow,deny,sameorigin. - Default:
'sameorigin' - Deviates from the upstream default
allow, which sends no header at all and lets any site embed the application in a frame.
shiny_server__conf_http_keepalive_timeout
- Seconds an HTTP connection stays open between requests.
- Type: Number.
- Default:
45
shiny_server__conf_listen_host
- Address Shiny Server listens on. Leave this on the loopback: Shiny Server authenticates nobody and passes every client header through to the application, so any address reachable from the network is a way around the reverse proxy and its authentication.
- Type: String.
- Default:
'127.0.0.1' - Deviates from the upstream default
*, which serves every interface.
shiny_server__conf_listen_port
- Port Shiny Server listens on.
- Type: Number.
- Default:
3838
shiny_server__conf_preserve_logs
- Keep the log files of Shiny processes that exited successfully. Useful while hunting a startup error, together with the browser-only error message. Thousands of files accumulate quickly, which is why the role also rotates them.
- Type: Bool.
- Default:
false
shiny_server__conf_run_as
- Account the applications run as unless a location names its own. This is the account the package creates.
- Type: String.
- Default:
'shiny'
shiny_server__conf_sanitize_errors
- Send only generic error messages to the browser. Individual messages reach the user through
stop(safeError(e))in the application code. - Type: Bool.
- Default:
true
shiny_server__conf_simple_scheduler
- Maximum number of concurrent requests per application before the server answers
503 Service Unavailable. Lower it noticeably for applications that need a lot of memory. - Type: Number.
- Default:
100
shiny_server__download_url
- Full URL of the Shiny Server package. Empty derives it from
shiny_server__versionand the platform. Set it to install from a local mirror. - Type: String.
- Default:
''
shiny_server__htpasswd_dir
- Directory the per-tenant
.htpasswdfiles are written to. - Type: String.
- Default:
'/etc/httpd'
shiny_server__log_dir
- Directory the application logs are written to, unless a location names its own.
- Type: String.
- Default:
'/var/log/shiny-server'
shiny_server__logrotate
- Application log files are rotated
countdays before being removed. - Type: Number.
- Default:
{{ logrotate__rotate | d(14) }}
shiny_server__memory_max
MemoryMaxof the service. Shiny Server Open Source enforces no memory limit of its own, and a single R session can grow until the host is full without load or process count looking unusual. The R workers are started throughsubut stay in the control group of the service, so the cap reaches them. Empty leaves the setting out of the drop-in.- Type: String.
- Default:
''
shiny_server__service_enabled
- Enables or disables the service, analogous to
systemctl enable/disable --now. - Type: Bool.
- Default:
true
shiny_server__service_state
- Changes the state of the service, analogous to
systemctl start/stop/restart/reload. - Type: String. One of
reloaded,restarted,started,stopped. - Default:
'started'
shiny_server__shared_dir
- Directory holding the application code shared between the tenants. Readable for the accounts the applications run as through their membership in
shiny_server__shared_group, invisible to the rest of the host. - Type: String.
- Default:
'/srv/shiny-server/shared'
shiny_server__shared_group
- Group that owns the shared code tree, and that every account an application runs as is a secondary member of. Keep it separate from
shiny_server__conf_run_as: that account's group owns the application directory of every location left at the defaultrun_as, so sharing one group for both purposes would let every tenant read those directories. - Type: String.
- Default:
'shiny-shared'
shiny_server__site_dir
- Root directory the role creates.
shiny_server__shared_dirandshiny_server__tenants_dirlive below it. The packaged Shiny Server serves this tree on/; this role does not, because it is handed out without any access control. A location withsite_dirgives the same behaviour where it is actually wanted, behind the tenant's vHost. - Type: String.
- Default:
'/srv/shiny-server'
shiny_server__tasks_max
TasksMaxof the service, capping processes and threads in its control group. R forks for parallel work, so without it a runaway application is bounded only by systemd'sDefaultTasksMax, which is 15% ofkernel.pid_maxand therefore commonly in the tens of thousands. Empty leaves the setting out of the drop-in.- Type: Number.
- Default:
''
shiny_server__tenants_dir
- Directory below which the per-tenant application directories live.
- Type: String.
- Default:
'/srv/shiny-server/tenants'
shiny_server__tmp_dir
TMPDIRof the R workers, injected into therrole. Shiny stores uploaded files in a per-session directory belowtempdir()and refuses locations outside it, so this is where uploads land. Put it on its own partition where large uploads are expected, so that a full upload cannot fill the root file system. Shiny cleans the session directory up at the end of the session; where the R process dies first, the files stay behind, so data worth protecting needs a deletion concept of its own, for example withsystemd-tmpfiles.- Type: String.
- Default:
'/var/lib/shiny-server/tmp'
shiny_server__users_home_dir
- Home directory root of the accounts the applications run as. Shiny Server does not start a worker whose account has no home directory.
- Type: String.
- Default:
'/var/lib/shiny-server/home'
Example:
# optional
shiny_server__conf_allow_app_override: false
shiny_server__conf_app_idle_timeout: 60
shiny_server__conf_app_init_timeout: 60
shiny_server__conf_frame_options: 'sameorigin'
shiny_server__conf_http_keepalive_timeout: 45
shiny_server__conf_listen_host: '127.0.0.1'
shiny_server__conf_listen_port: 3838
shiny_server__conf_preserve_logs: true
shiny_server__conf_run_as: 'shiny'
shiny_server__conf_sanitize_errors: true
shiny_server__conf_simple_scheduler: 20
shiny_server__download_url: 'https://mirror.example.com/shiny-server-1.5.23.1030-x86_64.rpm'
shiny_server__htpasswd_dir: '/etc/httpd'
shiny_server__log_dir: '/var/log/shiny-server'
shiny_server__logrotate: 7
shiny_server__memory_max: '8G'
shiny_server__service_enabled: true
shiny_server__service_state: 'started'
shiny_server__shared_dir: '/srv/shiny-server/shared'
shiny_server__shared_group: 'shiny-shared'
shiny_server__site_dir: '/srv/shiny-server'
shiny_server__tasks_max: 200
shiny_server__tenants_dir: '/srv/shiny-server/tenants'
shiny_server__tmp_dir: '/var/lib/shiny-server/tmp'
shiny_server__users_home_dir: '/var/lib/shiny-server/home'
Optional Role Variables - Tenants¶
shiny_server__locations__host_var / shiny_server__locations__group_var
- The applications Shiny Server serves, one entry per tenant application. Each becomes a
locationand gets its own R worker. - Type: List of dictionaries.
- Default:
[] -
Subkeys:
-
path:- Mandatory. The request path Shiny Server serves this entry at.
- Type: String.
-
app_dir:- Mandatory, unless
site_diris given. Directory of the Shiny application. - Type: String.
- Mandatory, unless
-
site_dir:- Optional, alternative to
app_dir. Directory tree served as a website, containing applications and static files. Served without access control. - Type: String.
- Default: unset, so the location serves the single application in
app_dir
- Optional, alternative to
-
log_dir:- Optional. Directory the application logs are written to.
- Type: String.
- Default: the value of
shiny_server__log_dir
-
run_as:- Optional. Account the application runs as. Naming one other than
shiny_server__conf_run_asmakes the role create it, which is what extends the separation into the file system. - Type: String.
- Default: the value of
shiny_server__conf_run_as
- Optional. Account the application runs as. Naming one other than
-
app_idle_timeout:- Optional. Seconds this application's R process keeps running without a connection.
- Type: Number.
- Default: the value of
shiny_server__conf_app_idle_timeout
-
app_init_timeout:- Optional. Seconds this application is given to start.
- Type: Number.
- Default: the value of
shiny_server__conf_app_init_timeout
-
directory_index:- Optional. List the directory contents when no
index.htmlis present. Only meaningful together withsite_dir. - Type: Bool.
- Default:
false(Shiny Server disables directory indexes when the directive is absent)
- Optional. List the directory contents when no
-
python:- Optional. Python interpreter or virtual environment for a Shiny for Python application.
- Type: String.
- Default: unset, so the location serves R applications
-
sanitize_errors:- Optional. Send only generic error messages to the browser.
- Type: Bool.
- Default: the value of
shiny_server__conf_sanitize_errors
-
simple_scheduler:- Optional. Maximum number of concurrent requests before this application answers 503. Lower it for memory-hungry applications.
- Type: Number.
- Default: the value of
shiny_server__conf_simple_scheduler
-
state:- Optional.
presentorabsent.absentdeletes the application directory including the tenant's data, the log directory, and the account the application ran as, unless another location still uses them. Removing the entry from the inventory instead only stops the location from being served and deletes nothing. - Type: String.
- Default:
'present'
- Optional.
-
shiny_server__vhosts__host_var / shiny_server__vhosts__group_var
- The Apache httpd vHosts, one entry per tenant. Injected into the
apache_httpdrole as aproxyvHost. - Type: List of dictionaries.
- Default:
[] -
Subkeys:
-
conf_server_name:- Mandatory. The tenant's hostname.
- Type: String.
-
locations:- Mandatory. The applications this tenant reaches through this vHost.
- Type: List of dictionaries.
-
Subkeys:
-
shiny_location:- Mandatory. The
pathof theshiny_server__locationsentry behind it. - Type: String.
- Mandatory. The
-
path:- Optional. The request path inside this vHost.
- Type: String.
- Default: the value of
shiny_location
-
state:- Optional.
presentorabsent. - Type: String.
- Default:
'present'
- Optional.
-
-
htpasswd:- Optional. The accounts allowed into this tenant's applications.
- Type: List of dictionaries.
-
Subkeys:
-
username:- Mandatory. The login name.
- Type: String.
-
password:- Mandatory for
state: 'present'. The password. - Type: String.
- Mandatory for
-
state:- Optional.
presentorabsent. - Type: String.
- Default: the
stateof the vHost
- Optional.
-
-
allowed_http_methods:- Optional. The HTTP methods this vHost allows.
- Type: List of strings.
- Default: the value of
shiny_server__vhost_default_allowed_http_methods
-
auth_enabled:- Optional. Protect this vHost with HTTP basic authentication.
- Type: Bool.
- Default: the value of
shiny_server__vhost_default_auth_enabled
-
auth_name:- Optional. The realm shown in the browser's login prompt.
- Type: String.
- Default:
'Shiny <conf_server_name>'
-
auth_user_file:- Optional. Path of this tenant's
.htpasswdfile. - Type: String.
- Default:
'{{ shiny_server__htpasswd_dir }}/.htpasswd-<conf_server_name>'
- Optional. Path of this tenant's
-
conf_proxy_preserve_host:- Optional.
ProxyPreserveHostof this vHost. - Type: String.
- Default:
'On'
- Optional.
-
conf_proxy_timeout:- Optional.
ProxyTimeoutof this vHost, in seconds. - Type: Number.
- Default: the value of
shiny_server__vhost_default_conf_proxy_timeout
- Optional.
-
credentials_header_enabled:- Optional. Pass the authenticated user into the application, readable there as
session$user. - Type: Bool.
- Default: the value of
shiny_server__vhost_default_credentials_header_enabled
- Optional. Pass the authenticated user into the application, readable there as
-
enabled:- Optional. Whether the vHost is linked into
sites-enabled. - Type: Bool.
- Default:
true
- Optional. Whether the vHost is linked into
-
filename:- Optional. Filename of the vHost configuration.
- Type: String.
- Default: the value of
conf_server_name
-
raw:- Optional. Verbatim Apache directives appended to this vHost, for TLS certificates and anything else the subkeys do not cover.
- Type: String.
- Default:
''
-
virtualhost_port:- Optional. Port of this vHost.
- Type: Number.
- Default: the value of
shiny_server__vhost_default_virtualhost_port
-
state:- Optional.
presentorabsent. - Type: String.
- Default:
'present'
- Optional.
-
Example:
# optional
shiny_server__locations__group_var:
- path: '/acme/app1'
app_dir: '/srv/shiny-server/tenants/acme/app1'
log_dir: '/var/log/shiny-server/acme'
run_as: 'shiny-acme'
app_idle_timeout: 900
- path: '/globex/app1'
app_dir: '/srv/shiny-server/tenants/globex/app1'
log_dir: '/var/log/shiny-server/globex'
run_as: 'shiny-globex'
simple_scheduler: 20
shiny_server__vhosts__group_var:
- conf_server_name: 'shiny-acme.example.com'
htpasswd:
- username: 'firstname.lastname'
password: 'linuxfabrik'
locations:
- shiny_location: '/acme/app1'
path: '/app1'
- conf_server_name: 'shiny-globex.example.com'
virtualhost_port: 443
htpasswd:
- username: 'globex.admin'
password: 'linuxfabrik'
locations:
- shiny_location: '/globex/app1'
path: '/app1'
raw: |
SSLEngine on
SSLCertificateFile /etc/pki/tls/certs/shiny-globex.example.com.crt
SSLCertificateKeyFile /etc/pki/tls/private/shiny-globex.example.com.key
Optional Role Variables - vHost Defaults¶
These apply to every generated vHost. Each can be overridden per vHost with the key of the same name without the shiny_server__vhost_default_ prefix.
shiny_server__vhost_default_allowed_http_methods
- The HTTP methods the generated vHosts allow.
- Type: List of strings.
- Default:
['GET', 'HEAD', 'OPTIONS', 'POST'] POSTis mandatory: file uploads and the SockJS fallback transports use it, and without it afileInput()fails with a 405 the user interface does not explain.HEADkeeps header-only monitoring checks from answering 405.
shiny_server__vhost_default_auth_enabled
- Protect the generated vHosts with HTTP basic authentication.
- Type: Bool.
- Default:
true
shiny_server__vhost_default_conf_proxy_timeout
ProxyTimeoutof the generated vHosts, in seconds. A WebSocket idles while R computes, and Apache drops it when this expires, which shows up in the browser as the application "greying out". Keep it above the longest expected computation.- Type: Number.
- Default:
300 - Deviates from the upstream default
60(Apache'sTimeout, whichProxyTimeoutinherits when unset): a Shiny session carries no traffic while R works, so at the upstream value any computation longer than a minute kills the connection.
shiny_server__vhost_default_credentials_header_enabled
- Pass the user authenticated by Apache into the application through the
Shiny-Server-Credentialsheader, where it is readable assession$user. Only safe because Shiny Server binds the loopback and the header is set, not added, so a value sent by the client is overwritten. This is not an authorization check: what the application does with the value is up to its own code. - Type: Bool.
- Default:
true
shiny_server__vhost_default_virtualhost_port
- Port of the generated vHosts.
- Type: Number.
- Default:
80
Example:
# optional
shiny_server__vhost_default_allowed_http_methods:
- 'GET'
- 'HEAD'
- 'OPTIONS'
- 'POST'
shiny_server__vhost_default_auth_enabled: true
shiny_server__vhost_default_conf_proxy_timeout: 900
shiny_server__vhost_default_credentials_header_enabled: true
shiny_server__vhost_default_virtualhost_port: 443
Troubleshooting¶
An application greys out a short while after it loads
- The WebSocket was cut, usually by a timeout in the reverse proxy or a firewall in between. While R computes, no data flows over the connection and the proxy takes it for dead. Raise
conf_proxy_timeouton the tenant's vHost above the longest expected computation.
A file upload aborts and the browser console shows a 405
- The reverse proxy does not let
POSTthrough, which is the only method Shiny accepts uploads on. Add it toallowed_http_methodson that vHost.
An application fails to start and the error is only visible in the browser
- The per-session logs below
/var/log/shiny-server/are deleted once an application has started successfully. Setshiny_server__conf_preserve_logs: true, reload the service, and reproduce the error; the logs then stay.
An application log reports there is no package called '...'
- The R package is not installed in the system library, or the account the application runs as cannot read it. Check with
sudo --user=<run_as account> R --quiet -e ".libPaths()"and add the package tor__cran_packages__group_var.
The run aborts with A vHost passes Shiny-Server-Credentials, but Shiny Server listens on ...
- The listener is reachable from the network, where a client can bypass Apache and set the identity header itself. Set
shiny_server__conf_listen_hostback to127.0.0.1, or turn the header off withcredentials_header_enabled: falseon the affected vHosts.
session$user is empty in the application
- The value arrives on the WebSocket upgrade request only, so
RequestHeadermust apply to it: theearlyflag would run the directive before authentication, and%{REMOTE_USER}sor%{REMOTE_USER}eboth yield the literal(null). The role writes the working form; a hand-written vHost has to useexpr=as well.
The service will not start and /var/log/shiny-server.log gains no new lines at all
- systemd is refusing to run it. The unit ships
Restart=on-failurewithStartLimitBurst=3andStartLimitInterval=45, so after three failed starts within 45 seconds the process is no longer launched and therefore cannot log anything, which makes it look as though the log had stopped rather than the service. Clear it withsystemctl reset-failed shiny-serverand start again. The usual underlying cause is a configuration Shiny Server rejects; the reason for that is in the last lines the log did manage to write.
Orphaned R processes after a restart
- The unit uses
KillMode=process, so systemd only stops the main process and leaves the workers to Shiny Server's own shutdown. Check withsystemctl status shiny-serverwhether any survived.