.. index:: pair: Web ; Apps pair: Dockerfile ; Exmple Flask .. _labs_webapps: ========================================== 2) Webapps with Docker (Python + Flask) ========================================== .. seealso:: - https://github.com/docker/labs/blob/master/beginner/chapters/webapps.md - https://github.com/docker/labs/tree/master/beginner/static-site - https://hub.docker.com/r/dockersamples/static-site/ .. contents:: :depth: 5 Introduction ============= Great! So you have now looked at docker run, played with a Docker container and also got the hang of some terminology. Armed with all this knowledge, you are now ready to get to the real stuff, deploying web applications with Docker. Run a static website in a container : docker run -d dockersamples/static-site ================================================================================= .. note:: Code for this section is in this repo in the `static-site directory`_. Let's start by taking baby-steps. First, we'll use Docker to run a static website in a container. The website is based on an existing image. We'll pull a Docker image from Docker Store, run the container, and see how easy it is to set up a web server. The image that you are going to use is a single-page website that was already created for this demo and is available on the Docker Store as dockersamples/static-site. You can download and run the image directly in one go using docker run as follows:: docker run -d dockersamples/static-site .. _`static-site directory`: https://github.com/docker/labs/tree/master/beginner/static-site :: C:\Tmp>docker run -d dockersamples/static-site :: Unable to find image 'dockersamples/static-site:latest' locally latest: Pulling from dockersamples/static-site fdd5d7827f33: Pull complete a3ed95caeb02: Pull complete 716f7a5f3082: Pull complete 7b10f03a0309: Pull complete aff3ab7e9c39: Pull complete Digest: sha256:daa686c61d7d239b7977e72157997489db49f316b9b9af3909d9f10fd28b2dec Status: Downloaded newer image for dockersamples/static-site:latest 3bf76a82d6127dfd775f0eb6a5ed20ce275ad7eaf02b18b2ce50bd96df1432ba docker images ============== :: docker images :: REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu trusty 02a63d8b2bfa 17 hours ago 222MB id3pvergain/get-started part2 ed5b70620e49 31 hours ago 148MB friendlyhello latest ed5b70620e49 31 hours ago 148MB alpine latest 3fd9065eaf02 6 days ago 4.15MB wordpress latest 28084cde273b 7 days ago 408MB centos latest ff426288ea90 7 days ago 207MB nginx latest 3f8a4339aadd 2 weeks ago 108MB ubuntu latest 00fd29ccc6f1 4 weeks ago 111MB python 2.7-slim 4fd30fc83117 5 weeks ago 138MB hello-world latest f2a91732366c 8 weeks ago 1.85kB docker4w/nsenter-dockerd latest cae870735e91 2 months ago 187kB dockersamples/static-site latest f589ccde7957 22 months ago 191MB docker run --name static-site -e AUTHOR="patrick.vergain" -d -P dockersamples/static-site =========================================================================================== :: C:\Tmp>docker run --name static-site -e AUTHOR="patrick.vergain" -d -P dockersamples/static-site :: 554e21d4b723a49e4b2019497d4411d955de2175e8b216a126d3a0c214ca9458 In the above command: - -d will create a container with the process detached from our terminal - -P will publish all the exposed container ports to random ports on the Docker host - -e is how you pass environment variables to the container - --name allows you to specify a container name - AUTHOR is the environment variable name and Your Name is the value that you can pass docker port static-site ======================== :: docker port static-site :: 443/tcp -> 0.0.0.0:32768 80/tcp -> 0.0.0.0:32769 If you are running Docker for Mac, Docker for Windows, or Docker on Linux, you can open http://localhost:[YOUR_PORT_FOR 80/tcp]. For our example this is http://localhost:32769/ .. figure:: nginx_port_32769.png :align: center http://localhost:32769/ docker run --name static-site-2 -e AUTHOR="patrick.vergain" -d -p 8888:80 dockersamples/static-site ====================================================================================================== :: C:\Tmp>docker run --name static-site-2 -e AUTHOR="patrick.vergain" -d -p 8888:80 dockersamples/static-site :: 839649f1be575ec442f9fe94d6957b0f218b63af3dfaa8df989f413e86896d16 .. figure:: nginx_port_8888.png :align: center http://localhost:8888/ To deploy this on a real server you would just need to install Docker, and run the above docker command(as in this case you can see the AUTHOR is Docker which we passed as an environment variable). Now that you've seen how to run a webserver inside a Docker container, **how do you create your own Docker image ?** This is the question we'll explore in the next section. But first, let's stop and remove the containers since you won't be using them anymore. docker stop static-site ========================= :: docker stop static-site :: static-site docker rm static-site ====================== :: docker rm static-site :: static-site Let's use a shortcut to remove the second site: docker rm -f static-site-2 ============================================================================= :: docker rm -f static-site-2 :: static-site-2 Docker Images ================ .. seealso:: - http://linuxfr.org/news/sortie-d-ubuntu-16-04-lts-xenial-xerus In this section, let's dive deeper into what Docker images are. You will build your own image, use that image to run an application locally, and finally, push some of your own images to Docker Cloud. **Docker images are the basis of containers**. In the previous example, you pulled the dockersamples/static-site image from the registry and asked the Docker client to run a container based on that image. To see the list of images that are available locally on your system, run the docker images command. :: C:\Tmp>docker images :: REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu trusty 02a63d8b2bfa 18 hours ago 222MB id3pvergain/get-started part2 ed5b70620e49 32 hours ago 148MB friendlyhello latest ed5b70620e49 32 hours ago 148MB alpine latest 3fd9065eaf02 6 days ago 4.15MB wordpress latest 28084cde273b 7 days ago 408MB centos latest ff426288ea90 7 days ago 207MB nginx latest 3f8a4339aadd 2 weeks ago 108MB ubuntu latest 00fd29ccc6f1 4 weeks ago 111MB python 2.7-slim 4fd30fc83117 5 weeks ago 138MB hello-world latest f2a91732366c 8 weeks ago 1.85kB docker4w/nsenter-dockerd latest cae870735e91 2 months ago 187kB dockersamples/static-site latest f589ccde7957 22 months ago 191MB Above is a list of images that I've pulled from the registry and those I've created myself (we'll shortly see how). You will have a different list of images on your machine. The TAG refers to a particular snapshot of the image and the ID is the corresponding unique identifier for that image. For simplicity, you can think of an image akin to a git repository - images can be committed with changes and have multiple versions. When you do not provide a specific version number, the client defaults to latest. For example you could pull a specific version of ubuntu image as follows: docker pull ubuntu:16.04 ========================= :: docker pull ubuntu:16.04 :: 16.04: Pulling from library/ubuntu 8f7c85c2269a: Pull complete 9e72e494a6dd: Pull complete 3009ec50c887: Pull complete 9d5ffccbec91: Pull complete e872a2642ce1: Pull complete Digest: sha256:d3fdf5b1f8e8a155c17d5786280af1f5a04c10e95145a515279cf17abdf0191f Status: Downloaded newer image for ubuntu:16.04 If you do not specify the version number of the image then, as mentioned, the Docker client will default to a version named latest. So for example, the docker pull command given below will pull an image named ubuntu:latest: :: docker pull ubuntu To get a new Docker image you can either get it from a registry (such as the Docker Store) or create your own. There are hundreds of thousands of images available on Docker Store. You can also search for images directly from the command line using docker search. An important distinction with regard to images is between base images and child images. - Base images are images that have no parent images, usually images with an OS like ubuntu, alpine or debian. - Child images are images that build on base images and add additional functionality. Another key concept is the idea of official images and user images. (Both of which can be base images or child images.) Official images are Docker sanctioned images. Docker, Inc. sponsors a dedicated team that is responsible for reviewing and publishing all Official Repositories content. This team works in collaboration with upstream software maintainers, security experts, and the broader Docker community. These are not prefixed by an organization or user name. In the list of images above, the python, node, alpine and nginx images are official (base) images. To find out more about them, check out the Official Images Documentation. User images are images created and shared by users like you. They build on base images and add additional functionality. Typically these are formatted as user/image-name. The user value in the image name is your Docker Store user or organization name. Create your first image ========================= .. note:: The code for this section is in this repository in the flask-app directory. Now that you have a better understanding of images, it's time to create your own. Our goal here is to create an image that sandboxes a small Flask application. **The goal of this exercise is to create a Docker image which will run a Flask app**. We'll do this by first pulling together the components for a random cat picture generator built with Python Flask, then dockerizing it by writing a Dockerfile. Finally, we'll build the image, and then run it. - Create a Python Flask app that displays random cat pix - Write a Dockerfile - Build the image - Run your image - Dockerfile commands summary .. https://github.com/docker/labs/tree/master/beginner/flask-app Create a Python Flask app that displays random cat pix ======================================================== For the purposes of this workshop, we've created a fun little Python Flask app that displays a random cat .gif every time it is loaded because, you know, who doesn't like cats ? Start by creating a directory called flask-app where we'll create the following files: - app.py - requirements.txt - templates/index.html - Dockerfile Make sure to cd flask-app before you start creating the files, because you don't want to start adding a whole bunch of other random files to your image. app.py ------- Create the app.py with the following content. .. literalinclude:: app_flask/app.py :linenos: requirements.txt ------------------- In order to install the Python modules required for our app, we need to create a file called requirements.txt and add the following line to that file .. literalinclude:: app_flask/requirements.txt :linenos: templates/index.html ----------------------- Create a directory called templates and create an index.html file in that directory with the following content in it. .. literalinclude:: app_flask/templates/index.html :linenos: :language: html Write a Dockerfile -------------------- .. seealso:: - https://docs.docker.com/engine/reference/builder/ We want to create a Docker image with this web app. As mentioned above, all user images are based on a base image. Since our application is written in Python, we will build our own Python image based on :ref:`Alpine `. We'll do that using a Dockerfile. A Dockerfile is a text file that contains a list of commands that the Docker daemon calls while creating an image. The Dockerfile contains all the information that Docker needs to know to run the app, a base Docker image to run from, location of your project code, any dependencies it has, and what commands to run at start-up. It is a simple way to automate the image creation process. The best part is that the commands you write in a Dockerfile are almost identical to their equivalent Linux commands. This means you don't really have to learn new syntax to create your own Dockerfiles. FROM alpine:3.5 +++++++++++++++++ We'll start by specifying our base image, using the FROM keyword:: FROM alpine:3.5 RUN apk add --update py2-pip ++++++++++++++++++++++++++++++ The next step usually is to write the commands of copying the files and installing the dependencies. But first we will install the Python pip package to the alpine linux distribution. This will not just install the pip package but any other dependencies too, which includes the python interpreter. Add the following RUN command next:: RUN apk add --update py2-pip Let's add the files that make up the Flask Application. COPY requirements.txt /usr/src/app/ ++++++++++++++++++++++++++++++++++++++ Install all Python requirements for our app to run. This will be accomplished by adding the lines: :: COPY requirements.txt /usr/src/app/ RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt COPY app.py /usr/src/app/ ++++++++++++++++++++++++++++ Copy the files you have created earlier into our image by using COPY command. :: COPY app.py /usr/src/app/ COPY templates/index.html /usr/src/app/templates/ EXPOSE 5000 +++++++++++++ Specify the port number which needs to be exposed. Since our flask app is running on 5000 that's what we'll expose. :: EXPOSE 5000 CMD ["python", "/usr/src/app/app.py"] ++++++++++++++++++++++++++++++++++++++++ The last step is the command for running the application which is simply python ./app.py. Use the CMD command to do that: :: CMD ["python", "/usr/src/app/app.py"] The primary purpose of CMD is to tell the container which command it should run by default when it is started. Verify your Dockerfile. Our Dockerfile is now ready. This is how it looks: .. literalinclude:: app_flask/Dockerfile :linenos: Build the image (docker build -t id3pvergain/myfirstapp) ---------------------------------------------------------- Now that you have your Dockerfile, you can build your image. The docker build command does the heavy-lifting of creating a docker image from a Dockerfile. When you run the docker build command given below, make sure to replace with your username. This username should be the same one you created when registering on Docker Cloud. If you haven't done that yet, please go ahead and create an account. The docker build command is quite simple - it takes an optional tag name with the -t flag, and the location of the directory containing the Dockerfile - the . indicates the current directory: :: docker build -t id3pvergain/myfirstapp . :: Y:\projects_id3\P5N001\XLOGCA135_tutorial_docker\tutorial_docker\samples\labs\webapps\app_flask>docker build -t id3pvergain/myfirstapp . :: Sending build context to Docker daemon 7.68kB Step 1/8 : FROM alpine:3.5 3.5: Pulling from library/alpine 550fe1bea624: Pull complete Digest: sha256:9148d069e50eee519ec45e5683e56a1c217b61a52ed90eb77bdce674cc212f1e Status: Downloaded newer image for alpine:3.5 ---> 6c6084ed97e5 Step 2/8 : RUN apk add --update py2-pip ---> Running in 1fe5bd53d58d fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/main/x86_64/APKINDEX.tar.gz fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/community/x86_64/APKINDEX.tar.gz (1/12) Installing libbz2 (1.0.6-r5) (2/12) Installing expat (2.2.0-r1) (3/12) Installing libffi (3.2.1-r2) (4/12) Installing gdbm (1.12-r0) (5/12) Installing ncurses-terminfo-base (6.0_p20170701-r0) (6/12) Installing ncurses-terminfo (6.0_p20170701-r0) (7/12) Installing ncurses-libs (6.0_p20170701-r0) (8/12) Installing readline (6.3.008-r4) (9/12) Installing sqlite-libs (3.15.2-r1) (10/12) Installing python2 (2.7.13-r0) (11/12) Installing py-setuptools (29.0.1-r0) (12/12) Installing py2-pip (9.0.0-r1) Executing busybox-1.25.1-r1.trigger OK: 61 MiB in 23 packages Removing intermediate container 1fe5bd53d58d ---> 23504d4e2c59 Step 3/8 : COPY requirements.txt /usr/src/app/ ---> 1be30128b66f Step 4/8 : RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt ---> Running in a5f6ada2483d Collecting Flask==0.10.1 (from -r /usr/src/app/requirements.txt (line 1)) Downloading Flask-0.10.1.tar.gz (544kB) Collecting Werkzeug>=0.7 (from Flask==0.10.1->-r /usr/src/app/requirements.txt (line 1)) Downloading Werkzeug-0.14.1-py2.py3-none-any.whl (322kB) Collecting Jinja2>=2.4 (from Flask==0.10.1->-r /usr/src/app/requirements.txt (line 1)) Downloading Jinja2-2.10-py2.py3-none-any.whl (126kB) Collecting itsdangerous>=0.21 (from Flask==0.10.1->-r /usr/src/app/requirements.txt (line 1)) Downloading itsdangerous-0.24.tar.gz (46kB) Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->Flask==0.10.1->-r /usr/src/app/requirements.txt (line 1)) Downloading MarkupSafe-1.0.tar.gz Installing collected packages: Werkzeug, MarkupSafe, Jinja2, itsdangerous, Flask Running setup.py install for MarkupSafe: started Running setup.py install for MarkupSafe: finished with status 'done' Running setup.py install for itsdangerous: started Running setup.py install for itsdangerous: finished with status 'done' Running setup.py install for Flask: started Running setup.py install for Flask: finished with status 'done' Successfully installed Flask-0.10.1 Jinja2-2.10 MarkupSafe-1.0 Werkzeug-0.14.1 itsdangerous-0.24 You are using pip version 9.0.0, however version 9.0.1 is available. You should consider upgrading via the 'pip install --upgrade pip' command. Removing intermediate container a5f6ada2483d ---> 68467d64c546 Step 5/8 : COPY app.py /usr/src/app/ ---> 62a6a857c6cd Step 6/8 : COPY templates/index.html /usr/src/app/templates/ ---> 639c61ea4a4b Step 7/8 : EXPOSE 5000 ---> Running in c15c0178577c Removing intermediate container c15c0178577c ---> f6d0fdcd6c29 Step 8/8 : CMD ["python", "/usr/src/app/app.py"] ---> Running in 222f91658593 Removing intermediate container 222f91658593 ---> 0ce3c7641c9a Successfully built 0ce3c7641c9a Successfully tagged id3pvergain/myfirstapp:latest SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories. If you don't have the alpine:3.5 image, the client will first pull the image and then create your image. Therefore, your output on running the command will look different from mine. If everything went well, your image should be ready! docker images -------------- Run docker images and see if your image (/myfirstapp) shows. Y:\projects_id3\P5N001\XLOGCA135_tutorial_docker\tutorial_docker\samples\labs\webapps\app_flask>docker images :: REPOSITORY TAG IMAGE ID CREATED SIZE id3pvergain/myfirstapp latest 0ce3c7641c9a 2 minutes ago 56.4MB ubuntu 16.04 2a4cca5ac898 38 hours ago 111MB ubuntu trusty 02a63d8b2bfa 38 hours ago 222MB friendlyhello latest ed5b70620e49 2 days ago 148MB id3pvergain/get-started part2 ed5b70620e49 2 days ago 148MB alpine 3.5 6c6084ed97e5 7 days ago 3.99MB alpine latest 3fd9065eaf02 7 days ago 4.15MB wordpress latest 28084cde273b 8 days ago 408MB centos latest ff426288ea90 8 days ago 207MB nginx latest 3f8a4339aadd 3 weeks ago 108MB ubuntu latest 00fd29ccc6f1 4 weeks ago 111MB python 2.7-slim 4fd30fc83117 5 weeks ago 138MB hello-world latest f2a91732366c 8 weeks ago 1.85kB docker4w/nsenter-dockerd latest cae870735e91 2 months ago 187kB dockersamples/static-site latest f589ccde7957 22 months ago 191MB Run your image (docker run -p 8888:5000 --name myfirstapp id3pvergain/myfirstapp) ------------------------------------------------------------------------------------ The next step in this section is to run the image and see if it actually works. :: docker run -p 8888:5000 --name myfirstapp id3pvergain/myfirstapp * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) Head over to http://localhost:8888 and your app should be live. .. note:: If you are using Docker Machine, you may need to open up another terminal and determine the container ip address using docker-machine ip default. .. figure:: docker_http_8888.png :align: center http://localhost:8888/ Hit the Refresh button in the web browser to see a few more cat images. Push your image (docker push id3pvergain/myfirstapp) ------------------------------------------------------ Now that you've created and tested your image, you can push it to Docker Cloud. First you have to login to your Docker Cloud account, to do that: docker login ++++++++++++++ :: Y:\projects_id3\P5N001\XLOGCA135_tutorial_docker\tutorial_docker\samples\labs\webapps\app_flask>docker login :: Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username (id3pvergain): Password: Login Succeeded docker push id3pvergain/myfirstapp ++++++++++++++++++++++++++++++++++++ :: Y:\projects_id3\P5N001\XLOGCA135_tutorial_docker\tutorial_docker\samples\labs\webapps\app_flask>docker push id3pvergain/myfirstapp :: The push refers to repository [docker.io/id3pvergain/myfirstapp] b7591dd05809: Pushed cd36128c70d4: Pushed cea459424f6e: Pushed 6ac80674ef6a: Pushed de7b45529bcb: Pushed d39d92664027: Mounted from library/alpine latest: digest: sha256:8f945ed63e2dc3ef3fa178fe4dded5a68eae07c5c9e854ec278c7cfa2c6bc6bb size: 1572 docker rm -f myfirstapp ------------------------- Now that you are done with this container, stop and remove it since you won't be using it again. Open another terminal window and execute the following commands: :: docker stop myfirstapp docker rm myfirstapp or:: docker rm -f myfirstapp :: myfirstapp docker ps ---------- :: Y:\projects_id3\P5N001\XLOGCA135_tutorial_docker\tutorial_docker\samples\labs\webapps\app_flask>docker ps :: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES Dockerfile commands summary ============================= Here's a quick summary of the few basic commands we used in our Dockerfile. FROM ----- FROM starts the Dockerfile. It is a requirement that the Dockerfile must start with the FROM command. Images are created in layers, which means you can use another image as the base image for your own. The FROM command defines your base layer. As arguments, it takes the name of the image. Optionally, you can add the Docker Cloud username of the maintainer and image version, in the format username/imagename:version. RUN ---- RUN is used to build up the Image you're creating. For each RUN command, Docker will run the command then create a new layer of the image. This way you can roll back your image to previous states easily. The syntax for a RUN instruction is to place the full text of the shell command after the RUN (e.g., RUN mkdir /user/local/foo). This will automatically run in a /bin/sh shell. You can define a different shell like this: RUN /bin/bash -c 'mkdir /user/local/foo' COPY ---- COPY copies local files into the container. CMD ---- CMD defines the commands that will run on the Image at start-up. Unlike a RUN, *this does not create a new layer for the Image*, but simply runs the command. There can only be one CMD per a Dockerfile/Image. If you need to run multiple commands, the best way to do that is to have the CMD run a script. CMD requires that you tell it where to run the command, unlike RUN. So example CMD commands would be: CMD ["python", "./app.py"] CMD ["/bin/bash", "echo", "Hello World"] EXPOSE ------ EXPOSE creates a hint for users of an image which ports provide services. It is included in the information which can be retrieved via docker inspect . .. note:: The EXPOSE command does not actually make any ports accessible to the host! Instead, this requires publishing ports by means of the -p flag when using $ docker run. PUSH ----- PUSH pushes your image to Docker Cloud, or alternately to a private registry .. note:: If you want to learn more about Dockerfiles, check out Best practices for writing Dockerfiles. Next Steps : Deploying an app to a Swarm =========================================== .. seealso:: - :ref:`labs_deploy_swarm` For the next step in the tutorial head over to :ref:`3.0 Deploying an app to a Swarm `