Deploy a Streamlit App on a Custom Domain with Apache2, Ubuntu, systemd, and HTTPS

9 minute read

In this tutorial, we will deploy a Streamlit app on a custom domain using Apache2.

Streamlit is great for building data apps quickly. But after building an app, we often want to host it on our own domain, such as:

https://data.example.com

To do this, we can run Streamlit on an internal port and use Apache2 as a reverse proxy. Apache receives requests from the browser and forwards them to the Streamlit app.

In this guide, we will use:

  • Ubuntu server
  • a domain or subdomain
  • DNS A record
  • Apache2
  • UFW firewall
  • Python virtual environment
  • Streamlit
  • systemd service
  • Certbot and Let’s Encrypt for HTTPS

The original version of this post used a Contabo VPS and Apache2. The same idea also works with other VPS providers such as Hetzner, DigitalOcean, AWS EC2, Oracle Cloud, Linode, or any Ubuntu server.

What We Will Build

The final setup will look like this:

User Browser
     |
     | https://data.example.com
     v
Apache2 on ports 80/443
     |
     | reverse proxy
     v
Streamlit app on localhost:8502

The user visits:

https://data.example.com

But internally, Streamlit runs on:

http://127.0.0.1:8502

This is cleaner than exposing the Streamlit port directly.

Requirements

Before starting, you need:

  • a VPS or cloud server running Ubuntu
  • a domain name
  • SSH access to the server
  • basic Linux command-line knowledge
  • a Streamlit app, for example app.py

In this tutorial, I will use:

data.example.com

as the example subdomain. Replace it with your own domain.

Step 1: Have a Domain Name

First, you need a domain name. In the original version, I used a domain from GoDaddy.

You can buy a domain from providers such as:

  • GoDaddy
  • Namecheap
  • Cloudflare Registrar
  • Porkbun
  • Google Domains alternatives
  • any local domain registrar

After buying a domain, you need to point it to your server.

Step 2: Understand DNS Records

DNS records tell the internet where your domain should point.

The most important record for this tutorial is the A record.

An A record maps a domain or subdomain to an IPv4 address.

Example:

Type: A
Name: data
Value: your_server_public_ip
TTL: Auto

This means:

data.example.com -> your_server_public_ip

If you want all subdomains to point to the same server, you can use a wildcard record:

Type: A
Name: *
Value: your_server_public_ip

But for a simple setup, it is better to create only the subdomain you need.

Step 3: Check DNS Propagation

After adding DNS records, it can take some time to update.

You can check from your local machine:

ping data.example.com

or:

dig data.example.com

If DNS is correct, it should show your server IP.

Step 4: Install Ubuntu on the Server

This tutorial uses Ubuntu because it is simple and widely supported.

Most VPS providers let you choose Ubuntu during server creation. Ubuntu 22.04 LTS or Ubuntu 24.04 LTS are good choices.

After creating the server, SSH into it:

ssh root@your_server_ip

or if you have a user:

ssh ubuntu@your_server_ip

Step 5: Update the Server

Run:

sudo apt update
sudo apt upgrade -y

Install useful packages:

sudo apt install -y python3 python3-pip python3-venv git curl ufw

Step 6: Install Apache2

Apache2 is a web server. We will use it as a reverse proxy.

Install Apache2:

sudo apt install -y apache2

Start and enable it:

sudo systemctl start apache2
sudo systemctl enable apache2

Check status:

sudo systemctl status apache2

If Apache is running, visit your server IP in the browser:

http://your_server_ip

You should see the Apache2 default page.

Step 7: Configure Firewall with UFW

If UFW is not installed:

sudo apt install -y ufw

Allow SSH first, otherwise you may lock yourself out.

sudo ufw allow OpenSSH

Allow Apache traffic:

sudo ufw allow "Apache Full"

Enable firewall:

sudo ufw enable

Check status:

sudo ufw status

You should see SSH and Apache allowed.

For this reverse-proxy setup, you do not need to expose Streamlit port 8502 publicly. Apache will access it locally.

Step 8: Check Apache Sites

Apache site configs are stored in:

/etc/apache2/sites-available/

Enabled sites are linked in:

/etc/apache2/sites-enabled/

Check current Apache virtual hosts:

sudo apache2ctl -S

In the original setup, the default page came from:

/var/www/html/index.html

You can enable or disable sites with:

sudo a2ensite site-name.conf
sudo a2dissite site-name.conf

After changes, reload Apache:

sudo systemctl reload apache2

Step 9: Create the Streamlit Project

Create a project folder.

sudo mkdir -p /opt/streamlit-apps/data-app
sudo chown -R $USER:$USER /opt/streamlit-apps/data-app
cd /opt/streamlit-apps/data-app

Create a virtual environment.

python3 -m venv env
source env/bin/activate

Install Streamlit and dependencies.

pip install --upgrade pip
pip install streamlit pandas plotly

