Deploying Shiny or Dash Apps with ShinyProxy, Docker, and AWS
In a previous post, we documented the steps for deploying shiny applications or interactive documents through shiny-server, rstudio-server, and AWS EC2. This post documents the steps for deploying apps using Docker, ShinyProxy, and AWS EC2.
Step 1: AWS Set Up
If you are working for an organization that uses AWS, the chances are that your IT department may already have an EC2 instance or protocols for IAM identity management. In that case, seek approval for and obtain the following:
An IAM user with minimum permissions attached required. The required permissions will be covered in more details in the coming sections.
To set up the server, we would need to connect to EC2 via a Secure Shell (SSH) using a Command Line Interface (CLI), and so we need to obtain the AWS EC2
.pem
key pair.
The rest of the setup steps may differ if you are using your organization’s EC2 or running your own. For the purpose of this post, however, we will create and provision our own personal Amazon Web Services (AWS) account, IAM users, and AWS Resources in a non-enterprise setting. The first step is to register for an AWS account, which is free of charge.
AWS Command Line Interface
The AWS command line interface is a tool for managing AWS services from the command line. Follow the installation instruction here for your operating system. The AWS CLI will be used for the following:
- Validating AWS CloudFormation template
- Pushing and pulling docker image to and from the Amazon Elastic Container Registry (ECR)
Administrative IAM User
When we first create an Amazon Web Services (AWS) account, a root user with complete access to all AWS services is created. It is not recommended to perform everyday tasks using the root user; instead, we can create an IAM user with administrative access to manage everyday tasks such as user creation. Follow the official documentations here to create such a user. Make sure to enable AWS management console access:
IAM User Group
Once we login with the administrative IAM user, we can create additional IAM users with specific permissions required to perform tasks related to configuration and deployment of our applications. This follows the principle of least privilege, granting only the permissions required for the task at hand. Open the IAM console using the search bar and create a user group. No need to attach any permissions policy yet.
Next, create an IAM user and enable AWS management console:
Assign the IAM user to the user group that we just created and generate credentials (Access Key ID and Secret Access Key) for programmatic access via the AWS command line interface:
Once we obtain the credentials, open the terminal, execute the following command, and copy and paste the credentials into the terminal when prompted:
# Change this to your IAM user name
$ aws configure --profile dashboard_dev
We can optionally save the credentials as a csv file on our local system; it is recommended that we rotate these credentials regularly. Finally, we can check that the IAM user is indeed added to the user group that we just created.
Key Pair
In order to verify our identity when connecting to our Amazon EC2 instances, we need to use a set of credentials called a key pair, which consists of a public key and a private key. In the EC2 console, under Network & Security -> Key Pairs
, create a new key pair using the administration IAM user.
The different types of key pairs has to do with the underlying signature algorithms. For the purpose of this post, we will choose ED25519. Once we create the key pair, we can download the private key file (.pem
file) to our local machine. We will use this key pair to connect to our EC2 instance via SSH.
AWS CloudFormation
AWS CloudFormation is a service that allows us provision and configure AWS resources (like Amazon EC2 instances, S3 bucket, or Amazon RDS DB instances) using a template that describes all them. This means that we no longer have to individually deploy, configure, and terminate AWS services. With AWS CloudFormation, we describe the resources and their properties in a template file (either written in Json or YAML) and create a stack, which is a collection of resources. Create a shinyproxy-template.yaml
file as follows:
AWSTemplateFormatVersion: 2010-09-09
Parameters:
EnvironmentName:
Description: An environment name that will be prefixed to resource names
Type: String
Default: shinyproxy
VpcCIDR:
Description: Please enter the IP range (CIDR notation) for this VPC
Type: String
Default: 10.0.0.0/16
PublicSubnet1CIDR:
Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
Type: String
Default: 10.0.0.0/24
InstanceType:
Description: The type of EC2 instance to be provisioned for ShinyProxy
Type: String
Default: t3.medium
AllowedValues:
- t3.micro
- t3.small
- t3.medium
ShinyProxyVolumeSize:
Description: Volume size for the ShinyProxy Instance in GiB
Type: Number
Default: 30
MinValue: 8
MaxValue: 100
ConstraintDescription: Must be between 8 and 100 GiB
MyIP:
Description: Your IP address in CIDR notation (e.g. Your-IPV4/32) to whitelist for SSH access to ShinyProxy instances
Type: String
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH access to the instances
Type: String
ImageId:
Description: AMI ID for the EC2 instance (Defaults to Amazon Linux 2 AMI (HVM))
Type: String
Default: ami-0bb4c991fa89d4b9b # Amazon Linux 2 AMI (HVM)
DeviceName:
Description: Device name for the root volume on the EC2 instances
Type: String
Default: '/dev/xvda'
DeleteOnTermination:
Description: Whether to delete the volume on instance termination
Type: String
Default: 'true'
Resources:
VPC:
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: !Ref VpcCIDR
Tags:
- Key: Name
Value: !Ref EnvironmentName
InternetGateway:
Type: 'AWS::EC2::InternetGateway'
Properties:
Tags:
- Key: Name
Value: !Ref EnvironmentName
InternetGatewayAttachment:
Type: 'AWS::EC2::VPCGatewayAttachment'
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
PublicSubnet1:
Type: 'AWS::EC2::Subnet'
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs ''
CidrBlock: !Ref PublicSubnet1CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName} Public Subnet (AZ1)'
PublicRouteTable:
Type: 'AWS::EC2::RouteTable'
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub '${EnvironmentName} Public Routes'
DefaultPublicRoute:
Type: 'AWS::EC2::Route'
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: 'AWS::EC2::SubnetRouteTableAssociation'
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet1
# Security Group for ShinyProxy instances
ShinyProxySecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: Security group for ShinyProxy EC2 instances controlling inbound traffic
VpcId: !Ref VPC
SecurityGroupIngress:
- CidrIp: '0.0.0.0/0' # Shiny-server port
IpProtocol: tcp
FromPort: 3838
ToPort: 3838
- CidrIp: '0.0.0.0/0' # Shinyproxy port
IpProtocol: tcp
FromPort: 8080
ToPort: 8080
- CidrIp: '0.0.0.0/0' # Dash port
IpProtocol: tcp
FromPort: 8050
ToPort: 8050
- CidrIp: '0.0.0.0/0' # HTTP port
IpProtocol: tcp
FromPort: 80
ToPort: 80
- CidrIp: '0.0.0.0/0' # HTTPS port
IpProtocol: tcp
FromPort: 443
ToPort: 443
- CidrIp: !Ref MyIP # Allow SSH into ShinyProxy instance from specified IP
IpProtocol: tcp
FromPort: 22
ToPort: 22
ShinyProxyInstance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: !Ref ImageId
InstanceType: !Ref InstanceType
KeyName: !Ref KeyName
SecurityGroupIds:
- !Ref ShinyProxySecurityGroup
SubnetId: !Ref PublicSubnet1
BlockDeviceMappings:
- DeviceName: !Ref DeviceName
Ebs:
VolumeType: 'gp3'
VolumeSize: !Ref ShinyProxyVolumeSize
DeleteOnTermination: !Ref DeleteOnTermination
ShinyProxyEIP:
Type: 'AWS::EC2::EIP'
Properties:
Domain: vpc
InstanceId: !Ref ShinyProxyInstance
DependsOn: InternetGatewayAttachment
Outputs:
VPC:
Description: A reference to the created VPC
Value: !Ref VPC
PublicSubnet1:
Description: A reference to the public subnet in the 1st Availability Zone
Value: !Ref PublicSubnet1
ShinyProxySecurityGroup:
Description: Security group for ShinyProxy EC2 instance
Value: !Ref ShinyProxySecurityGroup
ShinyProxyInstance:
Description: A reference to the ShinyProxy EC2 instance
Value: !Ref ShinyProxyInstance
ShinyProxyEIP:
Description: The Elastic IP address associated with the ShinyProxy EC2 instance
Value: !Ref ShinyProxyEIP
Here is the architecture diagram for the stack described by the template above:
The sections below provide some additional details on the most important components in this infrastructure.
Mandatory Parameters
- MyIP:
- Description: Your IP address in CIDR notation (e.g. Your-IPV4/32) to whitelist for SSH access to the EC2 instance. Whitelisting your IP address ensures that only you (or anyone using the same IP) can initiate an SSH connection to the EC2 intance. This is a critical security measure to prevent unauthorized access attempts from unknown sources. Always ensure that you only allow trusted IPs to maintain a secure environment.
- KeyName:
- Description: Name of an existing EC2 Key Pair to enable SSH access to the instances.
Parameters with Default Values
- EnvironmentName:
- Description: An environment name that will be prefixed to resource names.
- Default:
shinyproxy
- VpcCIDR:
- Description: The IP range (CIDR notation) for this VPC.
- Default:
10.0.0.0/16
- PublicSubnet1CIDR:
- Description: The IP range (CIDR notation) for the public subnet in the first Availability Zone.
- Default:
10.0.0.0/24
- InstanceType:
- Description: The type of EC2 instance to be provisioned for ShinyProxy.
- Default:
t3.medium
- Allowed Values:
t3.micro
,t3.small
,t3.medium
- ShinyProxyVolumeSize:
- Description: Volume size for the ShinyProxy Instance in GiB.
- Default:
30
- Range: 8 to 100 GiB
- ImageId:
- Description: AMI ID for the EC2 instance.
- Default:
ami-03a6eaae9938c858c
(Amazon Linux 2 AMI (HVM)) - Alternative:
ami-0fc5d935ebf8bc3bc
(Ubuntu Server 22.04 LTS (HVM))
- DeviceName:
- Description: Device name for the root volume on the EC2 instances. This must be adjusted based on the AMI, virtualization type, and block device.
- Default:
/dev/xvda
# Amazon Linux 2 AMI (HVM) - Alternative:
/dev/sda1
# Ubuntu Server 22.04 LTS (HVM)
- DeleteOnTermination:
- Description: Whether to delete the volume on instance termination.
- Default:
true
For more information on AMIs, see the “AMIs” tab in the side menu of the EC2 console.
VPC and Subnet Configuration
The Virtual Private Cloud (VPC) serves as an isolated virtual network, providing a controlled environment for our AWS resource deployments. Subnets further split this VPC based on availability zones, allowing for public and private resource segregation.
Resource | Description |
---|---|
VPC | The main virtual private cloud resource. |
PublicSubnet1 | A subnet with resources accessible from the internet. |
Internet Gateway
This component ensure that resources within the VPC can connect to the internet.
Resource | Description |
---|---|
InternetGateway | Enables internet access for the VPC. |
Route Tables
Route tables contain a set of rules, called routes, that determine where network traffic is directed. In this template, a route table is defined for public subnet.
One key route is DefaultPublicRoute
, which is set up to redirect all outbound traffic to IP (0.0.0.0/0
), so our EC2 instance can access the broader internet via the Internet Gateway. This ensure we can download tools like nginx
and shinyproxy
from the internet.
Resource | Description |
---|---|
PublicRouteTable | Route table for the public subnet. |
EC2 Configuration: ShinyProxy Instance
The Elastic Compute Cloud (EC2) is a service offering scalable computing capacity in AWS. This template provisions a single EC2 instance placed in the public subnet.
Resource | Description |
---|---|
ShinyProxyInstance | The primary EC2 instance, hosting ShinyProxy. |
Security Group Configuration
Security Groups act as virtual firewalls, controlling inbound and outbound traffic to resources.
Resource | Description |
---|---|
ShinyProxySecurityGroup | Controls traffic for the ShinyProxy EC2 instance, including HTTP, HTTPS, and SSH and ports for ShinyProxy |
At the juncture, we can save and upload the shinyproxy-template.yaml
to s3. Follow the official documentations on creating S3 buckets. Before we create the stack, however, we need one more step to grant the necessary permissions to the IAM user group we created earlier.
Elastic IP Configuration
The Elastic IP (EIP) provides a static IP address for our ShinyProxyInstance
. This is crucial for maintaining consistent access and is particularly important for network configurations where a stable IP is necessary.
Resource | Description |
---|---|
ShinyProxyEIP | A static IP address assigned to the ShinyProxy EC2 instance for consistent external access. |
Note on Dependencies: The EIP is associated with the ShinyProxyInstance
and has a DependsOn
attribute to ensure proper creation order. This is important to align with the VPC and Internet Gateway setup in the template, adhering to AWS best practices.
In-Line Policy for IAM User Group
In order to successfully execute the deployment, we need to create an IAM policy and attach it to the IAM user group we created. Permissions in the policy determine whether the requests to AWS resources (e.g. creating an EC2 instance) are allowed or denied. Most policies are stored in AWS as JSON documents. It is recommended to grant an IAM user group only the necessary access and permissions needed to perform project-related tasks. In the admin user account, navigate to the IAM dashboard, and create an inline policy for the user group:
Select the json policy editor, copy and paste the following into the text box. Make sure to change the "arn:aws:s3:::your-s3-bucket/*"
to the name of the s3 bucket containing your shinyproxy-template.yaml
CloudFormation template.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EC2Permissions",
"Effect": "Allow",
"Action": [
"ec2:*"
],
"Resource": "*"
},
{
"Sid": "ECRPermissions",
"Effect": "Allow",
"NotAction": [
"ecr:SetRepositoryPolicy",
"ecr:DeleteRepositoryPolicy"
],
"Resource": "arn:aws:ecr:*:*:*"
},
{
"Sid": "ECRGetAuthorizationToken",
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken"
],
"Resource": "*"
},
{
"Sid": "CloudFormationPermissions",
"Effect": "Allow",
"Action": [
"cloudformation:*"
],
"Resource": "*"
},
{
"Sid": "IAMPermissions",
"Effect": "Allow",
"Action": [
"iam:ListRoles"
],
"Resource": "*"
},
{
"Sid": "SNSPermissions",
"Effect": "Allow",
"Action": [
"sns:ListTopics"
],
"Resource": "*"
},
{
"Sid": "S3Permissions",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::yang-templates/*"
}
]
}
Before moving on, we can validate the template for syntax issues using the AWS CLI. If the name of your IAM user is not dashboard_dev
, change the --profile
flag below accordingly:
$ aws cloudformation validate-template --profile dashboard_dev --template-body file:///path/to/shinyproxy-template.yaml
Stack Creation
We are now ready to create the stack using the CloudFormation console. Login to the IAM user we created earlier (i.e. dashboard_dev
) and navigate to the CloudFormation console:
The two required parameters in the template are:
KeyName
: The name of the key pair to use for SSH access to the EC2 instance. This is the name of the.pem
file we downloaded earlier.MyIP
: The IP address range that can access the EC2 instance via SSH. This is the IP address of the machine we are using to SSH into the EC2 instance. We can find our IP address by searching “what is my IP address” on Google. For example, if our IPV4 address is192.168.1.1
, we can enter192.168.1.1/32
as the value forMyIP
.
Everything else can either be left as default or changed to suit our needs. If an error occurs during the stack creation, we can check the event log for more information.
Step 2: Connecting to AWS EC2
To connect to our EC2 instance via SSH, we will use the terminal (for windows, use PuTTY). Open the terminal, navigate to the location of our .pem
key:
# Set permission for read
$ cd path_to_pem_key && chmod 400 my_key.pem
In order to SSH into our EC2 instance:
# Ubuntu
$ ssh -i my_key.pem ubuntu@elastic-ip-address
# Amazon Linux 2
$ ssh -i my_key.pem ec2-user@elastic-ip-address
The elastic IP address for the EC2 instance can be retreived from “Elastic IPs” tab under “Network & Security” from the side menu of the EC2 console.
The default username for Amazon Linux 2 is
ec2-user
. For Ubuntu, the default username isubuntu
.
To disconnect from our instances:
$ exit
Important: From this point on, the command line syntax and tools used to download/install packages will differ depending on the AMI we selected above. In the template above, we chose an AMI that uses Amazon linux 2 (not Amazon 2023), but Ubuntu is also a popular choice. Therefore, we will include syntax for both of these operating systems.
If you are a VS Code user, you could follow this Youtube video or Microsoft’s official documentation to set up remote SSH, which allows us to open a remote folder on any remote machine, virtual machine, or container with a running SSH server. Compared to using the terminal, this has the added benefit of allowing us to use an IDE to edit files on the EC2 instance.
Step 3: Docker
Installation
Install docker on the EC2 instance.
Ubuntu
Based on the instructions in the official Docker documentation:
# Update command
$ sudo apt-get update
# Install packages to allow apt to use a repository over HTTPS
$ sudo apt-get install -y \
ca-certificates \
curl \
gnupg \
lsb-release
# Add Docker’s official GPG key
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Set up the docker repository
$ echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update
$ sudo apt-get update
# Install
$ sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Verify installation
$ sudo docker run hello-world
The difference between apt-get
and apt
is that the former is an older command with more options while apt
is a newer, more user-friendly command with fewer options.
Amazon Linux 2
The installation steps can be found in this post.
# Update
$ sudo yum update -y
$ sudo yum install -y docker
# Check status
$ sudo systemctl status docker
Docker Startup Options
According to the official documentation, ShinyProxy needs to connect to the docker daemon to spin up the containers for the applications. By default, ShinyProxy will do so on port 2375
of the docker host (our EC2 instance). In order to allow for connections on port 2375
, the docker startup options must be modified.
Ubuntu
$ sudo mkdir /etc/systemd/system/docker.service.d
$ sudo touch /etc/systemd/system/docker.service.d/override.conf
$ sudo nano /etc/systemd/system/docker.service.d/override.conf
Amazon Linux 2
# Edit docker.service.d
$ sudo systemctl edit docker
For both operating systems, add the following content to the docker startup options:
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H unix:// -D -H tcp://127.0.0.1:2375
Restart docker:
# Ubuntu
$ sudo systemctl daemon-reload
# Amazon Linux 2
$ sudo systemctl restart docker
Useful Commands for Docker
Ubuntu
$ sudo service docker status
$ sudo service docker start
$ sudo service docker stop
$ sudo service docker restart
$ sudo docker version
# List docker images
$ sudo docker image ls
# Remove docker images with '-f' force remove option
$ sudo docker image rm -f image_id
Amazon Linux 2
$ sudo systemctl enable docker.service
$ sudo systemctl status docker.service
$ sudo systemctl start docker.service
$ sudo systemctl stop docker.service
$ sudo systemctl restart docker.service
$ sudo docker version
# List docker images
$ sudo docker image ls
# Remove docker images with '-f' force remove option
$ sudo docker image rm -f image_id
Step 4: Install ShinyProxy
ShinyProxy is written using mature and robust Java technology, so we would need a Java 8 (or higher) runtime environment to run ShinyProxy.
Installation
Ubuntu
- Java installation:
# Shinyproxy requires Java 8 (or higher)
$ sudo apt-get -y update
$ sudo apt-get -yq install \
default-jre \
default-jdk
# Check version
$ java -version
- Download the most recent version shinyproxy from the official donwload page based on the platform. For Ubuntu (Debian Linux), download the
deb
file:
# Shinyproxy latest at the time of writing this post
$ export SHINYPROXY_VERSION="3.0.2"
# Download and install deb file
$ wget https://www.shinyproxy.io/downloads/shinyproxy_${SHINYPROXY_VERSION}_amd64.deb
$ sudo apt install ./shinyproxy_${SHINYPROXY_VERSION}_amd64.deb
$ rm shinyproxy_${SHINYPROXY_VERSION}_amd64.deb
Amazon Linux 2
- Java installation:
# Enable yum repostory
$ sudo amazon-linux-extras enable corretto8
# Install amazon corretto 8 as a full JDK
$ sudo yum install -y java-1.8.0-amazon-corretto-devel
$ java -version
- Download the most recent version shinyproxy from the official donwload page based on the platform. For Amazon linux 2, download the
rpm
file:
$ export SHINYPROXY_VERSION="3.0.2"
# Downloads RPM package file to current directory
$ sudo wget https://www.shinyproxy.io/downloads/shinyproxy_${SHINYPROXY_VERSION}_x86_64.rpm
$ sudo yum localinstall -y ./shinyproxy_${SHINYPROXY_VERSION}_x86_64.rpm
$ sudo rm ./shinyproxy_${SHINYPROXY_VERSION}_x86_64.rpm
# Check installation
$ sudo systemctl status shinyproxy
ShinyProxy Configurations
ShinyProxy is configured using the application.yml
file. Regardless of whether we’re using Ubuntu or Amazon Linux 2, we should find this configuration file in the same location:
$ sudo nano /etc/shinyproxy/application.yml
The file below is the default configuration created by installing ShinyProxy:
proxy:
title: Open Analytics Shiny Proxy
logo-url: https://www.openanalytics.eu/shinyproxy/logo.png
landing-page: /
heartbeat-rate: 10000
heartbeat-timeout: 60000
port: 8080
authentication: simple
admin-groups: scientists
# Example: 'simple' authentication configuration
users:
- name: jack
password: password
groups: scientists
- name: jeff
password: password
groups: mathematicians
port-range-start: 20000
specs:
- id: 01_hello
display-name: Hello Application
description: Application which demonstrates the basics of a Shiny app
container-cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
container-image: openanalytics/shinyproxy-demo
access-groups: [ scientists, mathematicians ]
- id: 06_tabsets
container-cmd: [ "R", "-e", "shinyproxy::run_06_tabsets()" ]
container-image: openanalytics/shinyproxy-demo
access-groups: scientists
logging:
file:
name: shinyproxy.log
For an in-depth explanation of each directive in the configuration file, refer to the official ShinyProxy documentation. Here are some key points to understand for a basic setup:
Port: We have set the
port
directive to 8080, which corresponds to the port we established during our EC2 instance creation.Authentication: By default, ShinyProxy allows us to specify users and their access levels via the
authentication
directive. While this is a straightforward approach, you might want to consider more secure authentication methods. Thankfully, ShinyProxy is compatible with various authentication technologies. For integrating with AWS Cognito, check out this detailed guide.Docker: With the
docker
directive, we can define theurl
andport
to communicate with the docker daemon.Template Groups: The
template-groups
directive allows us to categorize apps in the template into groups, allowing different user groups to access different applications.Apps: Every application that ShinyProxy serves requires its own configuration under the
specs
section. We will delve into this after containerizing our Shiny and Dash applications in the subsequent section.
Test ShinyProxy
To ensure ShinyProxy is working as expected, we can pull the demo application image from the OpenAnalytics repository:
$ sudo docker pull openanalytics/shinyproxy-demo
Restart ShinyProxy with the following commands:
# Ubuntu
$ sudo service shinyproxy restart
# Amazon Linux 2
$ sudo systemctl restart shinyproxy
We should now be able to access the ShinyProxy login page at the following URL: http://elastic-ip-address:8080
. The login credentials are contained in the sample configuration file above.
Useful Commands for ShinyProxy
Ubuntu
$ sudo service shinyproxy status
$ sudo service shinyproxy start
$ sudo service shinyproxy stop
$ sudo service shinyproxy restart
Amazon Linux 2
$ sudo systemctl status shinyproxy
$ sudo systemctl start shinyproxy
$ sudo systemctl stop shinyproxy
$ sudo systemctl restart shinyproxy
Step 5: Nginx
We will utilize nginx to set up a reverse proxy for our application. As detailed in my earlier post, the motivation behind this architectural decision is multifaceted. Fundamentally, off-loading SSL encryption tasks to a separate entity, like nginx, ensures efficient resource utilization and optimized performance. This setup is particularly beneficial for a tool like ShinyProxy, where the primary focus is on serving content or running applications. Adopting this recommended approach ensures both enhanced security and improved application responsiveness even in our non-enterprise deployment setting.
Installation
Ubuntu
$ sudo apt-get install -y nginx
$ sudo nginx -v
Amazon Linux 2
# Enable Extra Packages for Enterprise Linux (EPEL) repository
$ sudo amazon-linux-extras enable epel
$ sudo yum install -y epel-release
$ sudo yum install -y nginx
$ sudo nginx -v
Domain Name and SSL Encryption (Recommended)
A domain name is simply the name of a website. Examples of domain names include google.com
, wikipedia.org
, and youtube.com
. If we wish to use a domain name rather than the raw IPv4/elastic IP address of our EC2 instance, we need to purchase a domain name. The steps to obtain and set up a custom domain with Google Domain are covered in my previous post.
Once we purchased a domain name, we need to edit the DNS records to point it to our EC2 instance at the elastic IP address. The steps are also covered in the post linked above.
SSL Encryption
The default data stream transferring over HTTP is not encrypted, so it recommended that we use Hypertext transfer protocol secure (HTTPS). HTTPS uses an encryption protocol that is called Transport Layer Security (TLS). In order to switch from HTTP to HTTPS, we first need to obtain an TSL/SSL certificate, which is a data file hosted in a website’s server that contains the website’s public key and identity, along with other related information. We can usually obtain the certificate from a certificate authority (CA), but this approach has a cost to it. Instead, in this post, we will obtain our certificate from the Let’s Encrypt certificate authority, which is a non-profit entity offering digital certificates for free to anyone who owns a domain name, e.g. ourdomain.com
or subdomain.ourdomain.com
.
Ubuntu
Install certbot based on the software (Nginx) and the operating system (Ubuntu). The instructions for installing certbot uses snaps
, which is pre-installed and ready to go on all recent releases of Ubuntu (Ubuntu 16.04 LTS or later, including Ubuntu 22.04 LTS and Ubuntu 23.04).
# Install certbot
$ sudo snap install --classic certbot
# Create a symbolic link to be able to run certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
Amazon Linux 2
For Amazon Linux 2, a working installation of certbot can be found in this github issue:
# Download (non-verbose) the RPM package based on CentOS 7
$ sudo wget -O ~/epel.rpm –nv https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
# Install the EPEL repo
$ sudo yum install -y ./epel.rpm
# Install python2-based certbot
$ sudo yum install -y python2-certbot-nginx.noarch
$ sudo rm epel.rpm
Certbot Commands
For both operating systems, we can run the following command to obtain a certificate for our domain:
$ sudo certbot certonly --nginx
Upon following the prompts to enter our email address and our domain name, we should see the following output:
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/ourdomain.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/ourdomain.com/privkey.pem
Your certificate will expire on 2024-01-05. To obtain a new or
tweaked version of this certificate in the future, simply run
certbot again. To non-interactively renew *all* of your
certificates, run "certbot renew"
- If you like Certbot, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le
To test auto-renew, replacing the ourdomain.com
with your domain name:
$ sudo certbot renew --cert-name ourdomain.com --dry-run
We should see the following output:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/ourdomain.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator nginx, Installer nginx
Account registered.
Simulating renewal of an existing certificate for ourdomain
Performing the following challenges:
http-01 challenge for ourdomain
Using default addresses 80 and [::]:80 ipv6only=on for authentication.
Waiting for verification...
Cleaning up challenges
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed with reload of nginx server; fullchain is
/etc/letsencrypt/live/ourdomain/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/ourdomain/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
To get a list of all certificates:
$ sudo certbot certificates
For both operating systems, the certification, chain, and key file should be saved to the following directory:
$ sudo -i
# Substitute for 'ourdomain'
$ cd /etc/letsencrypt/live/ourdomain.com/
To delete a certificate by domain name:
$ sudo certbot delete --cert-name ourdomain.com
Nginx Configurations
The nginx
configuration files are located in the etc
(system configuration files) directory:
$ cd /etc/nginx
$ ls
The results of ls
may differ depending on the AMI (and thus the operating system) that we used.
Ubuntu
With Ubuntu, the default installation of nginx
would create a sites-avalable
and a sites-enabled
directory. Navigate to the /etc/nginx
directory, we should see at least the following sub-directories: conf.d
, sites-enabled
, nginx.conf
, sites-available
(if not, we can create them). Change directory to the sites-available
sub directory and create a new configuration file specifically for ShinyProxy:
$ sudo service nginx stop
$ cd sites-available
$ sudo nano shinyproxy.conf
The ShinyProxy official documentation has an example configuration for nginx
. Copy and paste the following block of directives in the shinyproxy.conf
file:
server {
listen 80;
# Enter ourdomain.com or subdomain.ourdomain.com, keeping shinyproxy as a subdomain
server_name shinyproxy.ourdomain.com;
rewrite ^(.*) https://$server_name$1 permanent;
}
server {
listen 443;
# Enter ourdomain.com or subdomain.ourdomain.com, keeping shinyproxy as a subdomain
server_name shinyproxy.ourdomain.com;
access_log /var/log/nginx/shinyproxy.access.log;
error_log /var/log/nginx/shinyproxy.error.log error;
ssl on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# Enter the paths to our ssl certificate and key file created in the previous subsection
ssl_certificate /etc/letsencrypt/live/ourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
In the configuration file above, substitute all ourdomain.com
with our custom domain and modify the paths to the ssl_certificate
and ssl_certificate_key
directives:
# For ssl_certificate
$ /etc/letsencrypt/live/ourdomain.com/fullchain.pem
# For ssl_certificate_key
$ /etc/letsencrypt/live/ourdomain.com/privkey.pem
Next, we need to create a shortcut (symbolic link) inside the sites-enabled
directory. The reason for this is that nginx
does not look at sites-available
but only the sites-enabled
directory in the /etc/nginx/nginx.conf
configuration file. We create the .conf
files inside sites-available
and create a shortcut inside sites-enabled
to access it. One benefit of this is that, to temporarily deactivate access to ShinyProxy, we only have to delete the shortcut but not the actual configuration file in sites-available
:
$ cd /etc/nginx/sites-enabled
# Use absolute path
$ sudo ln -s /etc/nginx/sites-available/shinyproxy.conf /etc/nginx/sites-enabled/
Important: By default, there will be default
configuration files located in the sites-available
and sites-enabled
directories, which we must remove:
$ cd /etc/nginx/sites-enabled && sudo rm default
$ cd /etc/nginx/sites-available && sudo rm default
Amazon Linux 2
On RedHat, CentOS, and Fedora, the default nginx
installation does not create directories such as sites-available
and sites-enabled
. For these operating systems, the standard directory to store configuration files (ending in .conf
) is /etc/nginx/conf.d/*.conf
.
Furthermore, within the /etc/nginx/nginx.conf
configuration file, it’s essential to include the directive include /etc/nginx/conf.d/*.conf;
within the http
block. This ensures that nginx
recognizes and incorporates any .conf
files located in the /etc/nginx/conf.d
directory. By default, this directive is included, but it is always good to double-check:
$ sudo nano /etc/nginx/nginx.conf
Ensure that include /etc/nginx/conf.d/*.conf;
is in the http
block. Then, we need to place the nginx
configuration file in the conf.d
directory:
$ sudo systemctl stop nginx
$ cd /etc/nginx/conf.d
$ sudo nano shinyproxy.conf
Write the same block of directives in the shinyproxy.conf
file:
server {
listen 80;
...
}
server {
listen 443;
...
location / {
proxy_pass http://127.0.0.1:8080/;
...
}
Again, make sure to substitute all ourdomain.com
with our custom domain and modify the paths to the ssl_certificate
and ssl_certificate_key
directives. Remove the default.d
configuration directory:
$ cd /etc/nginx && sudo rm -r default.d
Testing Syntax
For both operating systems, to test if the configuration files are syntactically correct, run the following:
$ sudo nginx -t
This should output the results below if the configuration test has passed:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
We are now ready to restart nginx:
# Ubuntu
$ sudo service nginx restart
# Amazon Linux 2
$ sudo systemctl restart nginx
If restart fails, try deleting all running processes on port 80 first:
$ sudo lsof -t -i :80 | xargs sudo kill
Configurations Breakdown (Optional)
When nginx
proxies a request, it does the following:
- Forwards the Request: When we (client) try to access a website or server (like an application on our EC2 instance), nginx takes our request and sends it to the specific server where the website or app is hosted.
- Retrieves the Response: After sending our request,
nginx
waits for the server to respond. This response contains the information the client asked for, like a webpage or app data. - Delivers the Response Back to Client: Once
nginx
receives the response from the server, it then sends this information back to our device. This is how we see the website or can interact with the app.
A few details on the configuration file above:
Section/Directive | Description |
---|---|
server (first block) | This server block listens on port 80, which is the standard port for HTTP. It’s primarily responsible for redirecting all HTTP traffic to HTTPS for security reasons. |
listen 80 | The server listens for incoming connections on port 80 (HTTP). |
server_name | This defines the domain name for this server block. Here, it’s set to shinyproxy.ourdomain.com . |
rewrite | This directive is used to redirect (permanent 301 redirect) all traffic from HTTP to HTTPS for the same domain. |
server (second block) | This server block listens on port 443, which is the standard port for HTTPS (secure HTTP). |
listen 443 | The server listens for incoming connections on port 443 (HTTPS). |
access_log and error_log | These directives specify the locations of the access and error logs for nginx. |
ssl on | This enables SSL for this server block. |
ssl_protocols | This specifies which TLS protocols are allowed. It’s set to allow TLS versions 1, 1.1, and 1.2. |
ssl_certificate and ssl_certificate_key | These directives specify the paths to the SSL certificate and private key files. |
location / | This block defines how to respond to requests for the root URL (/ ). It’s set up to proxy these requests to another server running on the same machine on port 8080. |
proxy_pass | Specifies the address of the proxied server to which requests should be passed. See more details here. |
proxy_http_version | Sets the HTTP protocol version for proxying. See more details here. |
proxy_set_header (multiple) | These directives set the values of headers that will be passed to the proxied server. They’re essential for WebSocket support and to provide the proxied server with original client information. |
proxy_redirect | This directive is set to off , which means that nginx will not modify the “Location” and “Refresh” header fields in a proxied server’s response. |
proxy_read_timeout | Defines a timeout for reading a response from the proxied server. The timeout is set to 600 seconds. |
Update ShinyProxy Configuration (Important)
Stop ShinyProxy:
# Ubuntu
$ sudo service shinyproxy stop
# Amazon Linux 2
$ sudo systemctl stop shinyproxy
Forwarded Header
According to the ShinyProxy documentation, when ShinyProxy is accessed through a reverse proxy that uses HTTPS, the direct connection to ShinyProxy is often over HTTP. This discrepancy can cause ShinyProxy to generate incorrect URLs, especially redirect URIs, using the “http” scheme instead of “https”. To address this, reverse proxies use the X-Forwarded-* headers (X-Forwarded-For
and X-Forwarded-Proto
) to specify the original protocol used by the client to access ShinyProxy.
In the application.yml
configuration file, we need to set the forward-headers-strategy
to native
, so that ShinyProxy uses these headers to generate the correct URLs with the “https” scheme. In addition, we also need to set secure-cookies
to true
to set the secure flag on all session cookies.
$ sudo nano /etc/shinyproxy/application.yml
Add the following to the configuration yaml file:
server:
forward-headers-strategy: native
secure-cookies: true
More details can be found in the FAQ of the ShinyProxy documentation.
Restrict ShinyProxy to Only Bind to Local Host
Because nginx
will pass requests to port 8080 on the loopback interface (127.0.0.1
) based on the proxy_pass
directive in the nginx
configuration file. The ShinyProxy documentation recommends that we restrict ShinyProxy to bind only on 127.0.0.1
(and not to the default 0.0.0.0
). We can set the bind-address
in the application.yml
configuration file:
proxy:
[...] # Other proxy settings
bind-address: 127.0.0.1 # Add this under the proxy entry
Container Logging
We can add the proxy.container-log-path
directive to the application.yml
configuration file to specify the path where the container logs will be written. This is useful for debugging purposes:
proxy:
[...] # Other proxy settings
bind-address: 127.0.0.1
container-log-path: /var/log/shinyproxy # Add this under the proxy entry
Once enabled, ShinyProxy will create log files with the following naming conventions:
<specId>_<proxyId>_<startupTime>_stdout.log
<specId>_<proxyId>_<startupTime>_stderr.log
For more details, see the ShinyProxy documentation.
Updated Configuration
The updated default configuration should look like this:
proxy:
title: Open Analytics Shiny Proxy
logo-url: https://www.openanalytics.eu/shinyproxy/logo.png
landing-page: /
heartbeat-rate: 10000
heartbeat-timeout: 60000
port: 8080
bind-address: 127.0.0.1 # Added the bind-address directive
container-log-path: /var/log/shinyproxy # Added the container-log-path directive
docker:
port-range-start: 20000
authentication: simple
admin-groups: scientists
users:
- name: jack
password: password
groups: scientists
- name: jeff
password: password
groups: mathematicians
specs:
- id: 01_hello
display-name: Hello Application
description: Application which demonstrates the basics of a Shiny app
container-cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
container-image: openanalytics/shinyproxy-demo
access-groups: [ scientists, mathematicians ]
- id: 06_tabsets
container-cmd: [ "R", "-e", "shinyproxy::run_06_tabsets()" ]
container-image: openanalytics/shinyproxy-demo
access-groups: scientists
server:
forward-headers-startegy: native # Added the forward-headers-strategy directive
secure-cookies: true # Added the secure-cookies directive
logging:
file:
shinyproxy.log
Upon restart, Shinyproxy should now be accessible at https://ourdomain.com
or https://subdomain.ourdomain.com
:
# Ubuntu
$ sudo service shinyproxy restart
# Amazon Linux 2
$ sudo systemctl restart shinyproxy
Useful Commands for Nginx
Ubuntu
$ sudo service nginx status
$ sudo service nginx start
$ sudo service nginx stop
$ sudo service nginx restart
Amazon Linux 2
$ sudo systemctl status nginx
$ sudo systemctl start nginx
$ sudo systemctl stop nginx
$ sudo systemctl restart nginx
Step 6: Containerize Applications
To build the images for our applications, we have a couple of options:
Using EC2 Host: We can directly utilize the compute resources of our EC2 host. For small applications, this option is both straight forward and sufficient. However, for more involved applications, this might not be optimal as the resources needed during build-time and run-time can vary significantly. It is also not ideal to have our EC2 host tied up with the build process, potentially running into memory issues.
Local Machine Builds: Alternatively, we can harness the power of our local machine (or, for the matter, any other machine that supports docker) to build the images. Once built, these images containing our applications can be pushed to the Elastic Container Registry (ECR). We can then pull these images from ECR to our EC2 host for deployment. This option is more flexible, because we can leverage the resource of any machine to build the images.
In the sections on In-Line Policy for IAM User Group, we outlined the necessary permissions granted to the IAM user to facilitate these tasks. The upcoming subsections requires that docker
is installed on both the EC2 (e.g., ShinyproxyInstance
) host and our local machine. In addition, both the machine used for building images and the EC2 instance should have the AWS Command Line Interface configured
We illustrate the deployment of two simple applications: one built with R Shiny and the other with Python Dash. Both applications will leverage a bash script for building the images.
R Shiny
On our local machine, create a directory for our Shiny application:
.
└── shiny_app
├── Dockerfile.shiny
└── app.R
The app.R
script contains the code for our Shiny application. This is a simple application that accomplishes the following:
Simulate predictors \(x_{1}\) through \(x_{6}\) from i.i.d normal distributions with mean 0 and standard deviation 1
The response variable \(y\) is deterministically calculated using a linear mode with predefined coefficients, only using predictors \(x_{1}\) through \(x_{3}\) (i.e., coefficients on predictors \(x_{4}\) through \(x_{6}\) should be zeros)
The user can select the regularization parameter \(\lambda\) for the Lasso and Ridge regression models using a slider
Plot the coefficients of the Lasso and Ridge regression models to better understand the effects of regularization on significance of model coefficients
library(bs4Dash)
library(shiny)
library(glmnet)
library(plotly)
# Simulation function -----------------------------------------------------
sumulate_data <- function(n = 1000) {
# Predictor variables that are used to simulate the response variable
significant_predictors <- lapply(
X = c("x1", "x2", "x3"),
FUN = function(x) {
rnorm(n = n, mean = 0, sd = 1)
}
)
# Predictor variables that are not used to simulate the response variable
insignificant_predictors <- lapply(
X = c("x4", "x5", "x6"),
FUN = function(x) {
rnorm(n = n, mean = 0, sd = 1)
}
)
# True parameter values
b_1 <- 12.34
b_2 <- 23.45
b_3 <- 2.45
# Simulate the response variable
intercept <- 20
y <- 20 + b_1 * significant_predictors[[1]] + b_2 * significant_predictors[[2]] + b_3 * significant_predictors[[3]]
# Standardize the response variable
y <- (y - mean(y)) / sd(y)
data <- data.frame(
y = y,
x1 = significant_predictors[[1]],
x2 = significant_predictors[[2]],
x3 = significant_predictors[[3]],
x4 = insignificant_predictors[[1]],
x5 = insignificant_predictors[[2]],
x6 = insignificant_predictors[[3]]
)
return(data)
}
# Train models ------------------------------------------------------------
train_models <- function(data, regularization_strength) {
predictors <- c("x1", "x2", "x3", "x4", "x5", "x6")
# Train models
lasso <- glmnet(
x = as.matrix(data[, predictors]),
y = data$y,
alpha = 1,
lambda = regularization_strength
)
ridge <- glmnet(
x = as.matrix(data[, predictors]),
y = data$y,
alpha = 0,
lambda = regularization_strength
)
# Coefficients
coefs <- data.frame(
predictor = rep(x = predictors, times = 2),
beta = c(coef(ridge)[predictors, ], coef(lasso)[predictors, ]),
model = rep(x = c("Ridge", "Lasso"), each = length(predictors))
)
return(coefs)
}
# UI ----------------------------------------------------------------------
ui <- bs4DashPage(
title = "Shiny Application",
header = bs4DashNavbar(disable = TRUE),
sidebar = bs4DashSidebar(disable = TRUE),
body = bs4DashBody(
bs4Card(
sliderInput(
inputId = "regularization_strength",
label = "Use the slider to change the regularization strength for Lasso and Ridge regression models:",
min = 0,
max = 1,
value = 0.2
),
title = "Regularization Strength",
width = 12
),
bs4Card(
plotlyOutput(
outputId = "box_plot"
),
width = 12
)
),
controlbar = dashboardControlbar(diable = TRUE)
)
# Server ------------------------------------------------------------------
server <- function(input, output) {
# Simulate 200 data sets, each with 200 observations
data <- reactive({
simulations <- lapply(
X = 1:200,
FUN = function(x) {
sumulate_data(n = 200)
}
)
})
# Train models for each data set
results <- reactive({
models <- lapply(
X = data(),
FUN = function(x) {
train_models(data = x, regularization_strength = input$regularization_strength)
}
)
# Combine results (row-bind)
results <- do.call(rbind, models)
})
# Box plot
output$box_plot <- renderPlotly({
# Plot
plot_ly(
data = results(),
x = ~predictor,
y = ~beta,
color = ~model,
type = "box"
) %>%
layout(
title = "Box plot",
xaxis = list(title = "Model"),
yaxis = list(title = "Coefficient")
)
})
}
shinyApp(ui, server)
The Dockerfile.shiny
file allows us to package the application code for deployment. A Dockerfile is a text file that contains instructions to assemble an image by layer. We build from the r-base
image, which is a Debian-based Linux image that contains the latest version of R. See the Rocker Project for a suite of other Docker images for R.
FROM r-base:4.3.2
WORKDIR /shiny_app
COPY app.R ./
# System dependencies for R packages (bs4Dash & plotly)
RUN apt-get update && \
apt-get install -y libcurl4-openssl-dev libssl-dev && \
rm -rf /var/lib/apt/lists/*
# Use the install.r utility script from the littler package
RUN install.r --ncpus -1 shiny bs4Dash glmnet plotly
EXPOSE 3838
CMD ["R", "-q", "-e", "shiny::runApp('/shiny_app/app.R', port = 3838, host = '0.0.0.0')"]
Dash
Create a new directory for the Dash application:
.
├── dash_app
│ ├── Dockerfile.dash
│ ├── app.py
│ ├── entrypoint.py
│ └── requirements.txt
The app.py
file contains the Dash application code, which is a sentiment analysis app. The app allows users to enter text and analyze the sentiment of the text. The sentiment analysis is performed using the TextBlob library, which is a Python library for processing textual data. The front-end is built with Dash, a Python framework for building web applications.
import os
import dash
from dash import html, dcc
from dash.dependencies import Input, Output
from textblob import TextBlob
app = dash.Dash(
__name__,
suppress_callback_exceptions=True,
requests_pathname_prefix=os.environ['SHINYPROXY_PUBLIC_PATH'],
routes_pathname_prefix= os.environ['SHINYPROXY_PUBLIC_PATH']
)
server = app.server
app.layout = html.Div([
html.H1('Simple Sentiment Analysis App'),
dcc.Textarea(
id='text-input',
value='',
style={'width': '100%', 'height': 100},
placeholder='Enter text here...'
),
html.Button('Analyze', id='analyze-button'),
html.Div(id='output-container')
])
@app.callback(
Output('output-container', 'children'),
[Input('analyze-button', 'n_clicks')],
[dash.dependencies.State('text-input', 'value')]
)
def update_output(n_clicks, input_value):
if n_clicks and input_value:
analysis = TextBlob(input_value)
sentiment_polarity = analysis.sentiment.polarity
sentiment = 'Positive' if sentiment_polarity > 0 else 'Negative' if sentiment_polarity < 0 else 'Neutral'
return f'Sentiment: {sentiment} (Polarity: {sentiment_polarity})'
return 'Enter some text and click Analyze.'
if __name__ == '__main__':
app.run_server(debug=True)
Dash requires the knowledge of the path used to access the app. ShinyProxy makes this path available as an environment variable SHINYPROXY_PUBLIC_PATH
. Next, the requirements.txt
file lists the Python libraries required for the Dash app:
textblob==0.17.1
dash==2.14.1
plotly==5.18.0
guniconrn==21.2.0
We create a entrypoint.py
file to start the Dash application using gunicorn, which is a Python WSGI HTTP Server for UNIX. This is the recommended way to deploy Dash applications in production. For more information on the entrypoint script, see the gunicorn documentations on custom application
import os
from typing import Dict, Any
from gunicorn.app.base import BaseApplication
# Importing the Dash app
from app import server as application
class StandaloneApplication(BaseApplication):
"""
A standalone application to run a Dash app with Gunicorn. This
class is designed to configure and run a Dash application using
the Gunicorn WSGI HTTP server.
Attributes
----------
application : Any
The Dash application instance to be served by Gunicorn.
options : Dict[str, Any], optional
A dictionary of configuration options for Gunicorn.
"""
def __init__(self, app: Any, options: Dict[str, Any] = None):
"""
Constructor for StandaloneApplication with a Dash app and options.
Parameters
----------
app : Any
The Dash application instance to serve.
options : Dict[str, Any], optional
A dictionary of Gunicorn configuration options.
"""
self.options = options or {}
self.application = app
super(StandaloneApplication, self).__init__()
def load_config(self):
"""
Load the configuration from the provided options. This method
extracts the relevant options from the provided dictionary and
sets them for the Gunicorn server.
"""
config = {key: value for key, value in self.options.items()
if key in self.cfg.settings and value is not None}
for key, value in config.items():
self.cfg.set(key.lower(), value)
def load(self) -> Any:
"""
Return the application to be served by Gunicorn. This method is
required by Gunicorn and is called to get the application instance.
Returns
-------
Any
The Dash application instance to be served.
"""
return self.application
if __name__ == '__main__':
# Retrieve the SHINYPROXY_PUBLIC_PATH from environment variable
public_path = os.environ['SHINYPROXY_PUBLIC_PATH']
options = {
'bind': '0.0.0.0:8050',
'workers': 4,
'env': {'SHINYPROXY_PUBLIC_PATH': public_path},
}
StandaloneApplication(application, options).run()
The Dockerfile.dash
in the dash_app
directory would be similar to Dockerfile.shiny
, but tailored to the Dash application:
FROM python:3.10.13-slim-bullseye
WORKDIR /dash_app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY app.py entrypoint.py ./
EXPOSE 8050
CMD ["python3", "entrypoint.py"]
Build and Push (Local Machine)
With the application source files created, we can build the images and push the images to Elastic Container Registry (ECR), which is an AWS managed container registry. Create a new private ECR repository by following the steps here:
Next, we will create a bash script build_and_push.sh
to automate the following steps:
Build the docker images on the local machine
Push the docker images to ECR
On windows, we would need other means to run these steps. Note that the following script assumes that the AWS CLI is installed and configured on the local machine, and that the user has sudo privilege.
#!/bin/bash
# Check if arguments are passed, otherwise prompt
if [ "$#" -eq 4 ]; then
docker_image_path="$1"
image_tag="$2"
ecr_repo="$3"
iam_user="$4"
else
read -p "Enter the absolute path to the docker image: " docker_image_path
read -p "Enter the custom image tag name: " image_tag
read -p "Enter the ECR repository name: " ecr_repo
read -p "Enter the IAM user name: " iam_user
fi
# Set build context as the parent directory of the docker image
build_context=$(dirname "$docker_image_path")
# [-z string]: True if the string is null (an empty string)
if [ -z "$docker_image_path" ] || [ -z "$image_tag" ] || [ -z "$ecr_repo" ] || [ -z "$iam_user" ]; then
echo "Please provide the docker image path, image tag, ECR repository name and IAM user name"
exit 1
fi
# AWS variables
account_id=$(aws sts get-caller-identity --profile "$iam_user" --query Account --output text)
region=$(aws configure --profile "$iam_user" get region)
image_name="$account_id.dkr.ecr.$region.amazonaws.com/$ecr_repo:$image_tag"
# Login to ECR based on 'https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html'
aws ecr get-login-password --profile "$iam_user" --region "$region" | docker login --username AWS --password-stdin "$account_id.dkr.ecr.$region.amazonaws.com"
docker build -f "$docker_image_path" \
-t "$image_name" "$build_context"
docker push "$image_name"
The script can be run as follows:
$ bash build_and_push.sh /path/to/Dockerfile YOUR_IMAGE_TAG YOUR_ECR_REPO YOUR_IAM_USER
Once the processes are completed, the images should be available in the ECR repository:
We can check that these images exist with:
$ aws ecr describe-images --profile YOUR_IAM_USER --repository-name YOUR_ECR_REPO
Pull and Run (EC2 Instance)
With our containerized applications built and pushed to ECR, we are now ready to pull the images and run them on our EC2 instance. In the terminal of the EC2 instance, configure the AWS CLI with the IAM user credentials:
With Amazon Linux 2 or the Amazon Linux AMI, THE AWS CLI is pre-installed
For other operating systems, follow the steps here
# Follow the prompts to enter the IAM user credentials
$ aws configure --profile YOUR_IAM_USER
Login to docker with the AWS credentials:
$ aws ecr get-login-password \
--profile YOUR_IAM_USER \
--region YOUR_REGION | \
sudo docker login \
--username AWS \
--password-stdin \
YOUR_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com
Pull the images from ECR based on the image uri’s, which can be found in the ECR repository. They following the following format: YOUR_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/YOUR_ECR_REPO:YOUR_IMAGE_TAG
.
$ sudo docker pull IMAGE_URI
Now these images should be available on the EC2 instance:
$ sudo docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
722696965592.dkr.ecr.us-east-1.amazonaws.com/shinyproxy shiny_app 203637949297 5 minutes ago 1.05GB
722696965592.dkr.ecr.us-east-1.amazonaws.com/shinyproxy dash_app e69d02fa8bb4 6 minutes ago 316MB
ShinyProxy Specs
Finally, we need to update the ShinyProxy configuration file to use the new images:
# Ubuntu
$ sudo service shinyproxy stop
# Amazon Linux 2
$ sudo systemctl stop shinyproxy
Open the ShinyProxy configuration file:
$ sudo nano /etc/shinyproxy/application.yml
proxy:
title: DashWu
logo-url: https://www.openanalytics.eu/shinyproxy/logo.png
landing-page: /
heartbeat-rate: 10000
heartbeat-timeout: 60000
port: 8080
bind-address: 127.0.0.1
container-backend: docker
container-log-path: /var/log/shinyproxy
docker:
port-range-start: 20000
authentication: simple
admin-groups: admin_users
users:
- name: yang
password: password
groups: admin_users
- name: other
password: password
groups: other_users
specs:
- id: shiny-app
display-name: Ridge & Lasso App
description: Demonstrate the effects of regularization on regularized linear models
container-image: YOUR-AWS-ACCOUNT-ID.dkr.ecr.YOUR-REGION.amazonaws.com/YOUR-ECR-REPO:YOUR-IMAGE-TAG
access-groups: [admin_users, other_users] # Accessible by all user groups
- id: dash-app
display-name: Sentiment Analysis App
description: Analyze the sentiments of input text
port: 8050
container-image: YOUR-AWS-ACCOUNT-ID.dkr.ecr.YOUR-REGION.amazonaws.com/YOUR-ECR-REPO:YOUR-IMAGE-TAG
target-path: "#{proxy.getRuntimeValue('SHINYPROXY_PUBLIC_PATH')}"
server:
forward-headers-startegy: native
secure-cookies: true
logging:
file:
shinyproxy.log
After updating, start ShinyProxy:
# Ubuntu
$ sudo service shinyproxy start
# Amazon Linux 2
$ sudo systemctl start shinyproxy
We can now access the ShinyProxy landing page by entering the domain or subdomain in the browser.