In my first post about Docker, I took you through what goes in the Dockerfile and docker-compose.yml files. In this post, let’s talk about what it’s like to actually use Docker.
This post assumes that the following are true for you:
- You have worked through the Get Started with Docker tutorial
- You have a Django project with a Dockerfile and docker-compose.yml file (see my first post for help setting these up)
- You can run docker-compose build and docker-compose up, start your server (if that isn’t part of your docker-compose.yml file already), and see a Django blue screen of success (or other “YAY!” indication) when you look at your web page in a browser
To see what my Dockerfile and docker-compose.yml file look like for the purposes of this post, head over to my first post.
On a New Day
If you've changed anything in your Dockerfile or requirements, then you will want to rebuild your image. This will retrieve any updates from the Python image you are using, re-install your requirements if they have changed, and make sure your image is up to date. If not much has changed in your Dockerfile or requirements, this will only take a few seconds. If you have made more substantial changes, this might take a little longer.
Open Terminal or your command line, navigate to the directory that contains your Dockerfile, and run this command:
$ docker-compose build
We’re using docker-compose build instead of docker build . because once you start using Compose, you should consistently use the Compose commands. I’ve found through some trial and error that the two commands don’t always have the same result, and that can be frustrating to debug.
You will see a lot of output that looks something like this:
$ docker-compose build db uses an image, skipping Building web Step 1/10 : FROM python:3.6.2 ---> 26acbad26a2c … Step 7/10 : RUN pip install -r /code/requirements.txt ---> Using cache ---> 6494267dadea Step 8/10 : COPY . /code/ ---> d80b24eb2470 Step 9/10 : WORKDIR /code/ ---> c0e80ec3605d Removing intermediate container 8d15670dbe57 Step 10/10 : EXPOSE 8000 ---> Running in aec283f5123c ---> c8f944861cbf Removing intermediate container aec283f5123c Successfully built c8f944861cbf Successfully tagged my_app_web:latest
You can see from this output that Docker treats each line in your Dockerfile as its own step. Step 1 retrieves the Python image you specified, Step 7 installs your requirements, and so on. This is what folks mean when they describe Docker as layered. Each line in your Dockerfile is a layer. Docker will skip any step that was complete the last time you built the image… as long as all the steps above it have stayed the same. If I changed the Python image that I’m basing this image on, then each step below that would be re-run in its entirely. If I change the WORKDIR on line 9, then steps 9 and 10 both have to be re-run. Jeff Triplett says, “I think of layers like dominoes. If you have 10 lines and you push the fourth one over, lines 4-10 are going to fall over and have to be rebuilt.” Read more about images and layers in the Docker docs.
Assuming your image built with no errors, you now have a container! Docker indicates that to you with the final lines:
Successfully built c8f944861cbf Successfully tagged my_app_web:latest
Docker successfully built an image for you, gave it an ID, labeled that image with whatever you told it to name your image (my_app_web in this example), and tagged it “latest.”
Now you’re ready to run your container(s). Run this command:
$ docker-compose up
You will see some output and see your server start (as long as starting your server is part of your docker-compose.yml file).
Starting my_app_db_1 ... Starting my_app_db_1 ... done Recreating my_app_web_1 ... Recreating my_app_web_1 ... done Attaching to my_app_db_1, my_app_web_1 db_1 | LOG: database system was shut down at 2017-10-19 00:59:19 UTC db_1 | LOG: MultiXact member wraparound protections are now enabled db_1 | LOG: autovacuum launcher started db_1 | LOG: database system is ready to accept connections web_1 | Performing system checks... web_1 | web_1 | System check identified no issues (0 silenced). web_1 | web_1 | October 19, 2017 - 01:08:50 web_1 | Django version 1.11.5, using settings 'my_app.settings.local' web_1 | Starting development server at http://0.0.0.0:8000/ web_1 | Quit the server with CONTROL-C.
Remember in our docker-compose.yml file when we asked Docker to not start our web service until our db service had successfully started? If you look at the top lines, you can see Docker is doing as we asked:
Starting my_app_db_1 ... Starting my_app_db_1 ... done Recreating my_app_web_1 ... Recreating my_app_web_1 ... done
Running manage.py commands
Once you’re using Compose, you can run your normal manage.py commands without any extra work or setup in the docker-compose.yml file. We set docker-compose.yml up to automatically start our server, but running the server isn’t the only thing we do with manage.py.
To access your manage.py commands, enter:
$ docker-compose run -rm web ./manage.py [command] [arguments]
docker-compose run tells Docker that we’re about to run a command. The -rm option will shut down this container when we’re finished with it. web identifies the service we want to run; manage.py commands will generally be in the web service.
The rest is the standard way to run manage.py commands: ./manage.py [command]. Since the Dockerfile defines our WORKDIR as /code/, we can run manage.py from the current directory. For example, to run tests, we would run:
$ docker-compose run -rm web ./manage.py test [app]
The output is what we expect:
Starting my_app_db_1 ... done Creating test database for alias 'default'... System check identified no issues (0 silenced). ............... ---------------------------------------------------------------------- Ran 15 tests in 0.577s OK Destroying test database for alias 'default'...
On the top line we see Docker start the db service, but the other output is probably familiar to you.
To make migrations, we would run:
$ docker-compose run -rm web ./manage.py makemigrations
Your container's bash prompt
You can also run commands from Docker’s bash shell. This is handy when you’ve run docker-compose up and you don’t want to stop your server while you do something else.
But in order to enter the bash shell of a container, you need the container id. While your server is running, open a new Terminal tab. Run the command docker ps to get your running containers and their IDs. You will see something like this:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES [container_1_id] my_app_web "python /code/mana..." 21 minutes ago Up 21 minutes 0.0.0.0:8000->8000/tcp my_app_web_1 [container_2_id] postgres:9.6.5 "docker-entrypoint..." 9 hours ago Up 21 minutes 5432/tcp my_app_db_1
Expand your Terminal window so you can see everything lined up pretty. You can see both of your containers, my_app_web_1 and my_app_db_1, listed. Copy the container ID of your web container.
Now run this:
$ docker exec -it [container_id] bash
exec is the command used to run a command in a running container. The -it option combines two options, -i and -t, which together make the shell interactive and set up a pseudo-TTY. Identify your container by its ID, then type bash to indicate that you want a bash prompt. Your Terminal window will looks a little different:
$ docker exec -it [container_id] bash root@[container_id]:/code#
You’re now inside your container’s bash shell. If you set your WORKDIR as /code/ in your Dockerfile, then you will automatically be in the /code/ directory. If you cd .. and then ls, you can see what other directories live in your container. Feel free to poke around.
You can also enter a container’s bash prompt with a Compose command:
$ docker-compose run [service] bash Starting my_app_db_1 ... done root@[container_id]:/code#
The difference between this command and docker exec -it [container_id] bash is that the exec command puts you into a bash prompt inside a container that was already running. The docker-compose command starts a brand new container and puts your into a bash prompt inside that container. You can see this happen in the output: the docker-compose command will print something like “Starting my_app_db_1,” to indicate it’s starting a new db service, since the new web service will require one. The difference probably won’t matter when things are going well, but it’s good to know.
Once you are in your bash prompt, you can enter the Python shell:
root@[container_id]:/code# ./manage.py shell Python 3.6.2 (default, Sep 13 2017, 14:26:54) Type 'copyright', 'credits' or 'license' for more information IPython 6.2.1 -- An enhanced Interactive Python. Type '?' for help. In [1]:
To exit your bash prompt (whichever way you got into it), just type exit or ctrl + d.
Going Home
I’m not sure how neat or messy you like to leave your desktop at the end of the workday. But if you like to button things up, you might want to stop your containers from running all weekend when you leave the office on Friday. To do this, run:
$ docker-compose down
This will stop your running containers. If you haven’t been using the -rm option when running management commands, you will probably see more container notifications than you expected to.
Resources
Thanks to Nick Lang for advice on this post, and to Frank Wiles and Jeff Triplett for their feedback on drafts and their patient debugging help.