Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Overview

When deploying Flask applications using Docker, it’s common for developers to encounter server connection issues.

For instance, a developer new to containerization could encounter a situation where the Flask server appears to be running inside the container but is unreachable from the outside. To clarify, the server seems invisible to the browser or the curl command even after building the image, running the container, and seemingly mapping the port correctly.

In this tutorial, we’ll present an example demonstrating the problem, provide a solution, and explore a few debugging tips.

2. Basic Problem Sample

To begin with, we introduce a relatively simple example.

2.1. Flask Application

Let’s use a minimal Flask app, index.py:

$ car index.py
from flask import Flask

app = Flask(__name__)
app.debug = True

@app.route('/')
def main():
    return 'Hello World!'

if __name__ == '__main__':
    app.run()

The Flask app above works as expected when run directly on the machine:

$ python3 index.py
 * Serving Flask app 'index'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 372-382-014

Above, we see the development server binds to port 5000 on localhost (127.0.0.1). We can now visit http://127.0.0.1:5000/ to see Hello World!.

2.2. Containerize the Application

Next, let’s package the Flask app in a Docker container with the help of the Dockerfile:

FROM python:3.9

WORKDIR /app

COPY . /app

RUN pip install --no-cache-dir flask

EXPOSE 5000

CMD ["python3", "index.py"]

Further, let’s build the image:

$ docker build -t flaskappimage .
[+] Building 212.2s (8/8) FINISHED                                                                                 docker:desktop-linux
...
 => [1/3] FROM docker.io/library/python:3.9@sha256:e2d6f8be31a35665d3c39561ac2e96ece5b847e49ff84c462ab1d6850900ba7d              199.2s
...
 => [2/3] WORKDIR /app                                                                                                             1.5s
 => [3/3] COPY . /app                                                                                                              2.0s
...

At this point, we are ready to run the container.

2.3. Deploy the Application Container

After setting up everything, we run the container:

$ docker run -d -p 5000:5000 --name myflaskapp flaskappimage
9044c0db6c776762af07f8a1d21d7843099d9e949cc4af4c8b4815a241d720b2

Let’s break down the command:

  • -d runs the container in detached mode
  • -p 5000:5000 maps host port 5000 to container port 5000
  • –name myflaskapp names the container myflaskapp
  • flaskappimage represents the name of the image to run

Now, when we try to access the app using curl, we get:

$ curl http://127.0.0.1:5000/
curl: (52) Empty reply from server

From the browser, we also can’t access the app because 127.0.0.1 doesn’t send any data.

3. Problem Analysis

Let’s start by looking at the logs inside the container:

$ docker logs myflaskapp
 * Serving Flask app 'index'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 100-495-488

The Flask development server binds to 127.0.0.1 inside the container:

* Running on http://127.0.0.1:5000

To explain, that address refers to the container’s interface, not that of the host. So, the server only listens within the container and not on any externally accessible interface.

Thus, the port is published, but the app is only reachable from inside the container itself, not from the host.

4. Binding to All Network Interfaces

To resolve basic server connection issues, we can instruct Flask to listen on all interfaces with the help of host=’0.0.0.0′.

Let’s first update the index.py file:

from flask import Flask

app = Flask(__name__)
app.debug = True

@app.route('/')
def main():
    return 'Hello World!'

if __name__ == '__main__':
    app.run(host='0.0.0.0')

Now, Flask binds to all available network interfaces.

To implement the change, let’s rebuild the image:

$ docker build --no-cache -t flaskappimage .

Once we rebuild the image, we stop and remove the old container:

$ docker stop myflaskapp && docker rm myflaskapp
myflaskapp
myflaskapp

At this point, we run the container:

$ docker run -d -p 5000:5000 --name myflaskapp flaskappimage
369fb687b84b225d50e80681167f97eba6242b70d54eff641a98d968a83b314b

Let’s now view the logs:

$ docker logs myflaskapp
 * Serving Flask app 'index'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.2:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 100-495-488

So, now we see the newline * Running on all addresses (0.0.0.0), indicating that the modifications worked as expected.

Finally, let’s try accessing the app from the host again:

$ curl http://127.0.0.1:5000/ && echo
Hello World!

We can now see Hello World! in the terminal. Here, && echo prints a newline after the output.

5. Why Binding to All Network Interfaces Is Important

Let’s briefly explain why binding to 0.0.0.0 is important.

Inside the container:

  • 127.0.0.1 is only visible inside the container
  • 0.0.0.0 is visible from any interface, including external ones exposed by Docker

Although port mapping (-p 5000:5000) instructs Docker to map port 5000 of the container to port 5000 on the host, the host may fail to reach the container even if the port is matched. In our case, we saw that this happens if the container only listens on 127.0.0.1.

Therefore, the combination of binding to 0.0.0.0 inside the app and mapping ports using the -p option in the docker run command makes the app externally accessible.

6. Debugging Tips

To debug basic issues, we can try several troubleshooting steps.

6.1. Using curl in the Container

Firstly, we can use the curl command inside the container:

$ docker exec -it <container_id> curl 127.0.0.1:5000
Hello World!
...

If this command works here but not on the host, then the app works fine, but it’s probably not exposed outside the container.

6.2. Temporary Debugging Tools

To inspect networking issues, we can use common debugging tools such as netstat. In some cases, the image may not contain these debugging tools. For example, since we’re using a minimal Docker image like python:3.9, we don’t have access to these debugging tools. In such cases, we can temporarily install them inside a running container.

Let’s begin by opening an interactive shell:

$ docker exec -it myflaskapp bash

If bash isn’t available, we can use sh instead:

$ docker exec -it myflaskapp sh

In the interactive shell, we update package lists and install the desired tools package:

# apt update && apt install -y net-tools iputils-ping

Then, we should be able to use the respective tool:

# netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      1/python3

The output above confirms that the Flask app listens on port 5000 on all interfaces (0.0.0.0). These changes are temporary because we lose the tools once we stop or delete the container. With the help of this approach, we can quickly debug without modifying the Docker image. For persistence, we can integrate these tools into a custom image.

6.3. Persistent Debugging Image

To avoid reinstalling tools every time the container starts, we can modify the Dockerfile to install them permanently. To achieve this, let’s modify the Dockerfile:

FROM python:3.9

WORKDIR /app

COPY . /app

# Install Flask and useful debug tools
RUN apt update && apt install -y net-tools iputils-ping && \
    pip install --no-cache-dir flask

EXPOSE 5000

CMD ["python3", "index.py"]

During image build, the Dockerfile now installs net-tools and iputils-ping. It makes these tools available every time we run a container from this image.

Thus, we manage to keep the Flask app behavior predictable during development or troubleshooting containerized environments.

7. Conclusion

In this article, we addressed server connection issues in the Docker deployment of a minimal Flask app.

Deploying a Flask app using Docker is straightforward. However, a small misconfiguration, such as binding the server to the wrong interface, can make the app unreachable. By default, Flask binds to 127.0.0.1, restricting access to inside the container only. That’s why we need to bind to 0.0.0.0.

Once we understand how Docker handles networking and port exposure, such problems become easy to troubleshoot and fix. Whether we’re building microservices or just experimenting with Docker, we can now debug similar issues in the future.