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).public_url
    print(' * Tunnel URL:', url)

This function performs the same function as the ngrok http 5000 command from the previous section. The ngrok.connect().public_url expression 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']:

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 (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.


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.


  • #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'.

  • #7 Gitau Harrison said 2020-10-24T05:35:11Z

    Much appreciation for the blog. Really useful for testing

  • #8 Gitau Harrison said 2020-11-02T11:30:05Z

    I have set up ngrok on another app and I am trying to run it on a mobile phone. I get Werkzeug: Routing.BuildError. The interesting thing is the localhost on my computer works just fine, not routing builderror, but it exists on the ngrok tunnel I have recreated and run on my phone.

    Could this be ngrok error?

    The complete error on the phone is:

    werkzeug.routing.BuildError: Could not build url for endpoint 'login'. Did you mean 'auth.login' instead?
  • #9 Miguel Grinberg said 2020-11-02T19:34:12Z

    @Gitau: this has nothing to do with ngrok, it is a bug in your application. Your code is clearly trying to build a URL for the "login" endpoint, and no such endpoint exists, you probably need to use "auth.login", or in other words, you forgot to include the blueprint in the endpoint name. The error is very clear about that.

  • #10 Alexander Ambrioso said 2020-12-18T13:31:35Z

    Thank you Mr. Grinberg. This is a fun and useful idea. It works equally well with a node.js and Fast API. Great for a quick demo or to check out an app on various devices.

  • #11 Rene said 2020-12-30T20:45:44Z

    Hi Miguel! This is extremely useful! I did not know about the ngrok, thanks a lot for writing this tutorial!

  • #12 Manohar Nookala said 2020-12-31T12:21:52Z

    Hi Sir, It worked successfully. Thanks a lot.

  • #13 Altaf said 2021-07-07T00:09:50Z

    Hello Miguel. I have been following most of you blogs. Thank you for these blogs, they provide much needed help I am using ngrok with a basic plan. I am building RestAPI using Flask in Python using PyCharm editor. Everytime I run the debugger using flask_ngrok, I get a new address. I have a custom domain that I want to use. How can I do that?

  • #14 Miguel Grinberg said 2021-07-07T14:51:16Z

    @Altaf: Not familiar with the flask-ngrok extension, so I don't know, the extensions might not support advanced options. I suggest you use the ngrok command directly, which allows you to log in to your account.

  • #15 RJC said 2021-09-06T12:09:57Z

    Thank you Miguel! You have some of the best tutorials I have sees in 6+ years of studying Python. Your Flask mega-tutorial is absolutely the best tutorial of any kind and worth every penny. After going through the first 12 chapters, I was able to build a real (albeit simple) API that is very useful. Thanks again!!

  • #16 Mo said 2022-07-16T19:12:52Z

    Hello Miguel !

    Thanks again for tips and tricks in the web development. I'm trying to use pyngrok to test my app but when it comes to access to my app with another device, i have an error from ngrok.

    To prevent visit abuse ngrok display this page but we can remove it if we do some implementation for remove the page :

    "To remove this page:

    Set and send an [ngrok-skip-browser-warning] request header with any value.
    Or, set and send a custom/non-standard browser [User-Agent] request header."

    Can you help me to figure it out how can i set and send custom non-standard or ngrok-skip-browser-warning with an request header in my flask application?

  • #17 Miguel Grinberg said 2022-07-17T07:35:43Z

    @Mo: This must be a new thing added to the service recently because I've never seen it. I'm not familiar enough to tell you have to avoid it, but in any case, this seems like a sensible thing to do, since ngrok tunnels are supposed to be used just for testing. I wouldn't personally waste time trying to remove the warning.

Leave a Comment