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:

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:

Diagram Built with Lucidchart

The sections below provide some additional details on the most important components in this infrastructure.

Mandatory Parameters

  1. 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.
  2. KeyName:
    • Description: Name of an existing EC2 Key Pair to enable SSH access to the instances.

Parameters with Default Values

  1. EnvironmentName:
    • Description: An environment name that will be prefixed to resource names.
    • Default: shinyproxy
  2. VpcCIDR:
    • Description: The IP range (CIDR notation) for this VPC.
    • Default: 10.0.0.0/16
  3. PublicSubnet1CIDR:
    • Description: The IP range (CIDR notation) for the public subnet in the first Availability Zone.
    • Default: 10.0.0.0/24
  4. InstanceType:
    • Description: The type of EC2 instance to be provisioned for ShinyProxy.
    • Default: t3.medium
    • Allowed Values: t3.micro, t3.small, t3.medium
  5. ShinyProxyVolumeSize:
    • Description: Volume size for the ShinyProxy Instance in GiB.
    • Default: 30
    • Range: 8 to 100 GiB
  6. 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))
  7. 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)
  8. 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.

ResourceDescription
VPCThe main virtual private cloud resource.
PublicSubnet1A subnet with resources accessible from the internet.

Internet Gateway

This component ensure that resources within the VPC can connect to the internet.

ResourceDescription
InternetGatewayEnables 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.

ResourceDescription
PublicRouteTableRoute 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.

ResourceDescription
ShinyProxyInstanceThe primary EC2 instance, hosting ShinyProxy.

Security Group Configuration

Security Groups act as virtual firewalls, controlling inbound and outbound traffic to resources.

ResourceDescription
ShinyProxySecurityGroupControls 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.

ResourceDescription
ShinyProxyEIPA 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 is 192.168.1.1, we can enter 192.168.1.1/32 as the value for MyIP.

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 is ubuntu.

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 the url and port 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

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:

  1. 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.
  2. 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.
  3. 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/DirectiveDescription
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 80The server listens for incoming connections on port 80 (HTTP).
server_nameThis defines the domain name for this server block. Here, it’s set to shinyproxy.ourdomain.com.
rewriteThis 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 443The server listens for incoming connections on port 443 (HTTPS).
access_log and error_logThese directives specify the locations of the access and error logs for nginx.
ssl onThis enables SSL for this server block.
ssl_protocolsThis specifies which TLS protocols are allowed. It’s set to allow TLS versions 1, 1.1, and 1.2.
ssl_certificate and ssl_certificate_keyThese 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_passSpecifies the address of the proxied server to which requests should be passed. See more details here.
proxy_http_versionSets 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_redirectThis 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_timeoutDefines 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:

  1. 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.

  2. 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.

Related