If your app needs more packages, install them too.

For example:

pip install numpy matplotlib scikit-learn

Step 10: Create a Simple Streamlit App

Create app.py.

nano app.py

Add this simple app:

import streamlit as st
import pandas as pd


st.set_page_config(
    page_title="Data App",
    page_icon="📊",
    layout="wide"
)

st.title("Streamlit App on Custom Domain")

st.write("This Streamlit app is running behind Apache2 reverse proxy.")

df = pd.DataFrame({
    "x": [1, 2, 3, 4, 5],
    "y": [10, 20, 15, 30, 25]
})

st.line_chart(df.set_index("x"))

Test it manually:

streamlit run app.py --server.address 127.0.0.1 --server.port 8502 --server.headless true

From the server, check:

curl http://127.0.0.1:8502

If it returns HTML, Streamlit is running.

Stop it with CTRL + C.

Step 11: Create Streamlit config.toml

You can configure Streamlit with a .streamlit/config.toml file.

Create the folder:

mkdir -p .streamlit
nano .streamlit/config.toml

Add:

[server]
headless = true
address = "127.0.0.1"
port = 8502
enableCORS = false
enableXsrfProtection = false

[browser]
serverAddress = "data.example.com"
serverPort = 443

This tells Streamlit to run locally on port 8502.

The app will not be directly exposed to the internet. Apache will expose it through the domain.

Step 12: Run Streamlit as a systemd Service

Running Streamlit manually in a terminal is not reliable. If the SSH session closes, the app may stop.

A better way is to run it as a systemd service.

Create a service file:

sudo nano /etc/systemd/system/streamlit-data-app.service

Add:

[Unit]
Description=Streamlit Data App
After=network.target

[Service]
User=ubuntu
Group=ubuntu
WorkingDirectory=/opt/streamlit-apps/data-app
Environment="PATH=/opt/streamlit-apps/data-app/env/bin"
ExecStart=/opt/streamlit-apps/data-app/env/bin/streamlit run app.py --server.address 127.0.0.1 --server.port 8502 --server.headless true
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Replace ubuntu with your Linux username.

Reload systemd:

sudo systemctl daemon-reload

Start the service:

sudo systemctl start streamlit-data-app

Enable it on boot:

sudo systemctl enable streamlit-data-app

Check status:

sudo systemctl status streamlit-data-app

View logs:

journalctl -u streamlit-data-app -f

Step 13: Enable Apache Proxy Modules

Apache needs proxy modules for reverse proxying.

Enable the required modules:

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
sudo a2enmod rewrite
sudo a2enmod headers

Restart Apache:

sudo systemctl restart apache2

Streamlit uses WebSocket connections, so WebSocket proxy support is important.

Step 14: Configure Apache VirtualHost

Create a new Apache config file:

sudo nano /etc/apache2/sites-available/data.example.com.conf

Add this configuration:

<VirtualHost *:80>
    ServerName data.example.com
    ServerAdmin admin@example.com

    ProxyPreserveHost On
    ProxyRequests Off

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule /(.*) ws://127.0.0.1:8502/$1 [P,L]

    ProxyPass / http://127.0.0.1:8502/
    ProxyPassReverse / http://127.0.0.1:8502/

    ErrorLog ${APACHE_LOG_DIR}/data.example.com-error.log
    CustomLog ${APACHE_LOG_DIR}/data.example.com-access.log combined
</VirtualHost>

Replace:

data.example.com
admin@example.com

with your own domain and email.

Enable the site:

sudo a2ensite data.example.com.conf

Check syntax:

sudo apachectl configtest

If it says Syntax OK, reload Apache:

sudo systemctl reload apache2

Now visit:

http://data.example.com

Your Streamlit app should load.

Step 15: Add HTTPS with Certbot

HTTP works, but production apps should use HTTPS.

Install Certbot:

sudo apt install -y certbot python3-certbot-apache

Run Certbot:

sudo certbot --apache -d data.example.com

Follow the prompts.

Certbot can automatically create the HTTPS virtual host and set up certificate renewal.

Test renewal:

sudo certbot renew --dry-run

After this, your app should be available at:

https://data.example.com

Step 16: Apache HTTPS Config Example

After Certbot, Apache may create or modify SSL config. A typical HTTPS reverse proxy config looks like this:

<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName data.example.com
    ServerAdmin admin@example.com

    ProxyPreserveHost On
    ProxyRequests Off

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule /(.*) ws://127.0.0.1:8502/$1 [P,L]

    ProxyPass / http://127.0.0.1:8502/
    ProxyPassReverse / http://127.0.0.1:8502/

    ErrorLog ${APACHE_LOG_DIR}/data.example.com-ssl-error.log
    CustomLog ${APACHE_LOG_DIR}/data.example.com-ssl-access.log combined

    SSLCertificateFile /etc/letsencrypt/live/data.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/data.example.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

