├── .github └── FUNDING.yml ├── .travis.yml ├── LICENSE ├── README.md ├── flask ├── README.md ├── db │ └── Dockerfile ├── docker-compose.yml └── www │ ├── Dockerfile │ ├── index.py │ ├── requirements.txt │ └── templates │ └── index.html ├── php ├── README.md ├── docker-compose.yml ├── nginx │ ├── Dockerfile │ └── default.conf └── source │ └── index.php ├── symfony └── README.md └── traefik ├── .env ├── README.md ├── docker-compose.yml └── traefik.yml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | --- 3 | github: geerlingguy 4 | patreon: geerlingguy 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: required 3 | 4 | env: 5 | global: 6 | - DOCKER_COMPOSE_VERSION: 1.14.0 7 | matrix: 8 | - TEST_DIR: "flask" 9 | - TEST_DIR: "php" 10 | # - TEST_DIR: "symfony" 11 | - TEST_DIR: "traefik" 12 | 13 | services: 14 | - docker 15 | 16 | before_install: 17 | # Upgrade Docker Compose. 18 | - sudo rm /usr/local/bin/docker-compose 19 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 20 | - chmod +x docker-compose 21 | - sudo mv docker-compose /usr/local/bin 22 | # Upgrade Docker. 23 | - sudo apt-get update 24 | - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce 25 | 26 | before_script: 27 | - cd $TEST_DIR 28 | 29 | script: 30 | - ls 31 | 32 | # Extra Docker Compose file for Traefik. 33 | - | 34 | if [ "${TEST_DIR}" == "traefik" ]; then 35 | docker-compose -f traefik.yml up -d 36 | fi 37 | 38 | - docker-compose up -d 39 | - docker-compose stop 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeff Geerling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Examples - by geerlingguy 2 | 3 | [![Build Status](https://travis-ci.org/geerlingguy/docker-examples.svg?branch=master)](https://travis-ci.org/geerlingguy/docker-examples) 4 | 5 | The web is full of Docker examples and tutorials and repos. 6 | 7 | There are many like it, but this one is mine. 8 | 9 | ## Philosophy 10 | 11 | I like learning from first principles. Docker masks a surprising amount of complexity, and most tutorials try to gloss over them to show 'the cool shiny things' before you can even understand what's going on. 12 | 13 | I'd rather start really simple, and build from there until I fully understand what's going on. Therefore the examples in this repo build on each other until we get to some actual 'this could do something useful' kinds of infrastructure. 14 | 15 | ## Installation 16 | 17 | 1. Install [Docker for Mac](https://www.docker.com/products/docker#/mac). 18 | 2. Start Docker.app 19 | 3. Open Terminal, make sure it's running with `docker --version`. 20 | 21 | ## Getting Started 22 | 23 | ### First Docker command 24 | 25 | To kick the tires and make sure things are working, run: 26 | 27 | docker run hello-world 28 | 29 | This command is doing the following: 30 | 31 | - [`docker`](https://docs.docker.com/engine/reference/commandline/cli/) - The main Docker command. 32 | - [`run`](https://docs.docker.com/engine/reference/run/) - Run a container. 33 | - `hello-world` - The name of the Docker Hub repository to pull from. In this case, we'll get the latest [`hello-world`](https://hub.docker.com/_/hello-world/) Docker image. If you don't specify a version, this is interpreted as `hello-world:latest`. 34 | 35 | If things are working correctly, you should see some output, then the container will exit. 36 | 37 | ### First real-world example - Simple Nginx Webserver 38 | 39 | Docker's [tutorial](https://docs.docker.com/docker-for-mac/) provides a simple example of running an Nginx webserver on localhost with the command: 40 | 41 | docker run -d -p 80:80 --name webserver nginx 42 | 43 | This command is doing the following: 44 | 45 | - [`docker`](https://docs.docker.com/engine/reference/commandline/cli/) - The main Docker command. 46 | - [`run`](https://docs.docker.com/engine/reference/run/) - Run a container. 47 | - [`-d`](https://docs.docker.com/engine/reference/run/#/detached-d) - Run a container detached; when the process (nginx, in this case) exits, the container will exit. 48 | - [`-p 80:80`](https://docs.docker.com/engine/reference/commandline/run/#/publish-or-expose-port-p-expose) - Publish or expose a port (`[host-port]:[container-port]`, so in this case bind the container's port `80` to the host's port `80`). 49 | - [`--name webserver`](https://docs.docker.com/engine/reference/commandline/run/#/assign-name-and-allocate-pseudo-tty-name-it) - Assign a name to a container. 50 | - `nginx` - The name of the Docker Hub repository to pull from. In this case, we'll get the latest [`nginx`](https://hub.docker.com/_/nginx/) Docker image. If you don't specify a version, this is interpreted as `nginx:latest`. 51 | 52 | The first time you run this command, it will download the Nginx Docker image (it actually downloads a few 'layers' which build up the official image), then run a container based on the image. 53 | 54 | Run the command, then access `http://localhost:80/` in a web browser. You should see the 'Welcome to nginx!' page. 55 | 56 | #### Playing with the Nginx container 57 | 58 | - Run `docker ps` to see a list of running containers; you should see the Nginx container you just started in the list. 59 | - Run `docker stop webserver` to stop the named container (you can also use the 'container ID' if you want). 60 | - Run `docker ps -a` to see a list of all containers on the system, including stopped containers. 61 | - Run `docker start webserver` to start the named container again. 62 | - Run `docker rm webserver` to delete the container entirely (you can also pass `--rm` to the `run` command if you want the container deleted after it exits). 63 | 64 | > Note: Starting and stopping a container is usually quicker than building it from scratch with `docker run`, so if possible, it's best to generate the container with `run` once and use `start`/`stop` until you need to rebuild the container. 65 | 66 | ### Second real-world example - Simple Python Flask App 67 | 68 | Docker has another [tutorial](https://docs.docker.com/engine/tutorials/usingdocker/) that digs a little deeper into Docker CLI usage, but for our purposes, we'll just run the main command, and this time allow Docker to map an ephemeral port (any available high port number on our host) to the port configured in the container's configuration: 69 | 70 | docker run -d -P training/webapp python app.py 71 | 72 | Besides the obvious, this command is doing a couple new things: 73 | 74 | - [`-P`](https://docs.docker.com/engine/reference/run/#/expose-incoming-ports) - Publish all ports that are `EXPOSE`d by the docker container to ephemeral ports on the host (unlike `-p`, which requires specification of each port mapping). 75 | - `training/webapp` - The name of the Docker Hub repository to pull from. In this case we'll get the latest [`training/webapp`](https://hub.docker.com/r/training/webapp/) Docker image. 76 | - `python app.py` - This is the command that will be run inside the container when it's launched. Until the `app.py` exits, or you `docker stop` or `docker kill` the container, it will keep running. 77 | 78 | Once the container is started, run `docker ps` to see what host port the container is bound to, then visit that port in your browser, e.g. `http://localhost:32768/`. You should see the text "Hello world!" in your browser. 79 | 80 | Since we didn't specify a `--name` when we ran this `docker run` command, Docker assigned a random name to the container (in my case, `romantic_bell`), so to `stop`, `rm`, or otherwise interact with the container, you have to use the generated name or the container ID. 81 | 82 | ### Other Essential commands 83 | 84 | At this point, you should be somewhat familiar with the main Docker CLI. Some other commands that come in handy at this point are: 85 | 86 | - `docker images`: Show a list of all images you have downloaded locally. 87 | - `docker rmi [image-name]`: Remove a particular image (save some disk space!). 88 | - `docker logs [container-name]`: Tail the logs (stdout) of a container (try this on the Flask app while refreshing the page!). 89 | 90 | ## Diving Deeper 91 | 92 | Other examples warrant their own dedicated directories, with example code and individual detailed README's explaining how they work. Included examples: 93 | 94 | - [`/flask`](/flask) - Python Flask and MySQL. 95 | - Introduces `docker-compose`. 96 | - [`/php`](/php) - PHP-FPM and Nginx. 97 | - Introduces extra package installation. 98 | - Introduces `HEALTHCHECK`. 99 | - [`/symfony`](/symfony) - Symfony and SQLite. 100 | - TODO. 101 | - [`/traefik`](/traefik) - Traefik proxy. 102 | - Introduces proxying of traffic for multiple hostnames on one port. 103 | - Introduces the `.env` file. 104 | 105 | ## License 106 | 107 | This project is licensed under the MIT open source license. 108 | 109 | ## About the Author 110 | 111 | [Jeff Geerling](https://www.jeffgeerling.com/) is the author of [Ansible for DevOps](https://www.ansiblefordevops.com/) and manages tons of infrastructure, as well as open source projects like [Drupal VM](https://www.drupalvm.com/). 112 | -------------------------------------------------------------------------------- /flask/README.md: -------------------------------------------------------------------------------- 1 | # Flask App with MySQL Database 2 | 3 | ## Quick Start 4 | 5 | 1. Run `docker-compose up` 6 | 2. Visit `http://localhost/` in your browser. 7 | 8 | ## Description 9 | 10 | [Flask](http://flask.pocoo.org/) is a web development microframework for Python. It's efficient, easy-to-use, and easy-to-deploy! 11 | 12 | I often use Flask when building small webservices or dynamic websites and feel like trying something fresh and different from PHP, my daily driver. Most webservices and sites require some sort of database to store data, so we need to create not one but _two_ Docker containers, and link them together: 13 | 14 | 1. Flask container (with Python). 15 | 2. MySQL container 16 | 17 | In early examples, we would build or import just one container and work with it to run some code. In real-world usage, you'll often need two, three, or even _dozens_ of containers to support the project you're working on! 18 | 19 | It would get quite unweildy to have to manage all of these containers with separate `run` and `stop` commands, so Docker has an answer: `docker-compose`. 20 | 21 | ## Docker Compose 22 | 23 | Notes: 24 | 25 | - [`docker-compose`](https://docs.docker.com/compose/reference/) 26 | - [`up`](https://docs.docker.com/compose/reference/up/) 27 | 28 | ## Persisting Data 29 | 30 | One thing repeated early and often in the world of Docker is _data stored inside a container is not persisted_. So... this creates quite the conundrum: what if you need data to persist after a container exits? 31 | 32 | Traditionally, Docker allowed the use of 'data containers' which would contain a `VOLUME` and nothing else, based on a really tiny base image, then you use `volumes_from` or `-v` to link the volume at the specified path into a container. 33 | 34 | In our case, we want to explicitly state that we want the path `/var/lib/mysql` to persist as a volume separate from the general `db` volume. This way if you build the Database container, store some data inside a MySQL database that's stored within that path, then exit, you can run the Database container again later, and the data you stored earlier won't have vanished! 35 | 36 | We did this inside the `docker-compose.yml` file here: 37 | 38 | services: 39 | ... 40 | db: 41 | ... 42 | volumes: 43 | - /var/lib/mysql 44 | 45 | The official MySQL image adds it's own `VOLUME` directive in its Dockerfile, so it's not necessary for us to specify the volume in our own `docker-compose` configuration... but I like to be explicit about things like _where my important data is stored_! 46 | 47 | After you bring up the containers, you can inspect the `db` container to ensure the volume is configured correctly: `docker inspect flask_db_1` — this displays all the container info, including a list of 'Volumes'. 48 | 49 | You can also mount a directory on the host as a vlume using the syntax `[host-path]:[container-path]`, so if you want to mount a `database` directory in your project folder as `/var/lib/mysql`, change the the volume like so: 50 | 51 | services: 52 | ... 53 | db: 54 | ... 55 | volumes: 56 | - ./database:/var/lib/mysql 57 | 58 | If you change the volume settings, then you will need to `stop` any running containers, then `rm` them and rebuild before the changes take effect (remember, containers are immutable!). 59 | 60 | Read more: [Manage data in containers](https://docs.docker.com/engine/tutorials/dockervolumes/) 61 | 62 | > Note for Mac/Windows users: If you encounter performance issues with your app running inside Docker containers, and your app has many (e.g. 1,000+) files, it could be related to some filesystem performance issues with current versions of Docker. See [File access in mounted volumes extremely slow](https://forums.docker.com/t/file-access-in-mounted-volumes-extremely-slow-cpu-bound/8076/107). 63 | > 64 | > This is a hard problem to solve, though the situation should improve as time goes on. For now, find ways to use volumes that aren't shared to your host whenever possible. You can also look into using tools like [docker-sync](https://docker-sync.io/) if you _must_ sync large numbers of files. 65 | -------------------------------------------------------------------------------- /flask/db/Dockerfile: -------------------------------------------------------------------------------- 1 | # A MySQL container. 2 | FROM mysql:5.7 3 | LABEL maintainer="Jeff Geerling" 4 | 5 | EXPOSE 3306 6 | CMD ["mysqld"] 7 | -------------------------------------------------------------------------------- /flask/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | www: 5 | build: www/. 6 | ports: 7 | - "80:80" 8 | links: 9 | - db 10 | depends_on: 11 | - db 12 | 13 | db: 14 | build: db/. 15 | volumes: 16 | - /var/lib/mysql 17 | environment: 18 | MYSQL_ROOT_PASSWORD: supersecure 19 | -------------------------------------------------------------------------------- /flask/www/Dockerfile: -------------------------------------------------------------------------------- 1 | # A simple Flask app container. 2 | FROM python:3-buster 3 | LABEL maintainer="Jeff Geerling" 4 | 5 | # Place app in container. 6 | COPY . /opt/www 7 | WORKDIR /opt/www 8 | 9 | # Install dependencies. 10 | RUN pip install -r requirements.txt 11 | 12 | EXPOSE 80 13 | CMD python index.py 14 | -------------------------------------------------------------------------------- /flask/www/index.py: -------------------------------------------------------------------------------- 1 | # Infrastructure test page. 2 | import os 3 | from flask import Flask 4 | from flask import Markup 5 | from flask import render_template 6 | from flask_sqlalchemy import SQLAlchemy 7 | from sqlalchemy.sql import text 8 | 9 | app = Flask(__name__) 10 | 11 | # Configure MySQL connection. 12 | db = SQLAlchemy() 13 | db_uri = 'mysql://root:supersecure@db/information_schema' 14 | app.config['SQLALCHEMY_DATABASE_URI'] = db_uri 15 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 16 | db.init_app(app) 17 | 18 | @app.route("/") 19 | def test(): 20 | mysql_result = False 21 | query_string = text("SELECT 1") 22 | # TODO REMOVE FOLLOWING LINE AFTER TESTING COMPLETE. 23 | db.session.query("1").from_statement(query_string).all() 24 | try: 25 | if db.session.query("1").from_statement(query_string).all(): 26 | mysql_result = True 27 | except: 28 | pass 29 | 30 | if mysql_result: 31 | result = Markup('PASS') 32 | else: 33 | result = Markup('FAIL') 34 | 35 | # Return the page with the result. 36 | return render_template('index.html', result=result) 37 | 38 | if __name__ == "__main__": 39 | app.run(host="0.0.0.0", port=80) 40 | -------------------------------------------------------------------------------- /flask/www/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-sqlalchemy 3 | mysqlclient 4 | -------------------------------------------------------------------------------- /flask/www/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flask + MySQL Docker Example 5 | 6 | 7 | 8 |

