# Starlink Satellite SMS Notifications With Python, Kubernetes, and Twilio

#### Set up a CronJob to check for satellites

Photo by [Phil Botha](https://unsplash.com/@philbotha?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/star?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

SpaceX is launching thousands of [Starlink](https://www.starlink.com/) satellites to assemble a giant interconnected constellation in space. If you look up at just the right time, you might be lucky enough to spot some. But how can you know ahead of time when a satellite is going to pass overhead?

You don’t have to count on luck to see these tiny silver ants parading across the night sky. This tutorial shows you how to set up a scheduled job to check if a satellite approaches and send an SMS alert out to notify you.

### Tutorial Requirements

*   Python version 3
*   A personal phone number to receive SMS notifications
*   [A freeTwilio account](http://www.twilio.com/referral/geBKT7) and a [Twilio phone number](https://www.twilio.com/user/account/phone-numbers/incoming) to send SMS notifications
*   A free [Docker Hub](https://hub.docker.com/) account, [Docker Desktop](https://docs.docker.com/desktop/) (for Mac or Windows) or [Docker Engine](https://docs.docker.com/engine/install/) (for Linux) to build and share containerized applications
*   A free [KubeSail account](https://kubesail.com/) to deploy your code to a Kubernetes cluster

### Set up the Python virtual environment

Make a new project directory, and change into the directory from the command line.

$ mkdir starlink-alert  
$ cd starlink-alert

Create a virtual environment called `venv`. Activate the virtual environment, and then install the required Python packages inside the virtual environment. If you’re on Unix or Mac operating systems, enter these commands in a terminal.

$ python3 -m venv venv  
$ source venv/bin/activate  
(venv) $ pip install twilio python-dotenv datetime pytz skyfield

If you’re on a Windows machine, enter these commands in a command prompt window.

$ python -m venv venv  
$ venv\\Scripts\\activate  
(venv) $ pip install twilio python-dotenv datetime pytz skyfield

The Python packages we’re using are:

*   [Twilio’s Python Library](https://github.com/twilio/twilio-python) — a Python module for communicating with the Twilio API
*   [python-dotenv](https://pypi.org/project/python-dotenv/) — to manage environment variables
*   [datetime](https://docs.python.org/3/library/datetime.html) and [pytz](http://pytz.sourceforge.net/) — to handle dates, times, and timezones
*   [skyfield](https://rhodesmill.org/skyfield/) — to compute positions for the satellites in orbit around the Earth

### Write the Python script

The purpose of the script is to check if a visible satellite is approaching a given city location. If so, your Twilio phone number sends an SMS alert to your personal phone number.

Copy the following code into a file called `tracker.py`:

import os  
import math  
from dotenv import load\_dotenv  
load\_dotenv()  
from skyfield.api import Topos, load  
from datetime import timedelta  
from pytz import timezone  
from twilio.rest import Client

\# load the satellite dataset from Celestrak  
starlink\_url = 'https://celestrak.com/NORAD/elements/starlink.txt'  
starlinks = load.tle\_file(starlink\_url)  
print ('Loaded', len(starlinks), 'satellites')

\# update city location and timezone  
location = Topos('37.7749 N', '122.4194 W')  
tz = timezone('US/Pacific')

\# establish time window of opportunity  
ts = load.timescale()  
t0 = ts.now()  
t1 = ts.from\_datetime(t0.utc\_datetime()+ timedelta(hours=2))

\# loop through satellites to find next sighting  
first\_sighting = {}  
for satellite in starlinks:

   # filter out farthest satellites and NaN elevation  
   elevation = satellite.at(t0).subpoint().elevation.km  
   isNan = math.isnan(elevation)  
   if elevation > 400 or isNan: continue  
   print ('considering: {} at {}km'.format(  
       satellite.name,  
       round(elevation)  
   ))

   # find and loop through rise / set events  
   t, events = satellite.find\_events(location, t0, t1, altitude\_degrees=30.0)  
   for ti, event in zip(t, events):  
        
       # check if satellite visible to a ground observer  
       eph = load('de421.bsp')  
       sunlit = satellite.at(t1).is\_sunlit(eph)  
       if not sunlit: continue

       # filter by moment of greatest altitude - culminate  
       name = ('rise above 30°', 'culminate', 'set below 30°')\[event\]  
       if (name != 'culminate'): continue  
            
       # find earliest time for next sighting  
       if (not first\_sighting) or (ti.utc < first\_sighting\['time'\]):  
           first\_sighting\['time\_object'\] = ti  
           first\_sighting\['time'\] = ti.utc  
           first\_sighting\['satellite'\] = satellite

if (first\_sighting): 

   # create body for SMS    
   next\_sighting = ('next sighting: {} {}'.format(  
       first\_sighting\['satellite'\].name,  
       first\_sighting\['time\_object'\].astimezone(tz).strftime('%Y-%m-%d %H:%M')  
   ))

   # send SMS via Twilio if upcoming sighting  
   account\_sid = os.environ.get('TWILIO\_ACCOUNT\_SID')  
   auth\_token = os.environ.get('TWILIO\_AUTH\_TOKEN')  
   client = Client(account\_sid, auth\_token)

   message = client.messages.create(  
       body=next\_sighting,  
       from\_=os.environ.get('TWILIO\_PHONE\_NUMBER'),  
       to=os.environ.get('MY\_PHONE\_NUMBER')  
   )

   print ('Message sent:', message.sid, next\_sighting)

else:

   print ('No upcoming sightings')

Update variables `location` and `tz` on rows 16 to 17 with your own geographic coordinates and timezone. You can find your latitude and longitude [here](https://www.latlong.net/). See all the timezones by running the following code in a Python shell. Once you find the timezone that applies to your location, exit the Python shell.

(venv) $ python  
\>>> import pytz  
\>>> pytz.all\_timezones  
\>>> exit()

The `tracker.py` script relies on a Python package called Skyfield to handle some of the complex space computations. The script loads the satellite dataset, looping through the satellites to find the next good sighting. Skyfield figures out when a satellite rises and sets near you and sees if the satellite is sunlit for maximum visibility. If there are any sightings predicted within the next two hours, Twilio sends the details with an SMS to your phone number.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1735163500754/411ebbee-f975-491e-98c2-44a85e7c59e8.jpeg)

The Starlink 3 train rides across the early morning sky, photo by [Forest Katsch](https://unsplash.com/@forestkatsch?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/starlink?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

To enable the Twilio SMS, store your credentials and other configuration information in a file called `.env` (notice the dot in front of this filename).

TWILIO\_ACCOUNT\_SID=<your-account-sid>  
TWILIO\_AUTH\_TOKEN=<your-auth-token>  
TWILIO\_PHONE\_NUMBER=<your-twilio-phone-number>  
MY\_PHONE\_NUMBER=<your-personal-phone-number>

To find your Twilio Account SID and Auth Token, log into the Twilio Console, then click on “Settings” in the sidebar and scroll down to “API Credentials”. You will also need your Twilio phone number to send the SMS.

Save your updates to the `.env` file, and then run the script from the command line. If you’re running the script during the day, you might determine a satellite is passing overhead, but not be able to see it very well in the daylight. Once you set up a CronJob, you can specify when to run the script for maximum satellite visibility, like at dusk or dawn.

(venv) $ python tracker.py

After you try the script and confirm that it is working, create a requirements file to store the dependencies, so that we can install them in the next step.

(venv) $ pip freeze > requirements.txt

Now, let’s create a Docker image to run the script.

### Create a Docker image and push to Docker Hub

Make sure Docker is installed and running on your machine. Add the following code in a file called `Dockerfile` (notice there’s no file extension) that describes how to build our image.

\# creates a layer from the python:3 Docker image  
FROM python:3

\# copy and install dependencies  
COPY requirements.txt /  
RUN pip install -r requirements.txt

\# add script  
COPY tracker.py /

\# define the command to run the script  
CMD \[ "python", "./tracker.py" \]

Next, we are going to build and run a Docker image for the Python script from the terminal. In the following commands, the image we are building is called `python-starlink` and the container is called `starlink_test`. Since we don’t want to include our environment variables in the container for security reasons, we run the container using the `--env-file` flag in the command line. If you’re on a Linux operating system, precede these `docker` commands with `sudo` or [create a docker group](https://docs.docker.com/engine/install/linux-postinstall/).

$ docker build --tag python-starlink .  
$ docker run --name starlink\_test --env-file=.env --rm python-starlink

Once you verify that the container runs successfully, share the Docker image on the [Docker Hub](https://hub.docker.com/) registry so it can be accessed and run from the cloud. Log into your Docker Hub account from the command line with `docker login`. Then create a repository and push your image to Docker Hub using the following commands:

$ docker tag python-starlink <Your Docker ID>/python-starlink:latest  
$ docker push <Your Docker ID>/python-starlink:latest

In the above commands, `Your Docker ID` is the username you have registered with Docker Hub.

In the next step, let’s schedule a Kubernetes CronJob to run the Python script.

### Deploy a CronJob on Kubernetes

Make sure you are logged into [KubeSail](https://kubesail.com/). KubeSail has public YAML examples. I made a CronJob template called `[satellites](https://kubesail.com/template/loopDelicious/satellites)` that has two kinds of Kubernetes resources:

*   [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) to securely store environment variables
*   [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) to complete a task on a schedule

Go to the `[satellites](https://kubesail.com/template/loopDelicious/satellites)` template. Under “Required Variables”, replace the default `DOCKER-IMAGE` with the base image you just pushed to Docker Hub. Then input the remaining environment variables.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1735163501654/f5ef77d9-6d99-4aba-b76a-1c065d09fa46.png)

If you want to inspect the YAML, click “Edit Yaml” to expand the code editor and review the underlying configuration for each resource. This YAML describes the resource `CronJob`.

apiVersion: batch/v1beta1  
kind: CronJob  
metadata:  
  name: starlink  
  labels:  
    app: starlink  
spec:  
  schedule: "0/30 19-21 \* \* \*"  
  jobTemplate:  
    spec:  
      template:  
        spec:  
           restartPolicy: Never  
          containers:  
            - name: starlink-tracker  
              image: "{{DOCKER-IMAGE|joycelin79/python-starlink:latest}}"  
              envFrom:  
                - secretRef:  
                    name: starlink-secret  
              imagePullPolicy: Always  
  successfulJobsHistoryLimit: 3  
  failedJobsHistoryLimit: 1

This YAML describes the resource `Secret` to store our environment variables.

apiVersion: v1  
kind: Secret  
metadata:  
  name: starlink-secret  
  labels:  
    app: starlink  
stringData:  
  TWILIO\_ACCOUNT\_SID: "{{TWILIO-ACCOUNT-SID}}"  
  TWILIO\_AUTH\_TOKEN: "{{TWILIO-ACCOUNT-SID}}"  
  TWILIO\_PHONE\_NUMBER: "{{TWILIO-PHONE-NUMBER}}"  
  MY\_PHONE\_NUMBER: "{{MY-PHONE-NUMBER}}"

When you’re ready, hit “Launch Template” to deploy a cluster to your Kubernetes namespace on KubeSail.

In the sidebar under “Resources”, you can see all the resources running in this Kubernetes context including the scheduled jobs.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1735163502464/c821c2cc-4abe-4b62-bfcd-e3607e5e09a8.png)

This is also where you can update the deployment. For example, update your environment variables in the `Secret` resource. To trigger a job every minute while you’re testing, update the YAML configuration for the `CronJob` resource and set the `schedule` to `"* * * * *"`(surrounded in double-quotes) and **Apply** your changes.

To edit the underlying Python script, save the changes locally in `tracker.py`. Then build and push the updated image to Docker Hub as we did before. The Kubernetes CronJob in KubeSail automatically pulls the latest version in Docker Hub because we specified `imagePullPolicy: Always` in our container configuration.

And that’s it!

### Conclusion

Creating an SMS notification is pretty straightforward. There’s a bunch of ways to do this — and now you know how to schedule a CronJob on Kubernetes.

For further exploration, you can:

*   **Adjust the criteria:** Each batch of Starlink satellites spreads out over time traveling to a higher elevation. If you want to increase the sensitivity of your tracker to see more satellites, adjust the elevation in your script. Or update the Cron schedule to run [when satellites are most visible from the ground](https://www.space.com/6870-spot-satellites.html) — at dusk and dawn.
*   **Change the deployment**: Set up a different Kubernetes context in KubeSail to deploy the CronJob to a cluster running locally or on another cloud provider. Or experiment with [other CronJob options in Kubernetes](https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/).
*   **Track something else**: If satellites aren’t your thing, swap it out for [a new dataset of celestial bodies](https://celestrak.com/).

Let me know if you spot something out of this world!