Usually, you do not need to write this manually if Certbot handles Apache configuration for you.

Step 17: Do You Need to Edit ports.conf?

In the original version, I added Streamlit’s port to Apache ports.conf.

For this reverse-proxy setup, you usually do not need to add:

Listen 8502

Apache should listen on:

Listen 80
Listen 443

Streamlit listens on 127.0.0.1:8502.

Apache proxies traffic to that local port.

This is cleaner and safer than exposing Streamlit’s internal port publicly.

Step 18: Using a Subdomain

In the original version, I used a subdomain like:

data.mydomain.com

The Apache config looked similar to this:

<VirtualHost *:80>
    ServerAdmin admin@mydomain.com
    ServerName data.mydomain.com
    ServerAlias data.mydomain.com

    ProxyRequests Off
    ProxyPreserveHost On

    ProxyPass / http://127.0.0.1:8502/
    ProxyPassReverse / http://127.0.0.1:8502/
</VirtualHost>

This idea still works. The key difference is that using 127.0.0.1 is better than using the public domain inside the proxy target.

Good:

ProxyPass / http://127.0.0.1:8502/

Avoid:

ProxyPass / http://mydomain.com:8502/

Using localhost keeps the internal app private.

Step 19: Multiple Streamlit Apps on One Server

You can host multiple Streamlit apps on one server by giving each app a different internal port.

Example:

data.example.com     -> 127.0.0.1:8502
ml.example.com       -> 127.0.0.1:8503
dashboard.example.com -> 127.0.0.1:8504

Each app should have:

  • separate project folder
  • separate virtual environment
  • separate systemd service
  • separate Apache virtual host
  • separate subdomain DNS record

Example systemd services:

streamlit-data-app.service
streamlit-ml-app.service
streamlit-dashboard-app.service

Step 20: Deploy Updates

When you update the app code:

cd /opt/streamlit-apps/data-app
git pull
source env/bin/activate
pip install -r requirements.txt
sudo systemctl restart streamlit-data-app

Check logs:

journalctl -u streamlit-data-app -f

Troubleshooting

Problem 1: Apache Default Page Still Shows

Check whether your site is enabled.

sudo apache2ctl -S

Enable your site:

sudo a2ensite data.example.com.conf
sudo systemctl reload apache2

You can disable the default site if needed:

sudo a2dissite 000-default.conf
sudo systemctl reload apache2

Problem 2: Streamlit App Does Not Load

Check if Streamlit is running:

sudo systemctl status streamlit-data-app

Check local response:

curl http://127.0.0.1:8502

Check logs:

journalctl -u streamlit-data-app -f

Problem 3: 502 Proxy Error

A 502 error usually means Apache cannot reach Streamlit.

Check:

  • Streamlit service is running
  • Streamlit is on the correct port
  • Apache config points to the correct port
  • firewall is not blocking local traffic
  • systemd service has correct working directory

Problem 4: WebSocket Error or App Keeps Reconnecting

Streamlit needs WebSocket support.

Make sure these modules are enabled:

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
sudo a2enmod rewrite
sudo systemctl restart apache2

Also make sure your VirtualHost includes WebSocket rewrite rules.

Problem 5: HTTPS Certificate Fails

Check DNS first:

dig data.example.com

Make sure port 80 is open:

sudo ufw status

Then retry Certbot:

sudo certbot --apache -d data.example.com

Problem 6: Permission Error in systemd

Make sure the service User owns the project folder.

sudo chown -R ubuntu:ubuntu /opt/streamlit-apps/data-app

Replace ubuntu with your actual username.

Security Tips

Here are some useful security tips:

  • do not expose Streamlit port publicly
  • bind Streamlit to 127.0.0.1
  • use HTTPS
  • keep Ubuntu updated
  • use UFW firewall
  • do not run the app as root
  • store secrets in environment variables
  • restrict database access
  • check Apache and systemd logs
  • keep Python dependencies updated

Final Checklist

Before calling the deployment complete, check:

  • DNS A record points to the server
  • Apache default page works
  • Streamlit works locally on 127.0.0.1:8502
  • systemd service starts on boot
  • Apache virtual host is enabled
  • Apache proxy modules are enabled
  • HTTP domain loads the Streamlit app
  • HTTPS certificate is installed
  • WebSocket connection works
  • Streamlit port is not publicly exposed

Final Thoughts

In this blog, we deployed a Streamlit app with a custom domain using Apache2. We configured DNS, installed Apache, created a Streamlit app, ran it with systemd, configured Apache as a reverse proxy, and added HTTPS with Certbot.

The most important idea is that Apache should be the public-facing server, while Streamlit should run privately on localhost. This gives a cleaner and safer deployment setup.

This approach works well for personal projects, dashboards, demos, and small internal tools. For larger production systems, you may also consider Docker, Nginx, cloud services, or a dedicated deployment platform.

Comments