Flask + MySQL Docker Example

9 |

MySQL Connection: {{ result }}

10 | 11 | 12 | -------------------------------------------------------------------------------- /php/README.md: -------------------------------------------------------------------------------- 1 | # PHP-FPM with Nginx 2 | 3 | ## Quick Start 4 | 5 | 1. Run `docker-compose up` 6 | 2. Visit `http://localhost/` in your browser. 7 | 8 | ## Description 9 | 10 | [PHP](http://php.net/) is a popular programming language for web projects. It is usually run behind a webserver (like Apache or Nginx) to serve web traffic, API requests, etc. 11 | 12 | This example is straightforward, running PHP-FPM behind a plain Nginx container, and Nginx routes requests to a PHP script that prints the environment information on a webpage. To set this up, we will run three containers: 13 | 14 | 1. Source code container (a data container to mount the source code we're running). 15 | 2. PHP container 16 | 2. Nginx container 17 | 18 | We'll use `docker-compose` to orchestrate multiple containers on our host; see notes elsewhere in this repository for more information about `docker-compose`. There are a couple other unique things being done in the Nginx container's `Dockerfile` which are detailed below. 19 | 20 | ## Installing Packages 21 | 22 | In order to run a health check (see the next section) to determine if Nginx is running properly, we need to install curl, a command line utility to make HTTP requests, inside the container. 23 | 24 | Because the upstream [`nginx:latest`](https://github.com/nginxinc/docker-nginx/blob/master/mainline/jessie/Dockerfile) container uses Debian Jessie as a base image, we can easily install `curl` via `apt-get`. But to keep our Docker image more compact and efficient, we also need to clean up apt resources after the installation. 25 | 26 | You might think to do this in a few steps, e.g.: 27 | 28 | RUN apt-get update 29 | RUN apt-get install -y curl 30 | RUN rm -rf /var/lib/apt/lists/* && rm -Rf /usr/share/doc && rm -Rf /usr/share/man 31 | RUN apt-get clean 32 | 33 | However, if you do it this way, Docker will save not one but _four_ extra image layers, taking up more space on your host, and slightly more time to build. Instead, it's recommended to do operations like installing new software in one giant `RUN` command, e.g.: 34 | 35 | RUN apt-get update \ 36 | && apt-get install -y curl \ 37 | && rm -rf /var/lib/apt/lists/* \ 38 | && rm -Rf /usr/share/doc && rm -Rf /usr/share/man \ 39 | && apt-get clean 40 | 41 | This way, when we build the Docker image via `docker-compose`, the `curl` installation will only add one layer, which is more efficient. 42 | 43 | ## Health Check 44 | 45 | One feature that can be useful in certain situations is Docker's built-in `HEALTHCHECK` functionality. In the `nginx` container's `Dockerfile`, there's a line that provides a command Docker can use to check whether the container is healthy: 46 | 47 | HEALTHCHECK CMD curl --fail http://localhost/ || exit 1 48 | 49 | Since we installed `curl` previously, we can use it to fetch `localhost`. If the request fails, `curl` will exit with an error, and Docker can use this to indicate whether the container is `healthy` or `unhealthy` while it's running. 50 | 51 | You can check the status (after `docker-compose up`) using `docker ps`, or individually for this container with `docker inspect [container_id]`. Docker will provide one of the following statuses: 52 | 53 | - `starting` 54 | - `healthy` 55 | - `unhealthy` 56 | 57 | Note that if you want to disable a base image's `HEALTHCHECK`, you can add a line `HEALTHCHECK NONE` in your Dockerfile. There are also more options available to control the `HEALTHCHECK` interval, timeout, and retries. Read [more about `HEALTHCHECK` in Docker's documentation](https://docs.docker.com/engine/reference/builder/#/healthcheck). 58 | -------------------------------------------------------------------------------- /php/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | 4 | php: 5 | image: php:fpm 6 | volumes_from: 7 | - data 8 | 9 | nginx: 10 | build: nginx/. 11 | ports: 12 | - "80:80" 13 | links: 14 | - php 15 | volumes_from: 16 | - data 17 | 18 | data: 19 | image: busybox:latest 20 | volumes: 21 | - ./source:/var/www/html -------------------------------------------------------------------------------- /php/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | ADD default.conf /etc/nginx/conf.d 4 | 5 | # Install curl. 6 | RUN apt-get update \ 7 | && apt-get install -y curl \ 8 | && rm -rf /var/lib/apt/lists/* \ 9 | && rm -Rf /usr/share/doc && rm -Rf /usr/share/man \ 10 | && apt-get clean 11 | 12 | # Add healthcheck. 13 | HEALTHCHECK CMD curl --fail http://localhost/ || exit 1 14 | -------------------------------------------------------------------------------- /php/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 0.0.0.0:80; 3 | 4 | root /var/www/html; 5 | 6 | location / { 7 | index index.php index.html; 8 | } 9 | 10 | location ~ \.php$ { 11 | include fastcgi_params; 12 | fastcgi_pass php:9000; 13 | fastcgi_index index.php; 14 | fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /php/source/index.php: -------------------------------------------------------------------------------- 1 |