1, 2, 3: Docker, Heroku, MongoDB Atlas, Python
1, 2, 3: Docker, Heroku, MongoDB Atlas, Python
This is a walkthrough explaining how to Dockerize and deploy on Heroku a Python application using MongoDB Atlas.
Why 1–2–3? Focus on simple essential aspects, clear code samples and relevant references.
The Application
It all starts with source code, of course, let’s create a Python app on top of the Flask framework.
The application provides a few REST endpoints and a DAO layer to access the database.
# app.py
from flask import Flask, request, Response
import logging
import requests, import os
try:
app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
except Exception as e:
logging.exception("Error at startup")
@app.route('/ping')
def ping():
"""
Ping the endpoint
:return:
"""
return "ping Ok"
def get_port():
"""
Retrieves port for env variable
:return:
"""
return int(os.environ.get("PORT", 5000))
if __name__ == '__main__':
app.run(debug=True, port=get_port(), host='0.0.0.0')
Verify you can run the application in your favourite IDE (PyCharm is mine) or from the command line (`python app.py`), it should respond to http://localhost:5000/ping
MongoDB Atlas
Sign up for MongoDB Atlas (if you do not have already an account) and set up your free MongoDB. It is straightforward following the Atlas step-by-step Wizard.
In a nutshell, you will need to:
create a cluster
create a DB user
allow incoming connections (edit firewall rules)
connect
A common mistake is to forget to allow your IP address (step #3 above): enter your current IP address or allow them all if your repository does not store critical data (for example for this tutorial).
The final and most interesting step is to connect to the MongoDB Atlas from your Python application
#db_client.py
from pymongo import MongoClient
connect_string = 'mongodb+srv://{db_username}:{db_password}@devcluster.s4lc7.mongodb.net/{db_name}?retryWrites=true&w=majority'
client = MongoClient(connect_string)
db = client.get_default_database()
# get user by name
def get_user_by_name(name):
return db.user.find_one({"name": name})
# add new user
def add_user(name):
ret = get_user_by_name(name)
if ret is None:
new_user = {"name": name}
x = db.user.insert_one(new_user)
ret = x.inserted_id
return ret
The connect_string
is hardcoded in the source code (don’t forget to replace the placeholders with the actual MongoDB username, password and DB name), this is done to simplify the example. You should really avoid that and instead, define the connection string (and any other configuration) as an environment variable.
Docker on Heroku
We have now a working application (you can add some unit testing too) and it is time to deploy.
The Heroku Docker Registry is a good solution to deploy your application for a few reasons:
you build a generic image that can be deployed on other Docker runtimes
Docker images are not subject to size restrictions unlike deployment from git repositories (max 500MB)
Pre-requisites
First, create a new Heroku application (using the Heroku CLI or the web interface) by choosing a good name ☺️
Secondly (and very importantly) log in to Heroku and in the Heroku Docker Registry
heroku login
heroku container:login
Dockerfile
Here is the Dockerfile for our example, starting from the python-slim
base image.
FROM python:3.9-slim
COPY app/ /app
EXPOSE 5000
WORKDIR /app
RUN pip install -r requirements.txt
ENTRYPOINT ["python"]
CMD ["app.py"]
Build, Push, Release
Time to rock: let’s Dockerize the application and push it to Heroku. Note the image tag (registry.heroku.com) and the process type (web).
# build the image
docker build -t myapp .
# tag
docker tag myapp registry.heroku.com/myapp/web
# push
docker push registry.heroku.com/myapp/web
# release (when deployment starts)
heroku container:release web -a myapp
Check the logs to see it is really happening
heroku logs -a myapp
Invoke the /ping
endpoint https://myapp.herokuapp.com/ping
TADAAAA!!! 😀
Config Vars
The last step is to remove the hardcoded database connection string (naughty developers!) and use instead an environment variable.
# read env variable
def get_mongo_connect_string():
return os.environ.get("MONGO_CONNECT_STRING", "")
In your local development environment is a good idea to create a .env
file which is typically gitignored (never pushed to your source repository) and uses the Python .dotenv package to load the values.
On Heroku, you want to define the environment variables as Config Vars: at deployment time Heroku will inject the variables (connection strings, tokens, secrets) in the Dyno provisioned for your application.
Conclusion
I hope this is helpful for developers who start to work with Docker and Heroku: the focus is mainly on providing clear code and configuration examples as they are the best way to learn.
References
Check out the walkthrough source code on GitHub
Docker Container Registry documentation on devcenter.heroku.com
Best practices for creating Dockerfiles on docker.com