self-host a Streamlit app' on a Ubuntu server
Tags: streamlit, ubuntu, githubactions, nginx
Published: 2024-05-10 22:27:18

Building up on [an excellent AI Makerspace video about Langgraph](https://www.youtube.com/watch?v=SEA3eJrDc-k), I've decided to follow their `build-ship-share` mentality and to put the whole thing in a deployed UI over the web.

I've been hearing a lot about [Streamlit](https://docs.streamlit.io/) and how you can easily spin up little POCs (and sometimes bigger things with it): I was not disappointed. However, if you need to deploy your app on a Ubuntu server, the docs are not very talkative about it and all you get from the official website is some links to various unstructured answers in their forum...

So I've taken a shot at it, so you won't waste much time as I did, the example repo is linked [here](https://github.com/yactouat/agentic-rag-demo). The application UI in itself is not very interesting, as I was just toying around with [this part of Streamlit docs](https://docs.streamlit.io/develop/tutorials/llms/llm-quickstart); I want to emphasize in this article on deployment and not on the logic of this app.

(Note: all the listed steps, whether locally or on the remote, were executed on a Ubuntu 24 system)
## run a streamlit app on Ubuntu 24: initial setup

My app (see link above) was as `agentic-rag-demo`, so naturally I've created an `agentic-rag-demo` folder on my remote server home folder.

### copy the repo from my machine to the remote server

From the beginning, my goal was to deploy the app automatically as I make changes to it; however, I found it way more practical to deploy it manually the first time, so that I can rest assured everything works fine before letting the CI/CD take care of the updates, e.g.:

`scp -r <repo> <user>@<remote-server-ip>:/home/<remote-user>/agentic-rag-demo` (replace `<repo>`, `<user>`, `<remote-server-ip>`, and `<remote-user>` with the appropriate values...

The above command copies the repo I had locally into the remote server using `scp`.

### nginx configuration

#### install system deps

- back to your remote server from there on: `sudo apt update && sudo apt upgrade -y`
- `sudo apt install nginx`
- `sudo apt install apache2-utils` (this package is useful for basic auth, as you will see later)

#### folders and permissions

- `sudo mkdir -p /var/www/<domain>` (replace `<domain>` with your domain or subdomain)
- `sudo chown -R www-data:www-data /var/www/<domain>`
- `sudo chmod -R 755 /var/www/<domain>`

#### a note on basic auth in my use-case

As my app uses an OpenAI API key that is stored in the Python env (and I don't know yet how to implement a login functionality in Streamlit), I've opted for simplicity: put the application behind Basic auth, so that the whole Internet does not suck up my OpenAI credits 😁

- `sudo htpasswd -c /etc/nginx/.htpasswd demo` (`demo` is the username, you will be prompted to enter a password)

The command above creates a user/password pair that I will be able to use later in my nginx conf to restrict access to my website.

#### now onto the website conf

- `sudo nano /etc/nginx/sites-available/<domain>.conf`
- write this content into the file =>

```
server {
    listen 80;
    listen [::]:80;

    root /var/www/<domain>;
    index index.html;

    server_name <domain>;

    location / {
        auth_basic "demo app request password @ <email>";
        auth_basic_user_file /etc/nginx/.htpasswd;
    
        proxy_pass http://0.0.0.0:8501;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }

}
```

This configuration allows to access your Streamlit app running locally on port 8501 via Nginx, the `http_upgrade` conf here is for allowing websockets, as Streamlit relies heavily on them.

- `sudo ln -s /etc/nginx/sites-available/<domain>.conf /etc/nginx/sites-enabled/` (enabling the site)
- `sudo nginx -t` (testing the syntax)
- `sudo systemctl reload nginx`

### wait... what about the domain? 🌎

You'll need to configure an A record in your DNS settings to point to your server's IP address for your domain or subdomain. There are plenty of resources on the Internet to help you just do that.

Before moving on, just make sure DNS is propagated. You can use a free DNS checker tool for this.

### the Python part

You'll need to:

- install your Python dependencies
- start the app

For the app to be served... makes sense right?

- `cd agentic-rag-demo`
- `pip install -r requirements.txt`
- `streamlit run agentic_rag.py` (of course your script may have a different name)
- at this point, you should be able to see the dummy content when you navigate to your domain or subdomain on plain HTTP

### TLS

This one is easy, just go to the official certbot website and follow the instructions, they are crystal clear.

### Running the Streamlit app as a Ubuntu service

Now we want to create a service for the app to run in the background, this helps us:

- manage crashes
- start the app on boot
- release the terminal when we start the app from our CI/CD pipeline
- provide us with an easy way of stopping the app in the same CI/CD pipeline before updating it

One particularity here is that we will create a user `systemd` service, which does not require `sudo` privileges, this is way easier to handle in CI/CD pipelines, as we will not have to connect as `root` to stop the app, update it, and relaunch it. Here is how to do it:

- `mkdir -p ~/.config/systemd/user`  
- `nano ~/.config/systemd/user/agentic-rag.service`
- write the following content:  
  
```  
[Unit]  
Description=a service for the agentic rag demo  
After=network.target  
  
[Service]  
Type=simple  
ExecStart=/usr/bin/python3 -m streamlit run /home/<remote-user>/agentic-rag-demo/agentic_rag.py  
Restart=on-failure  
RestartSec=2  
  
[Install]  
WantedBy=default.target  
```

... don't forget to replace the placeholders with the appropriate values.

- now we need our service to be stoppable/start-able from our CI/CD pipeline with our regular user so, for the sake of testing, let's do =>  
  
```bash  
systemctl --user daemon-reload

systemctl --user enable --now agentic-rag.service

systemctl --user start agentic-rag.service

systemctl --user status agentic-rag.service
```  
  
- to stop the service => `systemctl --user stop agentic-rag.service`  , as you can see we never needed `sudo` !
- we are now ready to let the CI/CD pipeline do its magic 🚀

## the GitHub Actions workflow

To be able to use this workflow, you will need to fill in a few repository secrets.

Things are always cooler when deployed automatically, here is my commented pipeline:

```yml
name: cicd  
  
on:  
  push:  
      branches: [master]  
  
jobs:  
  
  deploy-app:  
    runs-on: ubuntu-latest  
    steps:  
    - name: actions/checkout@v4  
      uses: actions/checkout@v4  
    - name: start-app  
      uses: appleboy/ssh-action@v1.0.3  
      with:  
        host: ${{ secrets.SSH_HOST }}  
        key: ${{ secrets.SSH_PRIV_KEY }}  
        port: ${{ secrets.SSH_PORT }}  
        username: ${{ secrets.SSH_USERNAME }}  
        script_stop: true  
        script: |  
              cd ~/agentic-rag-demo
              # we stop the app before copying the files to update it  
              systemctl --user stop agentic-rag.service  
    - name: copy-files  
      uses: appleboy/scp-action@v0.1.7  
      with:  
        host: ${{ secrets.SSH_HOST }}  
        key: ${{ secrets.SSH_PRIV_KEY }}  
        port: ${{ secrets.SSH_PORT }}  
        username: ${{ secrets.SSH_USERNAME }}  
        source: "./*"  
        target: /home/${{ secrets.SSH_USERNAME }}/agentic-rag-demo  
    - name: deploy-app  
      uses: appleboy/ssh-action@v1.0.3  
      env:  
        OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}  
      with:  
        host: ${{ secrets.SSH_HOST }}  
        key: ${{ secrets.SSH_PRIV_KEY }}  
        port: ${{ secrets.SSH_PORT }}  
        username: ${{ secrets.SSH_USERNAME }}  
        script_stop: true  
        envs: OPENAI_API_KEY  
        script: |  
              cd ~/agentic-rag-demo
              # this one below I'm not proud of, I was just lazy to use a virtual env
              python3 -m pip install --break-system-packages -r requirements.txt  
              cp .env.example .env  
              > .env  
              echo "OPENAI_API_KEY=${OPENAI_API_KEY}" >> .env  
              mv .streamlit/config.remote.toml .streamlit/config.toml  
              systemctl --user start agentic-rag.service
```

## wrapping it up

Now you know how to self-host a Streamlit app, spin applications up and keep them coming!