2020-06-29T17:00:56Z

Access Localhost From Your Phone Or From Anywhere In The World

Sometimes it is useful to quickly access your Flask application running on localhost from another device or location for testing purposes. In this article I'll show you how to use the pyngrok package to provision a temporary public URL for your application that works from your phone or from anywhere in the world!

The Ngrok Command

If you want to try this out with an application of yours, go into your application directory and activate its virtual environment. For the examples in this article I will be using the microblog application featured in my Flask Mega-Tutorial.

To begin, install pyngrok into your virtual environment:

(venv) $ pip install pyngrok

Pyngrok is a Python-friendly wrapper for ngrok. It downloads and installs a copy of the ngrok binary for your platform the first time you use it. Run ngrok --help with your virtual environment activated to confirm that you have it installed and working:

(venv) $ ngrok --help

I will show you how to integrate ngrok with your Flask application in the next section, but for now let's learn how it works by running it in standalone mode. You will need two terminal windows for this, both with the virtual environment activated.

On the first terminal, start your application. Normally you would do it with the flask run command, but if you start your application by running a Python script that is fine too. What matters is that your application listens for requests at http://localhost:5000 (or a different port if you like).

Now go to your second terminal and start ngrok as follows:

(venv) $ ngrok http 5000

If you use a port other than 5000, then adjust the command accordingly. This is going to create a tunnel between a randomly generated public URL in the ngrok.io domain and your application running on localhost. This is the output of ngrok:

Ngrok screenshot

Look for the two lines that begin with the word "Forwarding" to know your URL. These two lines show the http:// and https:// versions of your URL. In the example above the URL that I was given was https://3bb2431328e0.ngrok.io. You will get a similar one, but the subdomain portion is going to be different each time you run ngrok. While the ngrok process is running (limited to a maximum of eight hours) any requests that are sent to this URL are immediately forwarded to your application.

Send the URL to your phone and open it in your mobile browser to see how cool ngrok is. You can also send the URL to a friend if you like, as it works from anywhere in the world.

Create an Ngrok Tunnel from Python Code

If you've never used ngrok before, I'm sure by now you are excited about all the possibilities it opens. In this section I'm going to show you an improved workflow that uses the Python access into ngrok provided by the pyngrok package. This package can start the tunnel automatically when the application starts.

To open an ngrok tunnel I've added a start_ngrok() function to my application. For my microblog example, I put this function in the top-level microblog.py module, but it can really go anywhere you like. Here is the code for this function:

def start_ngrok():
    from pyngrok import ngrok

    url = ngrok.connect(5000)
    print(' * Tunnel URL:', url)

This function performs the same function as the ngrok http 5000 command from the previous section. The ngrok.connect() function returns the public URL that was assigned to the tunnel. My start_ngrok() function prints this URL to the terminal, so that I can then send it to my phone (or a friend) as needed.

To decide wether I want to start my application with a tunnel or not I'm going to use a START_NGROK configuration option. When the option is enabled I will call the above function to get a tunnel set up. I've added the following conditional to the main script in my application:

if app.config['START_NGROK']:
    start_ngrok()

Finally, I've added the START_NGROK configuration option to my Config class, which I have in the config.py module:

class Config:
    # ...
    START_NGROK = os.environ.get('START_NGROK') is not None

The START_NGROK option is going to be set to True whenever an environment variable with the same name is set. The value that is set in the variable does not really matter, as long as a non-empty value is set I'll consider the variable enabled.

Now I can set the environment variable before I start my application if I want to use ngrok:

(venv) $ START_NGROK=1 flask run
 * Serving Flask app "microblog.py"
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Tunnel URL: http://24d1fd7fd908.ngrok.io
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

And here you can see that in the line before last I'm showing the ngrok URL.

If you use the Flask reloader and enable the ngrok tunnel you will notice that two tunnels are started. This is because the reloader runs two processes: a parent process that monitors files for changes, and a child process that runs the actual server. The parent process kills and restarts the child process every time it detects changes to a source file.

It would be better if the tunnel could be created only in the parent process, which is the process that has a longer life. That would mean that a reload event would preserve the tunnel URL, since it is associated with the parent reloader process. So a nice improvement would be to make sure the START_NGROK configuration variable is always False in the child process.

Looking at the internals of the Flask reloader (which is actually in the Werkzeug package), I noticed that when the child process is launched, a WERKZEUG_RUN_MAIN variable is set to the string 'true' in the environment. This is used by Werkzeug to detect wether a process is the parent or the child. I can check on this variable and make sure START_NGROK is always False for the child process:

class Config:
    # ...
    START_NGROK = os.environ.get('START_NGROK') is not None and \
        os.environ.get('WERKZEUG_RUN_MAIN') is not 'true'

So now, when the main Flask process starts it will set up the tunnel. If the reloader is enabled, then a child process will be launched with WERKZEUG_RUN_MAIN=true in the environment, so there will be no second tunnel started. And every time the child process is recycled the tunnel set up in the parent process will be unaffected, so the same tunnel URL will remain valid for as long as the reloader process is running, up to a maximum of eight hours.

Conclusion

I hope you incorporate ngrok into your development workflow. I have used it for many years to run quick tests on my applications from other devices.

You should keep in mind that ngrok is a service intended for very low traffic, so it is not a deployment mechanism but just a convenient tool to run quick tests. If you end up become a frequent user of this tool, consider one of their paid plans, all of which allow you to secure a permanent URL.

6 comments

  • #1 Tanim Islam said 2020-06-30T02:49:52Z

    If your flask server lives on a machine with an SSH server, why not use an ssh tunnel instead?

  • #2 Miguel Grinberg said 2020-06-30T10:39:57Z

    @Tanim: first of all, the idea is that you would do this from your development machine, which is likely going to be behind a firewall, not from a server. But even if we ignore that, you want to create an ssh tunnel to where? And where do the public URLs come from if you use an ssh tunnel? The ngrok service solves all these problems for you.

  • #3 tc said 2020-07-29T20:12:01Z

    Hey Miguel! Love your mega tutorial and the ngrok tip above as well. I'm using this in my dev environment to build a site. I'm running into an issue and was hoping you had an answer: - I've set up pyngrok in my microblog.py equivalent project - Whenever I'm doing a database migration (i.e. flask db migrate), it looks like the tunnel is still opening. - Because of this, the db migration is caught in thread locking. I have to KeyboardInterrupt to end it

    (Traceback (most recent call last): File "/usr/lib/python3.8/threading.py", line 1388, in _shutdown lock.acquire() KeyboardInterrupt)

    The migration script is properly created, however. If I unset my START_NGROK variable (i.e. no tunnel open on flask command), the flask db migrate command finishes completely without locking.

    Any idea how to set ngrok up so that it doesn't open a tunnel during flask shell and flask db commands?

  • #4 Gene said 2020-07-31T01:59:05Z

    It's helpful for me, thanks

  • #5 Miguel Grinberg said 2020-07-31T09:22:19Z

    @tc: Yeah, so my workflow is to only set START_NGROK=1 when you run flask run. It appears you have set it globally for the shell session, so this variable is always set when you run a flask command. You could check sys.argv to see what command was issued and use that as additional input in deciding when to start ngrok or not.

  • #6 tc said 2020-08-01T06:20:16Z

    @miguel: Thanks a lot! The sys.argv suggestion helped. It looks like sys.argv[0] returns the full flask command path and sys.argv[1] returns the second word like 'run' or 'shell'. I've set start_ngrok() to run only where sys.argv[1] == 'run'.

Leave a Comment