I have been meaning to play around with containers for a while but for the life of me have not found a real reason to. I feel like without a real use case, any attempt I'd make to learn anything useful would be a huge waste of time. There are a bunch of neat toys out there, from running random ASCII art commands to a crazy script that 'emulates' some of the insane Hollywood style computer screens, as well as base images for all manner of application stacks and frameworks, but all of those are easily installable using your favorite package manager.
None of this really made me care enough to install and learn anything about any of the container ecosystems. I do like the idea of containers as sandboxes but as a macOS user I have that built in for free, so I have no impetus there either.
Still, there is a lot of talk about containers in the development community so I have been keeping an eye out for a use-case where I could justify investing time in them. Lately my primary development work has been creating various bespoke Flask applications. Flask comes with Werkzeug and a simple server built in, so I typically just run the internal server, iterate on the code, and then commit to my git repository. Eventually Puppet comes along and does the heavy lifting to deploy the changes to production. This works really well and I can't really figure out a reason to shoehorn a container into the process..
Turns out the excuse came from this web site. As I have written about before this entire site is generated from a home brew Python script. It takes all the design from templates and blog articles from markdown files and is triggered from a git post-receive hook on the web server. This lets me make a very fast web site that doesn't rely on any dynamic pages or API calls. The one drawback of this method lies in the differences between viewing pages over HTTP/HTTPS versus off the local filesystem. To test the site locally I was hand-editing some of the output to change some of the URLs from paths that would work on the website to paths that work on the local filesystem. This was getting annoying and frankly is just the thing to replace with a very small shell script.
I initially thought about modifying the build script to use filesystem paths when building locally, but that would just add complexity and potential for breakage. I then thought about fooling around with the web server built into macOS but I am generally loathe to mess around with things in the bowels of the OS lest I do something that Apple breaks in an update. In the end I figured this might finally be a good excuse to pull together a Docker container running Apache, that included the Python bits that the site builder needed and then in true ex-sysadmin fashion wrap it up in a nice shell script.
This resulted in a pretty reasonable work flow.
- Update working copy of site.
- Run test.sh
- build Docker image
- copy working copy into Docker image
- launch an instance of this image.
- Open a browser to the URL of the local Docker instance.
- Verify things are the way we want.
- Fix and GOTO 1 or continue.
- git add, commit, push to remote.
- git hook deploys to production.
Now to be fair there are probably easier ways to do this including using a staging branch that is served on another domain name, directory, or on an internal VM. This would save me from building, launching, and cleaning up images. I could use my normal publishing work flow and scripts to simply do the right thing and then merge back to master when I'm ready to deploy the site to production.
But that doesn't give me an excuse to play with 🐋 Docker. 😁
Details
As of the time of writing these are the main pieces that make this work flow possible.
Dockerfile
FROM debian:latest
LABEL version="0.3.0" \
vendor="Matthew Ernisse <matt@going-flying.com>" \
description="Build and serve going-flying.com"
RUN apt-get update \
&& apt-get install -y \
apache2 \
python \
python-pip \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/www/going-flying.com \
&& a2dissite 000-default
COPY docker/going-flying.conf /etc/apache2/sites-available
COPY . /var/www/going-flying.com
RUN a2ensite going-flying \
&& pip install \
--requirement /var/www/going-flying.com/docker/requirements.txt \
&& /var/www/going-flying.com/build.py
EXPOSE 80
CMD ["/usr/sbin/apachectl", "-DFOREGROUND"]
This is pretty straightforward. I take the Debian base Docker image and install the bits I need to build and serve the site. I also have a very basic apache configuration fragment that points the server to the location I will be copying the site files to (the same location as in production so the script doesn't have to care). I then simply copy the working copy of the site into the image and run build.py on it.
test.sh
#!/bin/sh
# test.sh (c) 2017 Matthew J. Ernisse <matt@going-flying.com>
# All Rights Reserved.
#
# Build and run a copy of the website inside a Docker container.
set -e
echo "going-flying.com test builder."
if ! which docker 2>&1 >/dev/null; then
echo "docker not found."
exit 1
fi
if [ "$(uname -s)" != "Darwin" ]; then
echo "Not running on macOS. Exiting."
exit 2
fi
cat << EOF
## .
## ## ## ==
## ## ## ## ## ===
/"""""""""""""""""\___/ ===
{ / ===-
\______ O __/
\ \ __/
\____\_______/
EOF
echo "Building image..."
_image=$(docker build --force-rm --squash . -t going-flying:latest | \
awk '/^Successfully built [0-9a-f]+/ { print $3 }')
docker run --rm -d -p 8080:80 --name going-flying $_image > /dev/null
open "http://localhost:8080"
echo "Container running, Press [RETURN] to end."
read
echo "Stopping..."
docker stop going-flying > /dev/null
echo "OK."
This just does the docker build and docker run dance that causes a container to be running. It can probably be simplified even further but it gets the job done. The biggest thing was to make sure that I wasn't leaving a pile of images and whatnot laying around. And not having to remember the different command line switches needed to make it all Just Work.
build.py
The only other change was a hook in build.py that changes the base URL of the site from the normal https://www.going-flying.com/ to http://localhost:8080/. It does this by simply detecting if it is running in a Docker container and changing an instance variable.
def is_docker():
''' Return True if we're running in docker.'''
if not os.path.exists('/proc/self/cgroup'):
return None
with open('/proc/self/cgroup') as fd:
line = fd.readline()
while line:
if 'docker' in line:
return True
line = fd.readline()
return None
[ ... later in main() ... ]
if is_docker():
BuildConfig.urlbase = "http://localhost:8080/"
print ":whale: container detected."
I was skeptical at first if this was going to be worth it, but after using this for a few site updates, I honestly feel that this was easier than many of the alternatives and in the end let me go back to fixing a bunch of style and template bugs that I had on the TODO list for some time. I'd call that a result that was worth the effort. I look forward to finding more places where a container fits into my work flow. It might even turn into an excuse to run a private registry and start playing with some of the CI tools to run builds.
Errata
It turns out that Safari doesn't like to auto play videos not in view when the page loads. I tried to slam together some JavaScript to 'fix' this, but your milage may vary. If the videos aren't playing you should be able to right click on one of them and say 'show controls' then hit play.