Webapps with Docker Part Deux
Now that we understand the structure of Docker images, it’s time to build a custom Docker image from a Dockerfile and deploy it as a web application.
Tasks:
Prerequisites
Ensure you have a DockerID. If you don’t have a DockerID you can get one for free via Docker Hub.
Task 1: Package and run a custom app using Docker
In this step you’ll learn how to package your own apps as Docker images using a Dockerfile.
The Dockerfile syntax is straightforward. In this task we’re going to create an NGINX website from a Dockerfile.
Clone the Lab GitHub Repo
Use the following command to clone the lab repo from GitHub.
$ git clone https://github.com/dockersamples/linux_tweet_app
Cloning into 'linux_tweet_app'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 14 (delta 5), reused 14 (delta 5), pack-reused 0
Unpacking objects: 100% (14/14), done.
Build a simple website image
Let’s have a look at the Dockerfile we’ll be using, which builds a simple website that allows you to send a tweet.
-
Make sure you’re in the
linux_tweet_appdirectory:$ cd ~/linux_tweet_app -
Display the contents of our Dockerfile.
$ cat Dockerfile FROM nginx:latest COPY index.html /usr/share/nginx/html COPY linux.png /usr/share/nginx/html EXPOSE 80 443 CMD ["nginx", "-g", "daemon off;"]Let’s see what each of these lines in the Dockerfile do.
- FROM specifies the base image to use as the starting point for this new image you’re creating. For this example we’re starting from
nginx:latest. - COPY copies files from the host into the image, at a known location. In our case it copies
index.htmland a graphic that will be used on our webpage. - EXPOSE documents which ports the application uses.
- CMD specifies what command to run when a container is started from the image. Notice that we can specify the command, as well as run-time arguments.
- FROM specifies the base image to use as the starting point for this new image you’re creating. For this example we’re starting from
-
In order to make commands more copy/paste friendly, export an environment variable containing your DockerID (if you don’t have a DockerID you can get one for free via Docker Hub)
$ export DOCKERID=<your docker id>Special Note: On Windows, use
set DOCKERID=<your docker id>in CMD (and reference it as%DOCKERID%), or$env:DOCKERID="<your docker id>"in PowerShell (and reference it as$env:DOCKERID). -
To make sure it stored correctly by echoing it back in the terminal
$ echo $DOCKERID <your docker id> -
Use the
docker image buildcommand to create a new Docker image using the instructions in your Dockerfile.--tagallows us to give the image a custom name. In this case it’s comprised of our DockerID, the application name, and a version. Having the Docker ID attached to the name will allow us to store it on Docker Hub in a later step.tells Docker to use the current directory as the build context
Be sure to include period (
.) at the end of the command as this indicates the current directory.$ docker image build --tag $DOCKERID/linux_tweet_app:1.0 . Sending build context to Docker daemon 32.77kB Step 1/5 : FROM nginx:latest latest: Pulling from library/nginx afeb2bfd31c0: Pull complete 7ff5d10493db: Pull complete d2562f1ae1d0: Pull complete Digest: sha256:af32e714a9cc3157157374e68c818b05ebe9e0737aac06b55a09da374209a8f9 Status: Downloaded newer image for nginx:latest ---> da5939581ac8 Step 2/5 : COPY index.html /usr/share/nginx/html ---> eba2eec2bea9 Step 3/5 : COPY linux.png /usr/share/nginx/html ---> 4d080f499b53 Step 4/5 : EXPOSE 80 443 ---> Running in 47232cb5699f ---> 74c968a9165f Removing intermediate container 47232cb5699f Step 5/5 : CMD nginx -g daemon off; ---> Running in 4623761274ac ---> 12045a0df899 Removing intermediate container 4623761274ac Successfully built 12045a0df899 Successfully tagged <your docker ID>/linux_tweet_app:1.0The output above shows the Docker daemon execute each line in the Dockerfile.
Feel free to run a
docker image lscommand to see the new image you created. -
Use the
docker container runcommand to start a new container from the image you created.As this container will be running an NGINX web server, we’ll use the
--publishflag to publish port 80 inside the container onto port 8080 on the host. This will allow traffic coming in to the Docker host on port 8080 to be directed to port 80 in the container. The format of the--publishflag ishost_port:container_port.$ docker container run \ --detach \ --publish 8080:80 \ --name linux_tweet_app \ $DOCKERID/linux_tweet_app:1.0Any external traffic coming into the server on port 8080 will now be directed into the container.
-
Open your newly created Web App in your Browser
http://localhost:8080 -
Once you’ve accessed the website, shut it down and remove it.
$ docker container rm --force linux_tweet_app linux_tweet_appNote: We used the
--forceparameter to remove the running container without shutting it down. This will ungracefully shutdown the container and permanently remove it from the Docker host.In a production environment you may want to use
docker container stopto gracefully stop the container and leave it on the host. You can then usedocker container rmto permanently remove it.
Task 2: Modify a running website
When you’re actively working on an application it is inconvenient to have to stop the container, rebuild the image, and run a new version every time you make a change to your source code.
One way to streamline this process is to bind mount the source code directory on the local machine into the running container. This will allow any changes made to the files on the host to be immediately reflected in the container.
We do this using something called a bind mount.
When you use a bind mount, a file or directory on the host machine is mounted into a container.
Start a web app with a bind mount
-
Let’s start the web app and mount the current directory into the container.
In this example we’ll use the
--mountflag to mount the current directory on the host into/usr/share/nginx/htmlinside the container.Be sure to run this command from within the
linux_tweet_appdirectory on your Docker host.$ docker container run \ --detach \ --publish 8080:80 \ --name linux_tweet_app \ --mount type=bind,source="$(pwd)",target=/usr/share/nginx/html \ $DOCKERID/linux_tweet_app:1.0Remember from our Dockerfile
usr/share/nginx/htmlis where are html files are stored for our web app -
Open the Linux_Tweet App in your Browser
http://localhost:8080to verify the website is running (you may need to refresh the browser to get the latest version).
Modify the running website
Because we did a bind mount, any changes made to the local filesystem are immediately reflected in the running container.
-
Copy a new
index.htmlinto the container.The Git repo that you pulled earlier contains several different versions of an index.html file. Run an
lscommand from within the~/linux_tweet_appdirectory to see a list of them. In this step we’ll replaceindex.htmlwithindex-new.html.$ cp index-new.html index.html -
Refresh the web page. The site will have changed.
Using your favorite editor (vi, emacs, etc) you can use it to load the
index.htmlfile and make additional real-time changes. Those too would be reflected when you reload the webpage.Edit the index.html file and edit line number 33 and change the text to “Docker is Awesome!”
$ vi index.htmlEven though we’ve modified the
index.htmllocal filesystem and seen it reflected in the running container, we’ve not actually changed the original Docker image.To show this, let’s stop the current container and re-run the
1.0image without a bind mount. -
Stop and remove the currently running container
$ docker container rm --force linux_tweet_app linux_tweet_app -
Rerun the current version without a bind mount.
$ docker container run \ --detach \ --publish 8080:80 \ --name linux_tweet_app \ $DOCKERID/linux_tweet_app:1.0 -
Open the Tweet Web App in your Browser
http://localhost:8080Notice it’s back to the original version with the blue background. -
Stop and remove the current container
$ docker container rm --force linux_tweet_app linux_tweet_app
Task 3: Update and version your image
To save the changes you made to the index.html file earlier, you need to build a new version of the image.
-
Build a new image and tag it as
2.0Remember that you have previously modified the
index.htmlfile on the Docker hosts local filesystem. This means that running anotherdocker image buildwill build a new image with the updatedindex.html.Be sure to include the period (
.) at the end of the command.$ docker image build --tag $DOCKERID/linux_tweet_app:2.0 .Notice how fast that built! This is because Docker only modified the portion of the image that changed vs. rebuilding the whole image.
-
Let’s look at the images on our system
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE <your docker id>/linux_tweet_app 2.0 01612e05312b 16 seconds ago 108MB <your docker id>/linux_tweet_app 1.0 bb32b5783cd3 4 minutes ago 108MB mysql latest b4e78b89bcf3 2 weeks ago 412MB ubuntu latest 2d696327ab2e 2 weeks ago 122MB nginx latest da5939581ac8 3 weeks ago 108MB alpine latest 76da55c8019d 3 weeks ago 3.97MBNotice you have both versions of the web app on your host now.
Test the new version
-
Run a container from the new version of the image.
Be sure to reference the image tagged as
2.0.$ docker container run \ --detach \ --publish 8080:80 \ --name linux_tweet_app \ $DOCKERID/linux_tweet_app:2.0 -
Open the Tweet Web App in your Browser
http://localhost:8080The web page will have an orange background.
We can run both versions side by side. The only thing we need to be aware of is that we cannot have two containers using port 8080 on the same host.
As we’re already using port 8080 for the container running from the
2.0version of the image, we will start a new container and publish it on port 8081. Additionally, we need to give our container a unique name (old_linux_tweet_app) -
Run the old version (make sure you map it to port 8081 on the host, give it the unique name, and reference the 1.0 version of the image).
$ docker container run \ --detach \ --publish 8081:80 \ --name old_linux_tweet_app \ $DOCKERID/linux_tweet_app:1.0 -
Open the Tweet Web App in your Browser
http://localhost:8081to view the old version of the website.
Bravo, we have successfully deployed 2 versions of our web app in parallel to our Docker host.
-
Stop the running containers
$ docker container ps $ docker container stop old_linux_tweet_app $ docker container stop linux_tweet_app
Review
What did we just accomplish?
- We created a Dockerfile, built a container image and ran a container from this newly create image
- Next, we modified the website both the version & index.html file showing real-time updates to the application
- Finally, we committed our new changes into a newly created image
- We ran version 1 and version 2 side-by-side
Dockerfile commands summary
Here’s a quick summary of the few basic commands we used in our Dockerfile.
-
FROMstarts the Dockerfile. It is a requirement that the Dockerfile must start with theFROMcommand. Images are created in layers, which means you can use another image as the base image for your own. TheFROMcommand defines your base layer. As arguments, it takes the name of the image. Optionally, you can add the Docker Hub username of the maintainer and image version, in the formatusername/imagename:version. -
RUNis used to build up the Image you’re creating. For eachRUNcommand, 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 aRUNinstruction is to place the full text of the shell command after theRUN(e.g.,RUN mkdir /user/local/foo). This will automatically run in a/bin/shshell. You can define a different shell like this:RUN /bin/bash -c 'mkdir /user/local/foo' -
COPYcopies local files into the container. -
CMDdefines the commands that will run on the Image at start-up. Unlike aRUN, this does not create a new layer for the Image, but simply runs the command. There can only be oneCMDper Dockerfile/Image. If you need to run multiple commands, the best way to do that is to have theCMDrun a script.CMDrequires that you tell it where to run the command, unlikeRUN. So exampleCMDcommands would be:
CMD ["python", "./app.py"]
CMD ["/bin/bash", "echo", "Hello World"]
EXPOSEcreates a hint for users of an image which ports provide services. It is included in the information which can be retrieved via$ docker container inspect <container-id>.
Note: The
EXPOSEcommand does not actually make any ports accessible to the host! Instead, this requires publishing ports by means of the-pflag when using$ docker container run.
Note: There is no
PUSHinstruction in a Dockerfile. To push an image to Docker Hub (or a private registry), use thedocker image pushCLI command.
Note: If you want to learn more about Dockerfiles, check out Best practices for writing Dockerfiles.
Great! So you have now looked at docker container 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.
Next Steps
For the next step in the tutorial head over to Docker & DevOps