The web development server that comes bundled with Flask is not robust, secure, or efficient enough to work in a production environment. In this chapter, production deployment options for Flask applications are examined.
Regardless of the hosting method used, there are a series of tasks that must be carried out when the application is installed on a production server. These include the creation or update of the database tables.
Having to run these tasks manually each time the application is installed or upgraded is error prone and time consuming. Instead, a command that performs all the required tasks can be added to flasky.py.
Example 17-1 shows a deploy command implementation that is appropriate for Flasky.
The functions invoked by this command were all created before; they are just invoked all together from a single command to simplify the deployment of the application.
If you have cloned the application’s Git repository on GitHub, you can run git checkout 17a to check out this version of the application.
These functions are all designed in a way that causes no problems if they are executed multiple times. Designing update functions in this way makes it possible to run just this deploy command every time an installation or upgrade is done without having to worry about side effects caused by a function that runs at the wrong time.
When the application is running in debug mode, Werkzeug’s interactive debugger appears whenever an error occurs. The stack trace of the error is displayed on the web page, and it is possible to look at the source code and even evaluate expressions in the context of each stack frame using Flask’s interactive web-based debugger.
The debugger is an excellent tool to debug application problems during development, but obviously it cannot be used in a production deployment. Errors that occur in production are silenced and instead the user receives a discrete code 500 error page. But luckily, the stack traces of these errors are not completely lost, as Flask writes them to a log file.
During startup, Flask creates an instance of Python’s logging.Logger class and attaches it to the application instance as app.logger. In debug mode, this logger writes to the console, but in production mode there are no handlers configured for it by default. Unless a handler is added, logs are not stored. The changes in Example 17-2 configure a logging handler that sends the errors that occur while running under the production configuration to the administrator email address configured in the FLASKY_ADMIN setting.
Recall that all configuration classes have an init_app() static method that is invoked by create_app(), which so far has not been used. In the implementation of this method for the ProductionConfig class, the application logger is now configured with a log handler that sends errors to an email recipient.
The logging level of the email logger is set to logging.ERROR, so only severe problems are going to be emailed. Messages logged on lesser levels can be logged to a file, syslog, or any other supported destination by adding the proper logging handlers. The logging method to use for these messages largely depends on the hosting platform.
If you have cloned the application’s Git repository on GitHub, you can run git checkout 17b to check out this version of the application.
The trend in application hosting is to host “in the cloud,” but this can mean many different things. At the most basic level, cloud hosting can mean that the application is installed on one or more virtual servers, which for all intents and purposes operate and feel like physical machines, but in reality are virtual machines managed by the cloud operator. An example of these types of servers are those available through the EC2 service from Amazon Web Services (AWS). Deploying an application to a virtual server is similar to doing a traditional deployment to a dedicated server, as described later in this chapter.
A more advanced deployment model is based on containers. A container isolates an application in an image of the application and its environment. A container image includes the application plus all the dependencies it needs to run. A container platform, such as Docker, can then install and execute a pregenerated container image on any system in which it runs.
Another deployment option, formally known as Platform as a Service (PaaS), frees the application developer from the mundane tasks of installing and maintaining the hardware and software platforms on which the application runs. In the PaaS model, a service provider offers a fully managed platform on which applications can run. All the application developer needs to do is upload the application code to the servers maintained by the provider, after which it automatically becomes available, usually within seconds. Most PaaS providers offer ways to dynamically “scale” the application by adding or removing servers as necessary to keep up with the number of requests received.
The remainder of this chapter offers an introduction to Heroku (one of the most popular PaaS providers), Docker containers, and finally traditional deployments, which are suitable for dedicated or virtual servers.
Heroku was one of the first PaaS providers, having been in business since 2007. The Heroku platform is very flexible and supports a long list of programming languages, including Python. To deploy an application to Heroku, the developer uses Git to push the application to Heroku’s special Git server, which automatically triggers the installation, upgrade, configuration, and deployment of the application.
Heroku uses units of computing called dynos to measure usage and charge for the service. The most common type of dyno is the web dyno, which represents a web server instance. An application can increase its request handling capacity by deploying more web dynos, each running an instance of the application. Another type of dyno is the worker dyno, which is used to perform background jobs or other support tasks.
The platform provides a large number of plug-ins and add-ons for databases, email support, and many other services. The following sections expand on some of the details involved in deploying Flasky to Heroku.
To work with Heroku, the application must be hosted in a Git repository. If you are working with an application that is hosted on a remote Git server, such as GitHub or Bitbucket, cloning the application will create a local Git repository that is perfect to use with Heroku. If the application isn’t already hosted in a Git repository, you’ll need to create one for it on your development machine.
If you plan on hosting your application on Heroku, it is a good idea to start using Git from the very beginning. GitHub has installation and setup guides for the three major operating systems in its help guide.
You must create an account with Heroku before you can use the service. Heroku provides a free tier that allows you to host a few simple applications, so this is a great platform to experiment with.
To work with the Heroku service, the Heroku CLI must be installed. This is a command-line client that manages the interactions with the service. Heroku provides installers for the three major operating systems.
The first thing to do after installing the CLI is to authenticate with your Heroku account through the heroku login command:
It is important that your SSH public key is uploaded to Heroku, as this is what enables the git push command. Normally the login command creates and uploads an SSH public key automatically, but the heroku keys:add command can be used to upload your public key separately from the login command or if you need to upload additional keys.
The next step is to create an application. Before this is done, the application needs to be under Git source control. If you have been using the GitHub repository to follow along with the code in this book, then you already have a Git repository. If not, you will need to create one now. To register the application with Heroku, run the following command from the application’s top-level directory:
Heroku application names must be unique across all customers, so you need to think of a name that is not taken by any other application. As indicated by the output of thecreate command, once deployed the application will be available at https://.herokuapp.com. Heroku also supports using a custom domain name for your application. As part of the application creation, Heroku creates a Git server dedicated to your application at https://git.heroku.com/.git. The create command adds this server to your local Git repository as a git remote with the name heroku: The flask command requires the FLASK_APP environment variable to be set to work. To make sure that any commands that are executed in the Heroku environment succeed, it is a good idea to register this environment variable so that it is always set when Heroku executes commands related to this application. This can be done with the config command:
Heroku supports Postgres databases as an add-on. The free service tier includes a small database of up to 10,000 rows. To attach a Postgres database to your application, use the following command:
As indicated by the output of the command, once the application runs inside the Heroku platform, it will see the database location and credentials in the DATABASE_URL environment variable. The format of this variable is a URL, exactly in the format SQLAlchemy expects. Recall that the config.py script uses the value of DATABASE_URL if it is defined, so the connection to the Postgres database will work automatically.
Logging of fatal errors by email was added earlier, but in addition to that it is important to configure logging of lesser message categories. A good example of these types of messages are the warnings for slow database queries added in Chapter 16.
Heroku considers any output written by the application to stdout or stderr logs, so a logging handler needs to be added to generate this output. The logging output is captured by Heroku and made accessible through the Heroku client with the heroku logs command.
The logging configuration can be added to the ProductionConfig class in its init_app() static method. But since this type of logging is specific to Heroku, a better approach is to define a new configuration specifically for this platform, leaving ProductionConfig as a baseline configuration for different types of production platforms. The HerokuConfig class is shown in Example 17-3.
When the application is executed by Heroku, it needs to know that this new configuration needs to be used. The application instance created in flasky.py uses the FLASK_CONFIG environment variable to know what configuration to use, so this variable needs to be set appropriately in the Heroku environment. Environment variables for the Heroku environment are set using the Heroku client’s config:set command:
To increase the security of your application, it is a good idea to configure a difficult-to-guess string as the application’s secret key, which is used to sign the user session and the authentication tokens. The Config base class includes the SECRET_KEY attribute for this purpose, and sets its value from an environment variable of the same name if it exists. When working on the application in your development system it is okay to leave this variable undefined and let the Config class configure a hardcoded value, but on a production platform it is extremely important to set a strong secret key that is not known to anyone, since a leaked key will enable an attacker to forge the contents of the user session or generate valid tokens. To make your key secure, just set the SECRET_KEY environment variable to a unique string that is not stored anywhere:
There are many ways to generate random strings that are appropriate to be used as secret keys. You can do so with Python as follows:
Heroku does not provide an SMTP server, so an external server must be configured. There are several third-party add-ons that integrate production-ready email sending support with Heroku, but for testing and evaluation purposes it is sufficient to use the default Gmail configuration inherited from the base Config class.
Because it can be a security risk to embed login credentials directly in the script, the username and password to access the Gmail SMTP server are provided as environment variables (if you haven’t yet, it is a very good idea that instead of using your personal email account you create a secondary email to use for testing):
Heroku installs package dependencies from a requirements.txt file stored in the top-level directory of the application. All the dependencies in this file will be imported into a virtual environment managed by Heroku as part of the deployment.
The Heroku requirements file must include all the common requirements for the production version of the application, plus the psycopg2 package that enables SQLAlchemy to access the Postgres database. A heroku.txt file with these dependencies can be added in the requirements directory and then imported from the top-level requirements.txt file as shown in Example 17-4.
-r requirements/heroku.txt
When the user logs in to the application by submitting a username and a password in a web form, these values are at risk of being intercepted by a malicious third party, as discussed several times before. During development this is not a problem, but this risk needs to be eliminated when you deploy the application on a production server. To prevent user credentials from being exposed while in transit, it is necessary to use secure HTTP, which encrypts all the communications between clients and the server using public key cryptography.
Heroku makes all applications that are accessed on the herokuapp.com domain available on both http:// and https:// without any configuration required. Because the application runs on Heroku’s domain, it will use Heroku’s own SSL certificate. The only necessary action to fully secure the application is to intercept any requests sent to the http:// interface and redirect them to https://, which is exactly what the Flask-SSLify extension does.
As usual, Flask-SSLify is installed with pip:
The code that activates this extension is added to the application factory function, as shown in Example 17-5.
Support for SSL needs to be enabled only in production mode, and only when the platform supports it. To make it easy to switch SSL on and off, a new configuration variable called SSL_REDIRECT is added. The base Config class sets it to False, so that SSL redirects are not used by default, and the class HerokuConfig overrides it so that only on that configuration are the redirects issued. The implementation of this configuration variable is shown in Example 17-6.
The value of SSL_REDIRECT in HerokuConfig is only set to True if the environment variable DYNO exists. This variable is set by Heroku in its environment, so using the Heroku configuration for local testing does not activate the SSL redirects.
With these changes, the users will be forced to use the SSL server when accessing the application on Heroku—but there is one more detail that needs to be handled to make this support complete. When using Heroku, clients do not connect to the application directly but to a reverse proxy server. The reverse proxy server receives requests from many applications, and forwards them to each of them as appropriate. In this type of setup, only the proxy server runs in SSL mode; the SSL connection is terminated at the proxy server, and applications receive the forwarded requests from the proxy server without encryption. This presents a problem when the application needs to generate absolute URLs, because in the Flask application the request object describes the forwarded request, which is not encrypted, and not the original request sent by the client through an encrypted connection.
An example of the problem this can cause is with the generation of account confirmation or password reset links that are sent by email to users. When url_for() is called with _external=True to generate an absolute URL for these links, Flask will use http:// for them, because it does not know that there is a reverse proxy that is accepting encrypted connections from the outside.
Proxy servers pass information that describes the original request from the client to the redirected web servers through custom HTTP headers, so it is possible to determine whether the user is communicating with the application over SSL by looking at these headers. Werkzeug provides a WSGI middleware that checks the custom headers from the proxy server and updates the request object accordingly so that, for example, request.is_secure reflects the encryption state of the request that the client sent to the reverse proxy server and not the request that the proxy server then forwarded to the application. Example 17-7 shows how to add the ProxyFix middleware to the application.
The middleware is added in the initialization method for the Heroku configuration. WSGI middlewares such as ProxyFix are added by wrapping the WSGI application. When a request comes, the middlewares get a chance to inspect the environment and make changes before the request is processed. The ProxyFix middleware is necessary not only for Heroku but in any deployment that uses a reverse proxy server.
Heroku expects applications to start their own production web server and configure it to listen to requests on the port number set in the environment variable PORT.
The development web server that comes with Flask will perform very poorly in this situation because it is not designed to run in a production environment. Two production-ready web servers that work well with Flask applications are Gunicorn and uWSGI.
It is a good idea to install the chosen web server in the local virtual environment, so that it can be tested in a way similar to how it will run in the Heroku environment. For example, Gunicorn is installed as follows:
To run the application locally under Gunicorn, use the following command:
The flasky:app argument tells Gunicorn where the application instance is located. The name given before the colon is the package or module that defines this instance, while the name after the colon is the actual application instance name. Note that Gunicorn uses port 8000 by default, not 5000 like Flask. Like the Flask development web server, you can exit Gunicorn with Ctrl+C.
The Gunicorn web server does not work on Microsoft Windows. The other recommended web server, uWSGI, does work on Windows, but it can be difficult to install due to it being written in native code. If you want to test the Heroku deployment on your Windows system, you can use Waitress, which is another pure Python web server that is in many ways similar to Gunicorn but has the advantage that it fully supports Windows. Waitress is installed with pip:
To start the Waitress web server, use the waitress-serve command:
Heroku needs to know what command to use to start the application. This command is given in a special file called Procfile. This file must be included in the top-level directory of the application.
Example 17-8 shows the contents of this file.
The format for the Procfile is very simple: in each line a task name is given, followed by a colon and then the command that runs the task. The task name web is special; it is recognized by Heroku as the task that starts the web server. Heroku will give this task a PORT environment variable set to the port on which the application needs to listen for requests. Gunicorn by default honors the PORT variable if it is set in the environment, so there is no need to include it in the startup command.
If you are using Microsoft Windows, or need your application to be fully compatible with that platform, you can instead use the Waitress web server:
Applications can declare additional tasks with names other than web in the Procfile. Each task included in the Procfile will be started on its own dyno.
If you have cloned the application’s Git repository on GitHub, you can run git checkout 17c to check out this version of the application. If you are using Microsoft Windows, run git checkout 17c-waitress to check out a version of the application configured to use the Waitress web server instead of Gunicorn.
The Heroku CLI includes the local command, used to run the application locally in a very similar way to how it runs on the Heroku servers. However, environment variables such as FLASK_APP are not available when running the application locally. The heroku local command looks for environment variables that configure the application in a file named .env in the top-level directory of the application. For example, the .env file can contain the following variables:
Because the .env file contains passwords and other sensitive account information, it should never be added to source control.
Before the application can be started, the deployment task needs to be executed to set up the database. One-off tasks can be executed with the local:run command:
(venv) $ heroku local:run flask deploy
[OKAY] Loaded ENV .env File as KEY=VALUE Format
INFO Context impl SQLiteImpl.
INFO Will assume non-transactional DDL.
INFO Running upgrade -> 38c4e85512a9, initial migration
INFO Running upgrade 38c4e85512a9 -> 456a945560f6, login support
INFO Running upgrade 456a945560f6 -> 190163627111, account confirmation
INFO Running upgrade 190163627111 -> 56ed7d33de8d, user roles
INFO Running upgrade 56ed7d33de8d -> d66f086b258, user information
INFO Running upgrade d66f086b258 -> 198b0eebcf9, caching of avatar hashes
INFO Running upgrade 198b0eebcf9 -> 1b966e7f4b9e, post model
INFO Running upgrade 1b966e7f4b9e -> 288cd3dc5a8, rich text posts
INFO Running upgrade 288cd3dc5a8 -> 2356a38169ea, followers
INFO Running upgrade 2356a38169ea -> 51f5ccfba190, comments
The heroku local command reads the Procfile and executes the tasks defined by it:
The logging output of all the tasks started by this command is consolidated into a single stream that is printed to the console, with each line prefixed with a timestamp and the task name.
The heroku local command also allows simulation of the use of multiple dynos to scale the application. The following command starts three web workers, each listening on a different port:
The final step in the process is to upload the application to the Heroku servers. Make sure that all the changes are committed to the local Git repository and then use git push heroku master to upload the application to the heroku remote:
$ git push heroku master
Counting objects: 502, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (426/426), done.
Writing objects: 100% (502/502), 108.03 KiB | 0 bytes/s, done.
Total 502 (delta 303), reused 146 (delta 61)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Python app detected
remote: -----> Installing python-3.6.2
remote: -----> Installing pip
remote: -----> Installing requirements with pip
...
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing...
remote: Done: 49.4M
remote: -----> Launching...
remote: Released v8
remote: https://<appname>.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/<appname>.git
* [new branch] master -> master
The application is now deployed and running, but it is not going to work correctly because the deploy command that initializes the database tables has not been executed yet. The Heroku client can run this command as follows:
After the database tables are created and configured, the application can be restarted so that it starts cleanly with an updated database:
The application should now be fully deployed and online at https://.herokuapp.com.The logging output generated by the application is captured by Heroku. To view the contents of the log, use the logs command:
During testing it can also be convenient to “tail” the log file, which can be done as follows:
When a Heroku application needs to be upgraded the same process needs to be repeated. After all the changes have been committed to the Git repository, the following commands perform an upgrade:
The maintenance option available on the Heroku CLI will take the application offline during the upgrade and will show a static page that informs users that the site will be coming back soon. This prevents users from accessing the application while it is going through the upgrade process.
You are now familiar with Heroku, which is a fairly high-level deployment option. In this section you will learn how to work with containers, and in particular with the Docker platform, which is not as automated as a PaaS but provides more flexibility and is not tied to a specific cloud provider.
Containers are a special type of virtual machine that run on top of the kernel of the host operating system, unlike standard virtual machines, which have their own virtualized kernel and hardware. Because the virtualization stops at the kernel, containers are much more lightweight and efficient than virtual machines, but they require dedicated support built into the operating system. The Linux kernel has full support for containers.
The most popular container platform is Docker, which has a free Community Edition (known as Docker CE) and a subscription-based Enterprise Edition (Docker EE). Docker can be installed on the three major desktop operating systems, and also on cloud servers. The easiest way to develop and test a “containerized” application is to install Docker CE on your development system. For macOS and Microsoft Windows there are one-click installers available from the Docker Store. This page also includes installation instructions for CentOS, Fedora, Debian, and Ubuntu Linux distributions.
After you complete the installation of Docker CE on your system, you should be able to access the docker command from your terminal:
Docker for Windows requires Microsoft’s Hyper-V feature to be enabled. The installer will normally enable it for you, but if Docker does not appear to work correctly after installation, the state of the Hyper-V hypervisor is the first thing to check. You should keep in mind that enabling Hyper-V on your Windows machine will prevent other hypervisors (such as Oracle’s VirtualBox) from working. If your system does not support Hyper-V virtualization, or you need a Docker solution that does not render other virtualization technologies unusable, you may want to install Docker Toolbox, a legacy Docker product for Windows that is based on VirtualBox.
The first task when working with containers is to build a container image for the application. An image is a snapshot of a container’s filesystem, used as a template when starting new containers. Docker expects the instructions to create the image to be provided in a file named Dockerfile. Example 17-9 shows a Dockerfile that builds the application featured in this book.
The build commands that can be included in a Dockerfile are documented in detail in the Dockerfile reference. In essence, these are deployment commands that install and configure the application in the container’s filesystem, which is isolated from your system.
The FROM command is required in all Dockerfiles to specify a base container image to start from. In most cases, this is going to be an image that is publicly available in Docker Hub, Docker’s container image repository. The repository contains official images for several Python interpreter versions. These are images that have a base operating system with Python installed on it. Images are specified with a name and a tag. The name of the official Docker Hub Python image is simply python. The different tags that are available can be seen in the Docker Hub page for the image. For the python image, tags are used to specify the desired interpreter version and platform. For this application, a 3.6 interpreter built on top of the Alpine Linux distribution is used. Alpine Linux is a platform commonly used in container images due to its small size.
The macOS and Windows versions of Docker are able to run Linux-based containers.
The ENV command defines runtime environment variables. This command takes two arguments: a variable name and its value. Any environment variables defined with this command will be available when a container based on this image is executed. The FLASK_APP variable required by the flask command is defined here, as is FLASK_CONFIG, which is the name of the configuration class the application uses to configure itself when it starts. The Docker deployment will use a new configuration called docker, implemented in a DockerConfig class as shown in Example 17-10. This new configuration class inherits from ProductionConfig and just configures logging to be directed to stderr, which Docker automatically captures and exposes through the docker logs command.
The RUN command executes a command in the context of the container image. In the first occurrence of RUN, a flasky user is created inside the container. The adduser command is part of Alpine Linux, and is available in the base image selected by the FROM command. The -D argument to adduser suppresses an interactive prompt for the user’s password.
The USER command selects the user under which the container will run, and also the user for the remaining commands in the Dockerfile. Docker uses the root user by default, but it is considered a good practice to switch to a regular user when root access isn’t needed.
The WORKDIR command defines the top-level directory where the application is going to be installed. For this application, the home directory for the newly created flasky user is used. The remaining commands in the Dockerfile will execute with this directory as the current directory.
The COPY command copies files from the local filesystem to the container’s filesystem. The requirements, app, and migrations directories are copied in their entirety, and then the top-level flasky.py, config.py, and new boot.sh files (discussed shortly) are copied as well.
The two additional RUN commands create a virtual environment and install the requirements in it. A dedicated requirements file was created for Docker as requirements/docker.txt. This file imports all the dependencies from requirements/common.txt and adds Gunicorn, which will be used as a web server as in the Heroku deployment.
The EXPOSE command defines the port on which the application running inside the container will install its server. When the container is started, Docker will map this port to a real port on the host machine, so that the container can receive requests from the outside world.
The final command is ENTRYPOINT. This command specifies how to execute the application when the container is started. The new boot.sh file, copied into the container above, is used as the startup script. Example 17-11 shows the contents of this file.
The script starts by activating the venv virtual environment that was created as part of the build. Then it runs the application’s deploy command, built earlier in this chapter and also used for the Heroku deployment. This will create a new database, upgrade it to the latest version, and insert the default roles. Because the DATABASE_URL environment variable hasn’t been set, the database will use the SQLite engine. Then a Gunicorn server listening on port 5000 is started. Docker captures all the output from the application and presents it as logs, so Gunicorn is configured to write both its access and error log files to standard output. Starting Gunicorn with exec makes the Gunicorn process take over the process running the boot.sh file. This is done because Docker pays special attention to the process that starts a container, and expects it to be the main process throughout its life. When this process ends, the container ends as well.
If you have cloned the application’s Git repository on GitHub, you can run git checkout 17d to check out this version of the application.
A container image for Flasky can now be built as follows:
The -t argument to docker build gives a name and tag to the container image, separated by a colon. The latest tag name is typically used for the most up-to-date version of a container image. The dot at the end of the build command sets the current directory as the top-level directory during the build. Docker will look for the Dockerfile in this directory, and will also make the files in this directory and all sub-directories available to be added to the container image.
As a result of a successful docker build command, the built container image is stored in a local image repository. The docker images command shows the contents of the image repository on your system:
This listing includes the just-built flasky:latest image and also the base Python 3.6 interpreter image referenced in the FROM Dockerfile, which Docker downloads and installs as part of the build.
Once a container image for the application is built, all that remains is to run it. The docker run command makes this a very simple task:
The --name option gives the container a name. Naming containers is optional; if a name is not given, Docker generates one using randomly selected words.
The -d option starts the container in detached mode, which means that the container will run in the background on your system. A container that is not detached runs as a foreground task attached to the console session.
The -p option maps port 8000 in the host system to port 5000 inside the container. Docker provides the flexibility of mapping container ports to any port in the host system. This mapping enables two or more instances of the same container image to run on different host ports, while each instance uses its own virtualized port 5000.
The -e option defines environment variables that are going to exist in the context of the container, in addition to any variables defined at build time with the ENV command in the Dockerfile. The value assigned to the SECRET_KEY variable ensures that user sessions and tokens are signed with a unique and very hard to guess key. You should generate your own unique key for this variable. The values for the MAIL_USERNAME and MAIL_PASSWORD variables configure email sending through the Gmail service. For a production deployment that uses a different email service provider the MAIL_SERVER, MAIL_PORT, and MAIL_USE_TLS variables should be defined as well.
The final argument in the docker run command is the container image and tag to execute. This should match the name and tag given as the -t option to the docker build command.
When the container starts in the background, the docker run command prints the container ID to the console. This is a 256-bit unique identifier printed in hexadecimal notation. This ID can be used in any commands that require a reference to a container (in practice, only the first few characters of the ID need to be provided, such that the container can be uniquely identified).
To confirm that the container is running, the docker ps command can be used:
To stop this container, use the docker stop command:
The stop command stops the container but does not remove it from the system. To remove it, use the docker rm command:
These two operations can be combined into one with docker rm -f:
When a container appears to misbehave, it might be necessary to debug it. The most obvious debugging mechanism is to add logging statements to the application and then monitor the running container with the docker logs command.
In some situations, however, it might be more convenient to open a shell session on the running container so that it can be inspected more closely. The docker exec command makes this possible:
In this example, Docker is going to open a shell session with sh (the Unix shell) without interrupting the container. The -it options connect the terminal session from which the command is issued to the new process, so that the shell can be operated interactively. If the container includes other, more advanced shells such as bash or even a Python interpreter, they can be used as well.
A common strategy when troubleshooting containers is to create a special image loaded with additional tools such as a debugger that can later be invoked from a shell session.
Having a container image locally is convenient when developing and testing an application, but when you are ready to share the image with others, you have to push it to an external registry server.
The Docker Hub registry is Docker’s image repository, a convenient service where you can host your images. A free Docker Hub account allows you to store an unlimited number of public container images, but only one private image. Paid plans increase the number of private images you can host. To create your Docker Hub account, go to https://hub.docker.com.
Once you have a Docker Hub account, you can log in to it from the command line with the docker login command:
Local container images are given a simple name. To prepare to push an image to Docker Hub, the image name must be prefixed with the Docker Hub account name and a slash as a separator. The flasky:latest image built earlier can be given a secondary name properly formatted for pushing to Docker Hub with the docker tag command:
To upload the image to Docker Hub, use the docker push command:
The container image is now publicly available, and anybody can start a container based on it with the docker run command:
One disadvantage of the way Flasky was deployed as a Docker container is that the default SQLite database lives in the same container as the application. This makes it very difficult to perform an upgrade, because once a running container is stopped, the database is gone with it.
A better approach is to host the database server separately from the application container. That makes upgrading the application while preserving the database an easy task, since all that is needed is to replace the application container with a new one.
Docker promotes a modular approach to building an application, in which each service is hosted in its own container. There are public container images available for MySQL, Postgres, and many other database servers. The docker run command can be used to deploy any of these directly to your system. The following command deploys a MySQL 5.7 database server to your system:
This command creates a container named mysql that runs in the background. The -e option assigns a few environment variables that this container takes as configuration. These and many other variables are documented in the Docker Hub page for the MySQL image. The preceding command configures the database with a randomly generated root password (use docker logs mysql right after starting the container to see the assigned password in the logs), and with a brand-new database called flasky that is configured to be accessed by a user named flasky as well. You need to provide a secure password for this user as a value for the MYSQL_PASSWORD environment variable.
To be able to connect to a MySQL database, SQLAlchemy requires a supported MySQL client package such as pymysql to be installed. This package can be added to the docker.txt requirements file.
If you have cloned the application’s Git repository on GitHub, you can run git checkout 17e to check out this version of the application.
The change made to the requirements/docker.txt file requires the container image to be rebuilt:
If you are still running the previous application container, stop it and remove it with docker rm -f. Then start a new container with the updated application:
There are two additions to the docker run command shown here. The --link option configures a connection between the new container and another existing one. The argument to --link consists of two names separated by a colon: the source container name or ID, and an alias for that container in the container being created. In this example the source container is mysql, the database container started earlier. This container is going to be accessible in the new Flasky container with the dbserver hostname.
To complete the configuration, a DATABASE_URL environment variable is added, with a connection URL that points to the flasky database in the mysql container. The dbserver alias is used as the database host, as Docker makes sure that this name resolves to the IP address of the linked container. The value of the MYSQL_PASSWORD environment variable set in the mysql container must be included in the connection URL for this container as well. The value of DATABASE_URL overrides the default SQLite database, so with this simple change the container will be configured to connect to the MySQL database.
The Docker Hub repository is a gold mine of very useful applications and services that are packaged and ready to use in a Docker environment, either standalone or as base images for your own containers. You will find that all sorts of projects (including databases, web servers, load balancers, programming languages, operating systems, and more) offer official images.
Containerized applications are usually composed of several running containers. You have seen in the previous section that the main application and the database server run in independent containers. As the application grows in complexity, it will invariably need more containers. Some applications are going to require additional services, such as message queues or caches. Other applications may take advantage of a microservices architecture and have a distributed structure with several smaller sub-applications, each running in its own container. Applications that have to handle high loads or need to be fault-tolerant will want to scale out by running several instances behind a load balancer.
As the number of containers that are part of the application increases, the task of managing and coordinating all these containers is going to become much harder if Docker alone is used. Container orchestration frameworks built on top of Docker help with this task.
The Compose toolset provides basic orchestration functionality, included with the Docker installation. With Compose, the containers that are part of an application are described in a configuration file, typically named docker-compose.yml. The docker-compose command can then start all the containers associated with the application using a single command.
Example 17-12 shows a docker-compose.yml file that represents the containerized Flasky along with its MySQL service.
This file is written in YAML, which is a clean and simple format that can represent hierarchical structures that are composed of key-value maps and lists. The version key specifies which version of Compose is used, and the services key defines the containers of the application as its children. In the case of Flasky, these are two services named flasky and mysql.
For services such as flasky, which are built as part of the application, the subkeys specify the arguments that are given to the docker build and docker run commands. The build key specifies the build directory, where the Dockerfile is located. The ports key specifies the network port mappings. The env_file key is a convenient way to define several environment variables that the container needs. The links key establishes a link to the MySQL container, by exposing it with the hostname dbserver. The restart key set to always provides a simple way for Docker to automatically restart the container if it exits unexpectedly. The .env file for this deployment should have the following variables in it:
The mysql service has a simpler structure, because this is a service that is started from a stock image that does not require a build step. The image key specifies the name and tag of the container image to use for this service. As with the docker run command, Docker will download this image from the container image registry. The env_file and restart keys are similar to those used in the flasky container. Note how the environment variables for the MySQL container are stored in a separate file named .env-mysql. While it would be easier to add the environment variables needed by all containers to the .env file, it is a good practice to prevent one container from having access to the secrets of another. The .env-mysql file needs the following environment variables defined:
The .env and .env-mysql files contain passwords and other sensitive information, so they should never be added to source control.
A complete reference for the docker-compose.yml file is found at “the Docker website”.
A typical problem with orchestrated systems is that containers are started in the wrong order—or in the correct order, but without giving the containers for base services enough time to start and initialize before starting higher-level containers that depend on them. In the case of Flasky, the mysql container needs to start first, so that the database is up and running when the flasky container starts. Then it can connect to the database, apply the database migrations, and finally start the web server.
Compose will start the mysql and flasky containers in the right order, because it will detect the dependency between them from the links key in the flasky container. But Compose is not going to wait for MySQL to start, which might take a few seconds. When designing distributed systems, it is a good practice to implement retries in all connections to external services. Example 17-13 shows how the boot.sh script that starts the flasky container can be made more robust by retrying the flask deploy command, which retries the database upgrade until it succeeds.
By running flask deploy inside a retry loop, the container will be able to tolerate failures due to the database service not being immediately ready to accept requests.
If you have cloned the application’s Git repository on GitHub, you can run git checkout 17f to check out this version of the application. Also make sure that the .env and .env-mysql environment files are created and populated with values correct for your environment.
Now that the Compose configuration is complete, the application can be started with the docker-compose up command:
The --build option to docker-compose up indicates that a build step should run before launching the application. This will cause the flasky container image to be built. After the image is created, the mysql and flasky containers will be started in that order. The -d option starts the containers in detached mode, as with the single container. After a few seconds, the application should be up and running in the background, and you should be able to connect to it at http://localhost:8000.
Compose consolidates the logging from all the containers into a single stream, which you can see with the docker-compose logs command:
Or, if you want to constantly monitor the log stream:
The docker-compose ps command shows a summary of all the application containers that are running and their state:
To upgrade an application to a new version, simply make the necessary changes to it and repeat the docker-compose up command used previously to start it. Compose will rebuild the application container if anything changed, and then replace the older container with a fresh one.
To stop the application, use the docker-compose down command, or docker-compose rm --stop --force if you also want to remove the stopped containers.
As you work with containers, your system will invariably accumulate old containers or images that are not needed anymore. It is a good idea to routinely review and clean those up, so that they don’t take up space on the system.
To see the list of containers in the system, use the following command:
This will show containers that are running, and containers that were stopped but are still in the system. To delete any containers from this list, use the docker rm -f command and provide the names or IDs to remove:
To see the list of container images stored in your system, use the docker images command. If there are any images that you want to remove, you can do so with the docker rmi command.
Some containers create virtual volumes on the host computer that are used for storage outside of the container filesystem. The MySQL container image, for example, puts all the database files in a volume. You can view a list of all the allocated volumes in your system with docker volume ls. To remove a volume that is unused, use docker volume rm.
If you prefer a more automatic cleanup, the docker system prune --volumes command will remove any unused images or volumes, and any stopped containers that are still in the system.
Many people consider Docker a development and testing platform only. While the techniques presented in the previous sections can be used to deploy applications on production servers running Docker, there are some limitations and security concerns that need to be considered:
Monitoring and alerting
What happens if a containerized application crashes? Docker can restart a container that exits unexpectedly, but it will not monitor your containers, nor will it send alerts when they behave erratically.
Logging
Docker maintains a separate log stream for each container. Compose improves this by offering a consolidated stream, but without long-term storage or searching and filtering capabilities.
Management of secrets
Configuring passwords and other credentials through environment variables is insecure, since Docker exposes pre-configured environment variables via the docker inspect command or through its API.
Reliability and scaling
To help with fault tolerance, or to accommodate increasing load demands, it is necessary to run several instances of the application on several hosts and behind one or more load balancers.
These limitations are generally addressed by more elaborated orchestration frameworks built on top of Docker or other container runtimes. Frameworks such as Docker Swarm (now incorporated into Docker), Apache Mesos, and Kubernetes are good choices for building robust container deployments.
So far you have seen how Heroku and Docker manage deployments. To complete this review of deployment strategies, this section will describe a traditional hosting option, which involves buying or renting a server, either physical or virtual, and manually setting up all the required components on it. This is obviously the most laborious option of all, but it can be a convenient option when you have terminal access to production server hardware. The following sections will give you an idea of the work involved.
There are several administration tasks that must be performed on the server before it can host applications:
Instead of manually performing these tasks, create a scripted deployment using an automation framework such as Ansible, Chef, or Puppet.
Similarly to Heroku and Docker, an application running on a standalone server relies on certain settings such as the database connection URL, email server credentials, etc. being provided in environment variables.
Because there is no Heroku or Docker to configure these variables before the application starts, the procedure to set the variables is dependent on the platform and tools used. To make the configuration of environment variables easier and uniform across deployment platforms, the short code block in Example 17-14 imports into the environment a .env file similar to the one used with the heroku local and docker-compose commands, using a Python package called python-dotenv that needs to be installed with pip. This is done in flasky.py before the application instance is created, so that by the time the configuration is imported these variables are accessible in the environment.
The .env file can define the FLASK_CONFIG variable that selects the configuration to use, the DATABASE_URL connection, the email server credentials, etc. As explained before, a .env file should not be added to source control due to the sensitive nature of some of the items in it.
If you created a .env file for use with Heroku or Docker, review it and adjust it appropriately, because with the changes just made, the application will import the variables defined in this file for all configurations.
For Unix-based servers, logging can be sent to the syslog daemon. A new configuration specifically for Unix can be created as a subclass of ProductionConfig, as shown in Example 17-15.
With this configuration, application logs will be written to the configured syslog messages file, typically /var/log/messages or /var/log/syslog depending on the Linux distribution. The syslog service can be configured to write a separate log file for application logs, or to send the logs to a different machine if desired.
If you have cloned the application’s Git repository on GitHub, you can run git checkout 17g to check out this version of the application.
Table of contents collapsed
Select a result to preview