Dockerizing a Flask-based web camera application
Tags: docker, python, flask
Published: 2019-11-08 19:01:32

In this tutorial we'll be:
* [creating a Flask-based web app for taking pictures using OpenCV](#chapter-1);
* [dockerizing this app](#chapter-2); then
* [building a multi-container app with Docker Compose for photo capture and storage](#chapter-3).

## Creating our web camera app <a name="chapter-1"></a>
First off, we'll use [Flask](https://palletsprojects.com/p/flask/), a light web framework, to quickly set up a web application. This app will also use OpenCV for taking pictures.

For this part of the tutorial, we'll give a brief overview, since we're really focused on the dockerization aspect of this app, but all code can be found on github: {% github carlosemv/camera-app no-readme %}

There are two main pages for the app, `index`, where you can take a picture, and `capture`, where you can see a captured picture, and download it or email it to yourself or someone else.

So we define *routes* for each of these pages, which will render the respective templates:

```python
# route for index (picture-taking) page
@app.route('/index/')
def index():
    return render_template('index.html')

# route for capturing picture
@app.route('/capture/')
def capture():
    camera = get_camera()
    stamp = camera.capture()
    return redirect(url_for('show_capture', timestamp=stamp))

# route for showing captured picture
@app.route('/capture/image/<timestamp>', methods=['POST', 'GET'])
def show_capture(timestamp):
    path = stamp_file(timestamp)
    ...
    return render_template('capture.html',
        stamp=timestamp, path=path, email_msg=email_msg)
```
Note that we omit the code that handles email sending, which will be a `POST` request on the same route as that for showing a captured picture (implemented by `show_capture`).

Besides these functions, a critical part of our code is the one that gets the camera feed:
```python
def get_camera():
    global camera
    if not camera:
        camera = Camera()

    return camera

def gen(camera):
    while True:
        frame = camera.get_feed()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

@app.route('/video_feed/')
def video_feed():
    camera = get_camera()
    return Response(gen(camera),
        mimetype='multipart/x-mixed-replace; boundary=frame')
```
So Camera is a class we created, and which we'll get into in a minute. `get_camera` implements what is basically a singleton pattern, lazily loading a single instance of the Camera when necessary.

`video_feed` is a route used *inside* `index`, in the `index.html` template, as an image link:
```html
<div id="camera">
  <img src="{{ url_for('video_feed') }}" alt="video feed">
</div>
```
So the `video_feed` returns a Response object containing an iterator which continuously generates frames from our Camera, and the corresponding mimetype `multipart/x-mixed-replace`, which allows us to continuously replace one frame with the next.

`gen` is a generator function which constantly gets a camera frame (a binary jpeg image) and returns binary data containing that image as well as the necessary headers.

Now, as for the Camera class: it basically instantiates an OpenCV VideoCapture object and gives frames as necessary, also capturing a particular frame when requested. Capturing implies saving the image locally (temporarily, at least), and returning a timestamp which serves as an unique identifier. 

```python
class Camera(object):
    CAPTURES_DIR = "static/captures/"
    def __init__(self):
        self.video = cv.VideoCapture(0)

    def get_frame(self):
        success, frame = self.video.read()
        ...  
        return frame

    def get_feed(self):
        frame = self.get_frame()
        if frame is not None:
            ret, jpeg = cv.imencode('.jpg', frame)
            return jpeg.tobytes()

    def capture(self):
        frame = self.get_frame()
        timestamp = strftime("%d-%m-%Y-%Hh%Mm%Ss", localtime())
        filename = Camera.CAPTURES_DIR + timestamp +".jpg"
        if not cv.imwrite(filename, frame):
            raise RuntimeError("Unable to capture image "+timestamp)
        return timestamp

```

We can now run this code with `python web.py`. Note that it's running on debug mode on `localhost:8080`. That can be changed in a number of ways, such as editing the line below on `main`, but be sure to check out the [Flask docs](https://flask.palletsprojects.com/) for more details.
```python
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, debug=True)
```

We can see below what our image capturing page and capture viewing page look like:
![Image capturing page](https://i.imgur.com/ZUOKfrU.png)
![Capture viewing page](https://i.imgur.com/3j7L4Lo.png)

## Dockerizing our camera app <a name="chapter-2"></a>
When building our Dockerfile, we'll start from a Linux base (Ubuntu, in this case) instead of using a straight Python environment, as it's a little more flexible, and makes dependency management clearer. But, you always have the option of building off of the Python image instead. This is done in the first line: `FROM ubuntu:18.04`.

Now, we embed some metadata into our image with the `LABEL` instruction. In this case, we simply define the project maintainer, like so: `LABEL maintainer="me@mail.com"` (this is of course a template, which should be replaced with your own information).

Now we get into configuring our custom environment. First we update and install necessary packages:
```docker
RUN apt-get update -y && apt-get install -y build-essential cmake \
libsm6 libxext6 libxrender-dev \
python3 python3-pip python3-dev
```
The `libsm6 libxext6 libxrender-dev` sequence of packages is important for a proper installation of OpenCV later on. Note that we will be using python 3, and pip as a package manager.

Next is `WORKDIR /app`. This creates a directory at root, and sets it as the working directory for any following commands. We copy our requirements file first, before any source code, with `COPY ./requirements.txt /app/requirements.txt`. We do this to take advantage of Docker's layer-based cache system.

Finally, the instruction `RUN pip3 install -r requirements.txt` installs all of our dependencies (Flask and OpenCV), and we are ready to run our app. Note that we don't have to specify the whole path to the requirements file, as it is already in our working directory.

So, as our container will run as an executable (as opposed to a shell), we define our entry point: `ENTRYPOINT ["python3"]`, then run our application with `CMD ["web.py"]`. Below, we can review our whole Dockerfile.


```docker
FROM ubuntu:18.04
LABEL maintainer="me@mail.com"

RUN apt-get update -y && apt-get install -y build-essential cmake \
libsm6 libxext6 libxrender-dev \
python3 python3-pip python3-dev

COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip3 install -r requirements.txt

COPY . /app
ENTRYPOINT ["python3"]
CMD ["web.py"]
```

Running `docker build -t camera-app .` will now build our image named camera-app. We can then instantiate a container from it with the following command: `docker run -d --name cam --device /dev/video0 -p 8080:8080 camera-app`.
- `-d`, or `--detach`, makes the container run in the background.
- `--device /dev/video0` adds a device from the host, in this case a camera, to the container.
- `-p 8080:8080`, for `--publish`, will publish or map a container's port (the one on the right side of the colon) to a port on the host (on the left).

## Photo storage with docker-compose <a name="chapter-3"></a>
Now, we have our Flask web app working with Docker. But, pictures captured do not persist and are not easily accessible, being in a temporary volume created by Docker.

So we will use a self-hosted file storage server with a web interface called droppy: {% github silverwind/droppy no-readme %}

We use droppy because it is very lightweight, has a simple web interface, and has a very easy setup, since it provides a ready to use Docker image. But keep in mind another type of server for image storage could also be used with Docker, and would have a very similar setup.

Droppy will run as a separate container from the one running our camera app. But these two container can be orchestrated together easily, thanks to Docker Compose.

Our docker-compose file (which is a yaml data file) will have two important parts: `volumes` and `services`. In `volumes`, we create a volume called `captures`, which will store our captured images and be shared between the two containers.

In `services`, we configure our two services, which will run off of our two images: camera-app, and droppy. In the file below, we can see these two services have some things in common. For both, we define
- `container_name`, the name of the container , once it is instantiated;
- `image`, the image which will be used to instantiate the container;
- `ports`, similarly to the `-p` flag discussed previously, will map the necessary ports for the localhost; and
- `volumes` will mount the volume we created (`captures`) into the directory where the images are saved in the camera-app container, and also into the file storage directory in the droppy container.

This last instruction makes it so that, essentially, images are uploaded to droppy as soon as they are captured by our camera app. Besides these shared fields, we still have to map our camera device from the host to the camera app container, as we have before, on `devices`.

```yaml
version: '3'
volumes:
  captures:
services:
  droppy:
    container_name: droppy
    image: silverwind/droppy
    ports:
      - '127.0.0.1:8989:8989'
    volumes:
      - captures:/files
  camera-app:
    container_name: cam
    image: camera-app
    ports:
      - '127.0.0.1:8080:8080'
    devices:
      - '/dev/video0:/dev/video0'
    volumes:
      - captures:/app/static/captures
```

Running this setup with `docker-compose up`, we can now access our camera at `localhost:8080`, and our droppy image server at `localhost:8989`. Any image taken on the camera app will show up on droppy server, like we see below.

![droppy screenshot](https://i.imgur.com/IluEzla.png)

When you're done, make sure to use the `-v` flag when running `docker-compose down` if you wish to clear the volume we created, containing all images captured. Docker Compose makes integrating applications like this very straightforward, so have fun configuring this setup for your own needs, or running your very own containers!