|
| 1 | +--- |
| 2 | +author: |
| 3 | + name: Bob Strecansky |
| 4 | +description: 'This guide describes how to effectively use Docker in production using a sample NGINX/Flask/Gunicorn/Redis/Postgresql Application Stack.' |
| 5 | +og_description: 'This guide shows how to deploy a simple microservice using Docker. Best practices for using Docker in production are also demonstrated and explained.' |
| 6 | +keywords: ["docker", "nginx", "flask", "gunicorn", "redis", "postgresql", "microservice"] |
| 7 | +license: '[CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0)' |
| 8 | +modified: 2018-01-04 |
| 9 | +modified_by: |
| 10 | + name: Bob Strecansky |
| 11 | +published: 2018-01-04 |
| 12 | +title: 'How to Deploy Microservices with Docker' |
| 13 | +contributor: |
| 14 | + name: Bob Strecansky |
| 15 | + link: https://twitter.com/bobstrecansky |
| 16 | +external_resources: |
| 17 | +- '[Github Repository for Example Microservice](https://github.com/bobstrecansky/flask-microservice)' |
| 18 | +- '[Using Containers to Build a Microservices Architecture](https://medium.com/aws-activate-startup-blog/using-containers-to-build-a-microservices-architecture-6e1b8bacb7d1)' |
| 19 | +--- |
| 20 | + |
| 21 | +## What is a Microservice? |
| 22 | + |
| 23 | +Microservices are an increasingly popular architecture for building large-scale applications. Rather than using a single, monolithic codebase, applications are broken down into a collection of smaller components called microservices. This approach offers several benefits, including the ability to scale individual microservices, keep the codebase easier to understand and test, and enable the use of different programming languages, databases, and other tools for each microservice. |
| 24 | + |
| 25 | +[Docker](https://www.docker.com) is an excellent tool for managing and deploying microservices. Each microservice can be further broken down into processes running in separate Docker containers, which can be specified with Dockerfiles and Docker Compose configuration files. Combined with a provisioning tool such as Kubernetes, each microservice can then be easily deployed, scaled, and collaborated on by a developer team. Specifying an environment in this way also makes it easy to link microservices together to form a larger application. |
| 26 | + |
| 27 | +This guide shows how to build and deploy an example microservice using Docker and Docker Compose. |
| 28 | + |
| 29 | +## Before You Begin |
| 30 | + |
| 31 | +You will need a Linode with Docker and Docker Compose installed to complete this guide. |
| 32 | + |
| 33 | +### Install Docker |
| 34 | + |
| 35 | +{{< section file="/shortguides/docker/install_docker_ce.md" >}} |
| 36 | + |
| 37 | +### Install Docker Compose |
| 38 | + |
| 39 | +{{< section file="/shortguides/docker/install_docker_compose.md" >}} |
| 40 | + |
| 41 | +## Prepare the Environment |
| 42 | + |
| 43 | +This section uses Dockerfiles to configure Docker images. For more information about Dockerfile syntax and best practices, see our [How To Use Dockerfiles guide](/docs/applications/containers/how-to-use-dockerfiles) and Docker's [Dockerfile Best Practices guide](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#sort-multi-line-arguments). |
| 44 | + |
| 45 | +1. Create a directory for the microservice: |
| 46 | + |
| 47 | + mkdir flask-microservice |
| 48 | + |
| 49 | +2. Create the directory structure for the microservice components within the new directory: |
| 50 | + |
| 51 | + cd flask-microservice |
| 52 | + mkdir nginx postgres web |
| 53 | + |
| 54 | +### NGINX |
| 55 | + |
| 56 | +1. Within the new `nginx` subdirectory, create a Dockerfile for the NGINX image: |
| 57 | + |
| 58 | + {{< file "nginx/Dockerfile" yaml >}} |
| 59 | +from nginx:alpine |
| 60 | +COPY nginx.conf /etc/nginx/nginx.conf |
| 61 | +{{</ file >}} |
| 62 | + |
| 63 | +2. Create the `nginx.conf` referenced in the Dockerfile: |
| 64 | + |
| 65 | + {{< file "/nginx/nginx.conf" conf >}} |
| 66 | +user nginx; |
| 67 | +worker_processes 1; |
| 68 | +error_log /dev/stdout info; |
| 69 | +error_log off; |
| 70 | +pid /var/run/nginx.pid; |
| 71 | + |
| 72 | +events { |
| 73 | + worker_connections 1024; |
| 74 | + use epoll; |
| 75 | + multi_accept on; |
| 76 | +} |
| 77 | + |
| 78 | +http { |
| 79 | + include /etc/nginx/mime.types; |
| 80 | + default_type application/octet-stream; |
| 81 | + |
| 82 | + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
| 83 | + '$status $body_bytes_sent "$http_referer" ' |
| 84 | + '"$http_user_agent" "$http_x_forwarded_for"'; |
| 85 | + |
| 86 | + access_log /dev/stdout main; |
| 87 | + access_log off; |
| 88 | + keepalive_timeout 65; |
| 89 | + keepalive_requests 100000; |
| 90 | + tcp_nopush on; |
| 91 | + tcp_nodelay on; |
| 92 | + |
| 93 | + server { |
| 94 | + listen 80; |
| 95 | + proxy_pass_header Server; |
| 96 | + |
| 97 | + location / { |
| 98 | + proxy_set_header Host $host; |
| 99 | + proxy_set_header X-Real-IP $remote_addr; |
| 100 | + |
| 101 | + # app comes from /etc/hosts, Docker added it for us! |
| 102 | + proxy_pass http://flaskapp:8000/; |
| 103 | + } |
| 104 | + } |
| 105 | +} |
| 106 | +{{</ file >}} |
| 107 | + |
| 108 | +### PostgreSQL |
| 109 | + |
| 110 | +The PostgreSQL image for this microservice will use the official `postgresql` image on Docker Hub, so no Dockerfile is necessary. |
| 111 | + |
| 112 | +In the `postgres` subdirectory, create an `init.sql` file: |
| 113 | + |
| 114 | +{{< file "postgres/init.sql">}} |
| 115 | +SET statement_timeout = 0; |
| 116 | +SET lock_timeout = 0; |
| 117 | +SET idle_in_transaction_session_timeout = 0; |
| 118 | +SET client_encoding = 'UTF8'; |
| 119 | +SET standard_conforming_strings = on; |
| 120 | +SET check_function_bodies = false; |
| 121 | +SET client_min_messages = warning; |
| 122 | +SET row_security = off; |
| 123 | +CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; |
| 124 | +COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; |
| 125 | +SET search_path = public, pg_catalog; |
| 126 | +SET default_tablespace = ''; |
| 127 | +SET default_with_oids = false; |
| 128 | +CREATE TABLE visitors ( |
| 129 | + site_id integer, |
| 130 | + site_name text, |
| 131 | + visitor_count integer |
| 132 | +); |
| 133 | + |
| 134 | +ALTER TABLE visitors OWNER TO postgres; |
| 135 | +COPY visitors (site_id, site_name, visitor_count) FROM stdin; |
| 136 | +1 linodeexample.com 0 |
| 137 | +\. |
| 138 | +{{</ file >}} |
| 139 | + |
| 140 | +{{< caution >}} |
| 141 | +In Line 22 of `init.sql`, make sure your text editor does not convert tabs to spaces. The app will not work without tabs between the entries in this line. |
| 142 | +{{< /caution >}} |
| 143 | + |
| 144 | +### Web |
| 145 | + |
| 146 | +The `web` image will hold an example Flask app. Add the following files to the `web` directory to prepare the app: |
| 147 | + |
| 148 | +1. Create a `.python-version` file to specify the use of Python 3.6: |
| 149 | + |
| 150 | + echo "3.6.0" >> web/.python-version |
| 151 | + |
| 152 | +2. Create a Dockerfile for the `web` image: |
| 153 | + |
| 154 | + {{< file "web/Dockerfile" yaml >}} |
| 155 | +from python:3.6.2-slim |
| 156 | +RUN groupadd flaskgroup && useradd -m -g flaskgroup -s /bin/bash flask |
| 157 | +RUN echo "flask ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers |
| 158 | +RUN mkdir -p /home/flask/app/web |
| 159 | +WORKDIR /home/flask/app/web |
| 160 | +COPY requirements.txt /home/flask/app/web |
| 161 | +RUN pip install --no-cache-dir -r requirements.txt |
| 162 | +RUN chown -R flask:flaskgroup /home/flask |
| 163 | +USER flask |
| 164 | +ENTRYPOINT ["/usr/local/bin/gunicorn", "--bind", ":8000", "linode:app", "--reload", "--workers", "16"] |
| 165 | +{{</ file >}} |
| 166 | + |
| 167 | +3. Create `web/linode.py` and add the example app script: |
| 168 | + |
| 169 | + {{< file "web/linode.py" >}} |
| 170 | +from flask import Flask |
| 171 | +import logging |
| 172 | +import psycopg2 |
| 173 | +import redis |
| 174 | +import sys |
| 175 | + |
| 176 | +app = Flask(__name__) |
| 177 | +cache = redis.StrictRedis(host='redis', port=6379) |
| 178 | + |
| 179 | +# Configure Logging |
| 180 | +app.logger.addHandler(logging.StreamHandler(sys.stdout)) |
| 181 | +app.logger.setLevel(logging.DEBUG) |
| 182 | + |
| 183 | +def PgFetch(query, method): |
| 184 | + |
| 185 | + # Connect to an existing database |
| 186 | + conn = psycopg2.connect("host='postgres' dbname='linode' user='postgres' password='linode123'") |
| 187 | + |
| 188 | + # Open a cursor to perform database operations |
| 189 | + cur = conn.cursor() |
| 190 | + |
| 191 | + # Query the database and obtain data as Python objects |
| 192 | + dbquery = cur.execute(query) |
| 193 | + |
| 194 | + if method == 'GET': |
| 195 | + result = cur.fetchone() |
| 196 | + else: |
| 197 | + result = "" |
| 198 | + |
| 199 | + # Make the changes to the database persistent |
| 200 | + conn.commit() |
| 201 | + |
| 202 | + # Close communication with the database |
| 203 | + cur.close() |
| 204 | + conn.close() |
| 205 | + return result |
| 206 | + |
| 207 | +@app.route('/') |
| 208 | +def hello_world(): |
| 209 | + if cache.exists('visitor_count'): |
| 210 | + cache.incr('visitor_count') |
| 211 | + count = (cache.get('visitor_count')).decode('utf-8') |
| 212 | + update = PgFetch("UPDATE visitors set visitor_count = " + count + " where site_id = 1;", "POST") |
| 213 | + else: |
| 214 | + cache_refresh = PgFetch("SELECT visitor_count FROM visitors where site_id = 1;", "GET") |
| 215 | + count = int(cache_refresh[0]) |
| 216 | + cache.set('visitor_count', count) |
| 217 | + cache.incr('visitor_count') |
| 218 | + count = (cache.get('visitor_count')).decode('utf-8') |
| 219 | + return 'Hello Linode! This page has been viewed %s time(s).' % count |
| 220 | + |
| 221 | +@app.route('/resetcounter') |
| 222 | +def resetcounter(): |
| 223 | + cache.delete('visitor_count') |
| 224 | + PgFetch("UPDATE visitors set visitor_count = 0 where site_id = 1;", "POST") |
| 225 | + app.logger.debug("reset visitor count") |
| 226 | + return "Successfully deleted redis and postgres counters" |
| 227 | + |
| 228 | +{{</ file >}} |
| 229 | + |
| 230 | +4. Add a `requirements.txt` file with the required Python dependencies: |
| 231 | + |
| 232 | + {{< file "web/requirements.txt" text >}} |
| 233 | +flask |
| 234 | +gunicorn |
| 235 | +psycopg2 |
| 236 | +redis |
| 237 | +{{</ file >}} |
| 238 | + |
| 239 | +## Docker Compose |
| 240 | + |
| 241 | +Docker Compose will be used to be define the connections between containers and their configuration settings. |
| 242 | + |
| 243 | +Create a `docker-compose.yml` file in the `flask-microservice` directory and add the following: |
| 244 | + |
| 245 | +{{< file "docker-compose.yml" yaml >}} |
| 246 | +version: '3' |
| 247 | +services: |
| 248 | + # Define the Flask web application |
| 249 | + flaskapp: |
| 250 | + |
| 251 | + # Build the Dockerfile that is in the web directory |
| 252 | + build: ./web |
| 253 | + |
| 254 | + # Always restart the container regardless of the exit status; try and restart the container indefinitely |
| 255 | + restart: always |
| 256 | + |
| 257 | + # Expose port 8000 to other containers (not to the host of the machine) |
| 258 | + expose: |
| 259 | + - "8000" |
| 260 | + |
| 261 | + # Mount the web directory within the container at /home/flask/app/web |
| 262 | + volumes: |
| 263 | + - ./web:/home/flask/app/web |
| 264 | + |
| 265 | + # Don't create this container until the redis and postgres containers (below) have been created |
| 266 | + depends_on: |
| 267 | + - redis |
| 268 | + - postgres |
| 269 | + |
| 270 | + # Link the redis and postgres containers together so that they can talk to one another |
| 271 | + links: |
| 272 | + - redis |
| 273 | + - postgres |
| 274 | + |
| 275 | + # Pass environment variables to the flask container (this debug level lets you see more useful information) |
| 276 | + environment: |
| 277 | + FLASK_DEBUG: 1 |
| 278 | + |
| 279 | + # Deploy with 3 replicas in the case of failure of one of the containers (only in Docker Swarm) |
| 280 | + deploy: |
| 281 | + mode: replicated |
| 282 | + replicas: 3 |
| 283 | + |
| 284 | + # Define the redis Docker container |
| 285 | + redis: |
| 286 | + |
| 287 | + # use the redis:alpine image: https://hub.docker.com/_/redis/ |
| 288 | + image: redis:alpine |
| 289 | + restart: always |
| 290 | + deploy: |
| 291 | + mode: replicated |
| 292 | + replicas: 3 |
| 293 | + |
| 294 | + # Define the redis NGINX forward proxy container |
| 295 | + nginx: |
| 296 | + |
| 297 | + # build the nginx Dockerfile: http://bit.ly/2kuYaIv |
| 298 | + build: nginx/ |
| 299 | + restart: always |
| 300 | + |
| 301 | + # Expose port 80 to the host machine |
| 302 | + ports: |
| 303 | + - "80:80" |
| 304 | + deploy: |
| 305 | + mode: replicated |
| 306 | + replicas: 3 |
| 307 | + |
| 308 | + # The Flask application needs to be available for NGINX to make successful proxy requests |
| 309 | + depends_on: |
| 310 | + - flaskapp |
| 311 | + |
| 312 | + # Define the postgres database |
| 313 | + postgres: |
| 314 | + restart: always |
| 315 | + # Use the postgres alpine image: https://hub.docker.com/_/postgres/ |
| 316 | + image: postgres:alpine |
| 317 | + |
| 318 | + # Mount an initialization script and the persistent postgresql data volume |
| 319 | + volumes: |
| 320 | + - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql |
| 321 | + - ./postgres/data:/var/lib/postgresql/data |
| 322 | + |
| 323 | + # Pass postgres environment variables |
| 324 | + environment: |
| 325 | + POSTGRES_PASSWORD: linode123 |
| 326 | + POSTGRES_DB: linode |
| 327 | + |
| 328 | + # Expose port 5432 to other Docker containers |
| 329 | + expose: |
| 330 | + - "5432" |
| 331 | +{{</ file >}} |
| 332 | + |
| 333 | + |
| 334 | +## Test the Microservice |
| 335 | + |
| 336 | +1. Use Docker Compose to build all of the images and start the microservice: |
| 337 | + |
| 338 | + cd flask-microservice/ && docker-compose up |
| 339 | + |
| 340 | + You should see all of the services start in your terminal. |
| 341 | + |
| 342 | +2. Open a new terminal window and make a request to the example application: |
| 343 | + |
| 344 | + curl localhost |
| 345 | + |
| 346 | + {{< output >}} |
| 347 | +Hello Linode! This page has been viewed 1 time(s). |
| 348 | +{{< /output >}} |
| 349 | + |
| 350 | +3. Reset the page hit counter: |
| 351 | + |
| 352 | + curl localhost/resetcounter |
| 353 | + |
| 354 | + {{< output >}} |
| 355 | +Successfully deleted redis and postgres counters |
| 356 | +{{< /output >}} |
| 357 | + |
| 358 | +4. Return to the terminal window where Docker Compose was started to view the standard out log: |
| 359 | + |
| 360 | + {{< output >}} |
| 361 | +flaskapp_1 | DEBUG in linode [/home/flask/app/web/linode.py:56]: |
| 362 | +flaskapp_1 | reset visitor count |
| 363 | +{{< /output >}} |
| 364 | + |
| 365 | +## Using Containers in Production: Best Practices |
| 366 | + |
| 367 | +The containers used in the example microservice are intended to demonstrate the following best practices for using containers in production: |
| 368 | + |
| 369 | +Containers should be: |
| 370 | + |
| 371 | +1. **Ephemeral**: It should be easy to stop, destroy, rebuild, and redeploy containers with minimal setup and configuration. |
| 372 | + |
| 373 | + The Flask microservice is an ideal example of this. The entire microservice can be brought up or down using Docker Compose. No additional configuration is necessary after the containers are running, which makes it easy to modify the application. |
| 374 | + |
| 375 | +2. **Disposable**: Ideally, any single container within a larger application should be able to fail without impacting the performance of the application. Using a `restart: on-failure` option in the `docker-compose.yml` file, as well as having a replica count, makes it possible for some containers in the example microservice to fail gracefully while still serving the web application with no degradation to the end user. |
| 376 | + |
| 377 | + {{< note >}} |
| 378 | +The replica count directive will only be effective when this configuration is deployed as part of a [Docker Swarm](/docs/applications/containers/how-to-create-a-docker-swarm-manager-and-nodes-on-linode/), which is not covered in this guide. |
| 379 | +{{< /note >}} |
| 380 | + |
| 381 | +3. **Quick to start**: Avoiding additional installation steps in the Docker file, removing dependencies that aren't needed, and building a target image that can be reused are three of the most important steps in making a web application that has a quick initialization time within Docker. The example application uses short, concise, prebuilt Dockerfiles in order to minimize initialization time. |
| 382 | + |
| 383 | +4. **Quick to stop**: Validate that a `docker kill --signal=SIGINT {APPNAME}` stops the application gracefully. This, along with a restart condition and a replica condition, will ensure that when containers fail, they will be brought back online efficiently. |
| 384 | + |
| 385 | +5. **Lightweight**: Use the smallest base container that provides all of the utilities that are needed to build and run your application. Many Docker images are based on [Alpine Linux](https://alpinelinux.org/about/), a light and simple Linux distribution that takes up only 5MB in a Docker image. Using a small distro saves network and operational overhead and greatly increases container performance. The example application uses alpine images where applicable (NGINX, Redis, and PostgreSQL), and uses a python-slim base image for the Gunicorn / Flask application. |
| 386 | + |
| 387 | +6. **Stateless**: Since they are ephemeral, containers typically shouldn't maintain state. An application's state should be stored in a separate, persistent data volume, as is the case with the microservice's PostgreSQL data store. The Redis key-value store does maintain data within a container, but this data is not application-critical; the Redis store will fail back gracefully to the database should the container not be able to respond. |
| 388 | + |
| 389 | +7. **Portable**: All of an app's dependencies that are needed for the container runtime should be locally available. All of the example microservice's dependencies and startup scripts are stored in the directory for each component. These can be checked into version control, making it easy to share and deploy the application. |
| 390 | + |
| 391 | +8. **Modular**: Each container should have one responsibility and one process. In this microservice, each of the major processes (NGINX, Python, Redis, and PostgreSQL) is deployed in a separate container. |
| 392 | + |
| 393 | +9. **Logging**: All containers should log to `STDOUT`. This uniformity makes it easy to view the logs for all of the processes in a single stream. |
| 394 | + |
| 395 | +10. **Resilient**: The example application restarts its containers if they are exited for any reason. This helps give your Dockerized application high availability and performance, even during maintenance periods. |
0 commit comments