Ok here is the complete solution for deploying a Streamlit app, from local development to deployment on Render . I managed to deploy my app using the exact steps below. I will try to explain why I use them, along with the errors I encountered along the way.
Audience
This is aimed at someone who, like me, might be intermediate with Python and comfortable with Streamlit, ok with GitHub but not an expert, but is a complete newbie when it comes to deployment outside of Streamlit. I am not a software developer/ computer scientist but rather a mechanical engineer who can program. My explanations will likely lack precision but hopefully you might relate to how I explain things because of my outside perspective.
I still find AWS an intimidating place for deployment of apps so Render is a very attractive alternative for me because it handles build, deploy, provisioning, scaling and metrics for me.
Assumptions
This assumes you have a Render account already, and you can access your Render dashboard.
Also apologies up front: Render doesn’t allow me to paste more than one screenshot in so I included about a dozen screens which got rejected so sorry this is so text-heavy! I am simply copying and pasting from Render here (my post has not yet been approved).
This setup also assumes:
-
You have secrets you want to manage e.g. because you use API keys or have sensitive user names and passwords you store in a secrets file. Streamlit stores its secrets files in secrets.toml
-
You don’t need to create a database in Render because you access a database elsewhere via your credentials you manage in your secrets.toml
file. In my case, I am accessing AWS S3 buckets.
Basic deployment concepts
This section is written for you if you are brand new to the concept of deploying your Streamlit code. If you’re already comfortable with these concepts, feel free to skip straight to the next section Steps.
Firstly, why do we deploy, why does it seem like so much trouble, and how is it different to programming locally? The short answer to this is that Streamlit makes it terrifically easy to publish our code in a way that lets others use it. There’s an awful lot of smart engineering going on behind the scenes to make it so easy. This isn’t necessarily the case on other cloud providers.
This is a limitation of only writing code on our local computer: how do we share it with others so that it runs on their computer which might be set up quite differently to ours?
One of the answers to this is to set up something called a “virtual machine” in the cloud which is like a version of our local computer, but with internals that are expressed in the language of cloud infrastructure. In our local computing environment, we need to have hardware, an operating system (Windows or Mac, say), all our libraries and applications, along with software that maintains it all. Setting up a virtual machine in the cloud is no different.
So part of the process of deploying our code is not only to make it work locally, but to specify the configuration of the virtual machine in the cloud so that our code can run as we expect. A recipe, if you will, for building each virtual machine in each cloud environment. Each cloud provider will have different recipes for building these virtual machines, but their goals are similar overall.
Thus, since each cloud provider uses their own recipe, we can’t necessarily guarantee that the recipe we used in one environment can be used directly in another. Most of the problems we encounter when deploying our code on a new cloud provider will be in running up against the differences in that recipe. Unfortunately, it isn’t always easy to know what those differences are upfront other than reading their literature, doing deep dives in forums (like this one!) or doing it yourself and figuring it out as you go (as I did).
However, there are some similarities that are common to deployment on any cloud provider, such as:
- The libraries your Streamlit app needs to run e.g.
pandas
, scikit learn
, numpy
, streamlit
etc. To avoid incompatibility issues, most of the time we will want to specify versions of these libraries as well. In Streamlit, this is defined in our requirements.txt
file
- I want to call out the runtime separately to the above because the run-time environment is Python but the version we specify is crucial to making our virtual machine environment run as we expect. Put simply, we should specify the same Python version in the cloud as we use on our local machine e.g. Python 3.8.5 etc. otherwise you may come across many incompatibility issues at build time.
- Where your code repository lives. GitHub is a popular choice here.
- Environmental variable management for handling [
key
, value
] pairs that specify configuration parameters for your app
- Secrets management for handling sensitive data like user-names and passwords. I am deliberately making this separate from environmental variable management for reasons I explain below.
- Build and run commands (handled automatically in Streamlit)
- What type of virtual machine (or “instance”) you want to setup. Basically, how powerful you need it to be at the price point you’re willing to pay, and where do you want it set up.
Now, there is a big piece of work not mentioned in the above which is the process of containerising your code. I have not needed to containerise my code (e.g. using Docker) when deploying on Render by following the steps below so I don’t go into it at all in this writeup. However, you may not be able to deploy your app unless you containerise it. Unfortunately, because I have very limited experience with using Docker (or other containerisation services), I won’t be able to say when you’ll need it or not. However, it appears to be related to the kinds of dependencies you need, and how portable you want your container to be across cloud platforms. In my case, I came across lots of Operating System-level version omissions and conflicts in my first Render deploy, and the excellent help desk suggested I firstly fix up my Build commands (upgrade pip install) and if that didn’t work, containerise my app. Fortunately, the first fix works (and is repeated below) but if that doesn’t work for you, you might need to proceed to containerisation. But thankfully, there are lots of good tutorials and other resources available for that.
Ok, now on to the actual steps.
Steps for deploying on Render
-
After you have finished your Streamlit code, activate your preferred virtual environment in your terminal if you haven’t already. I use Conda so the command I use is conda activate streamlit
-
In your terminal Navigate to the code directory on your local computer where you have written your code. Create a requirements.txt file. The command I use is:
pipreqs --encoding utf-8 C:\<your code>
I have found pipreqs
works because it only installs the dependencies I need, while pip freeze
includes many more dependencies that add bulk (and potential fragility) to the rest of the installation process. I also use --encoding utf-8
because my code has emojis that the base pipreqs
command returns an error for because it doesn’t know how to encode emojis.
-
Push your code to your preferred Github repository using your preferred method. Some like using the Command Line Interface, but I am going ok with just drag and drop for the time being. I know shocking right! Don’t worry that still works at this very small scale.
-
Go to your Render dashboard and choose to deploy a webservice
-
Connect the GitHub repository you have just created
-
Fill out the details to suit. Singapore is the best location for me so I chose that. You can specify the exact Python version you want in the “Environmental Variables” section later so just leave as Python 3 for now. We will come back to the Build command and Start commands in later steps.
-
I ran into build problems when I used Render’s default version of Python (which was 3.7) due to being out of date with some of the Streamlit package requirements. If you also encounter the same problems in your Streamlit build process, it may also be due to this Python version issue (e.g. Python 3.7 won’t be able to handle your most recent versions of numpy
, scikit learn
etc.) After much investigation, it turns out all I needed to do was specify which Python version I wanted and you can do that in “Environmental Variables”. Go to “Advanced” and click “Environmental Variables” at the bottom of the screen. We will return to Build command later
-
Under “Add Environmental Variable” type “PYTHON_VERSION” in key
, and in value
type whatever version you want. In my case, 3.8.1
was the version compatible with my Streamlit build.
Environmental variables are a recommended way for specifying configurations that might change between deployment environments or user contexts, but that otherwise don’t alter the underlying code. They are much better than hard coding variables, or committing variables to code repos. They are not recommended for storing secrets, however, because passwords can be leaked when the code is replicated, and they are not generally encrypted at rest. Secrets managers are better for that purpose.
- Add a “Secret File” in the modal (dialogue box).
A few things to note about this, some of which is of particular interest to Streamlit developers:
i) Streamlit expects this file to be called secrets.toml
and it should be located in a .streamlit
folder at the top of the directory. We’ll come to how we do this later when we specify what goes into Render’s Build command
ii) The contents of the file should be in TOML
format
iii) You will notice that the password is encrypted. There are different libraries for doing this in Python and Streamlit so choose the one that is appropriate in your circumstance. What that means is, you should avoid storing the “raw” password in your secrets.toml file and instead encrypt your password then store the encrypted version. When your code extracts SECRET_USER_PASSWORD
from your secrets.toml
file, it is better that it compares encrypted versions, not raw versions of your password.
iv) You access the contents of secrets.toml
within your Streamlit code by calling
user_name = st.secrets("SECRET_USER_NAME")
password = st.secrets("SECRET_USER_PASSWORD")
In my case, I want to access an AWS S3 bucket so I use boto3
, and pass user_name
and password
into my boto3
statement. If we alter the declarations slightly whilst retaining the functionality I’m trying to demonstrate, this might look like:
ACCESS_ID = st.secrets["AWS_ACCESS_ID"]
ACCESS_KEY = st.secrets["AWS_ACCESS_KEY"]
s3 = boto3.client('s3', aws_access_key_id=ACCESS_ID, aws_secret_access_key=ACCESS_KEY)
This arrangement (which is rather nice and simple) will only work in Streamlit if you establish your secrets within a secrets.toml
file that Streamlit expects to be located in a .streamlit
folder in your root directory. You could try to recreate this by declaring your user name and password as “Environmental variables” in Render using the method I described above, then retrieving them using os
commands such as os.getenv()
but as I explain before, this is not the best for security.
- Complete your Build command
mkdir .streamlit; cp /etc/secrets/secrets.toml ./.streamlit/; pip install --upgrade pip && pip install -r requirements.txt
Let’s explain what’s happening here
i) The Build commands instructs Render to build app folders, copy and paste files and so on, and upgrade installation libraries like pip
ii) mkdir .streamlit;
We have told Render that we want to specify our secrets in a secrets.toml
file. Streamlit is expecting this file to be located in a .streamlit
at the ROOT (top) of our folders. Since we are pulling our app code from a GitHub repo, and since our GitHub repo does not have a .streamlit
folder available in it (.foldername
folders are hidden and can’t be pushed to GitHub), we need to tell Render to make this folder.
ii) cp /etc/secrets/secrets.toml ./.streamlit/;
This tells Render to copy the secrets.toml
file we have just attached in the previous step into the newly created .streamlit
folder at the root of our folders
ii) pip install --upgrade pip
I was having lots of build problems using the default version of Python Render uses (3.7) and also with the default version of pip install
Render uses. For instance, I was getting rust
, arrow
and lots of OS-level package errors that were difficult to resolve, leading me down rabbit holes in forums. Luckily, following some advice from Render support, it turned out upgrading pip
to the latest version solved all these problems! Sometimes error messages in your build logs actually can be pure gold , while others can lead you nowhere .
iii) pip install -r requirements.txt
this builds the libraries in accordance with the requirements.txt file
- Complete your Start Command
This is straightforward and is simply streamlit run <appname>.py
. Make sure it is worded exactly the same as what you’ve specified in your GitHub repository otherwise it won’t know which file to run as your main app file.
-
Select whatever instance type you require according to the location, RAM, CPU and price you need. Pick a location that is closest to you to reduce latency.
-
Hit the Create Web Service button
-
You will know if the Build is successful and your service is live by the logs and also when your little deployment badget turns to “live”