loading...
Cover image for docker learn #06: Hello Python in Alpine

docker learn #06: Hello Python in Alpine

alexoeducative profile image Alex Ortiz ・5 min read

So here I am with a full month of Docker under my belt, wondering what I can do to show myself my progress. To decide what to write about this week, let me start from scratch: what do I know?

I know that to run a docker container, I need a docker image. And to build a docker image, I need a Dockerfile, which contains the files plus metadata necessary to run a self-contained environment. The environment includes an operating system and the application I wish to run. For example, if I want to run a Python3 shell inside a container as if I were running it on my local machine's terminal, then I'm going to need, at a minimum, the following environment:

  • an operating system on which to run Python3
  • the Python3 programming language installed on the OS
  • a command line interface to interact with Python3 in shell mode

In other words, I need to be able to run the Python3 shell inside a terminal inside a Linux Docker container.

Easy enough.

Installing a Base Image...but Which One?

First up is the operating system.

Python is a pretty lightweight language that just about any operating system can handle. So I want to use a super lightweight Linux distribution to serve as my operating system for this exercise. In lieu of Ubuntu, let's go with Alpine, which is 1/10th the size.

If I run the following command from my Docker CLI, though, I'll run into trouble right away:

docker run alpine

I won't want to run just this command, because if I do, then this happens:

  • my Docker client calls the docker daemon via the Docker API
  • it tells the daemon that I want to run an alpine container (totally is true)
  • the daemon will dutifully do all this stuff:
    • pull the alpine image from Docker Hub 💪🏾
    • run the alpine image on my Docker server 💪🏾
    • run an alpine container from this image 💪🏾
    • immediately exit said container 😤

This is because the Dockerfile for an alpine image has but one command that runs as default: "/bin/sh".

So when I tell Docker to pull or run alpine, it does that, and the base image runs, and it does what it's supposed to, but then the shell a) doesn't receive any additional instructions, b) determines that there is nothing for it to do, and consequently c) stops running, which leaves the container without a process to run, causing the container to be terminated (exited). No process, no container. I get it.

Side Note 1: This can be confirmed with the docker ps -a command, which will show that an alpine container did indeed momentarily run but was then exited.

This is of course no good to me if what I want to do is run Python3 inside a container.

So it's time to break out the useful -ti flag.

Using -ti To Run an Interactive Terminal

For all the above reasons, I run the following instead:

docker run -ti alpine

There we go. Now the docker daemon will do its thing again, but this time, the alpine container won't exit. The combo of the two Docker flags t and i will instruct Docker to please allocate me a terminal and patiently listen for my input.

I'll now be able to use the built-in shell to move on to my next step: installing Python3 inside this container.

Adding Python3 with the Alpine Package Manager, apk

Alpine Linux doesn't come with Python3 pre-installed. You have to install it manually. It's very simple to do with the alpine package manager, apk. A quick check of the Alpine Linux APK website shows that python3 is indeed one of the packages.

Since I'm now running an Alpine Linux docker container with an interactive terminal waiting for me to provide further instruction, I can proceed:

apk add python3

This will install python3 on my alpine operating system inside the Docker container. I can confirm the installation afterwards by typing python3 --version into the alpine terminal, which outputs Python 3.7.5:

/ # python3 --version
Python 3.7.5

We're in business! On to the final step: launching Python3 in shell mode.

Side Note 2: By the way, the presence of / # indicates that I'm in the interactive shell terminal within my alpine container.

Actually Running Python3 in Shell Mode

At last, I can run Python3. Since I just installed python3, I can simply run it by typing python3:

/ # python3
Python 3.7.5 (default, Oct 17 2019, 12:25:15) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

The presence of >>> indicates that I'm in Python3's shell mode. That is, I am now actually running the Python3 application. Now for the fun stuff! 😎

Taking on a Small Python Challenge

Now that I have Python running in shell mode inside an interactive shell terminal inside an Alpine Docker container, I can take on a fun challenge. For example, my Educative team recently wrote a blog post with six fun Python challenges.

Let's try two of them right now :).

Challenge #1: World, Can You Hear Me?

Here was my team's prompt to readers:

Challenge #1 from Level up your Python skills with these 6 challenges

Challenge accepted. In my Docker container, I type this:

>>> print("Hello World\nLet's Learn Python\nSincerely, Alex")

And the Python shell prints the following:

Hello World
Let's Learn Python
Sincerely, Alex

Challenge #2: Some Maths and Physics

Here was my team's prompt to readers:

Challenge #2 from Level up your Python skills with these 6 challenges

They also provided the following values:

G = 6.67 x 10^-11
MSun = 2.0 x 1030
mEarth = 6.0 x 1024
r = 1.5 x 1011

So in my Docker container, I created variables G, M, m, and r and stored their respective values in each, using Python-friendly scientific notation. Then I created the variable grav_force as instructed, though converting it into the Python-friendly notation. Finally, I instructed Python to print the variable I just created, outputting the result of Newton's beautiful gravitational force equation.

Putting this all together, it was like so:

>>> G = 6.67e-11
>>> M = 2.0e30
>>> m = 6.0e24
>>> r = 1.5e11
>>> grav_force = (G*M*m)/(r*r)
>>> print(grav_force)
3.5573333333333336e+22

Close enough, and not bad for this all running inside a Docker container! :)

I then type (exit) to exit the Python3 shell, and then exit to exit the alpine container I've been using. To confirm that the daemon terminated the container, I use docker ps -a -l, and it shows me that the last container that ran indeed was exited, seconds ago.

Wrapping Up

I've once again run out of time to write all the other cool stuff I learned this week or am excited about with respect to Docker. But I'm really happy that I got to "test" myself by starting from what I've learned so far and then moving towards a tangible demonstration of my learnings—even crossing over into Python-land.

And did I mention how sweet it is that we just saw how easy and cool it is to run Python-in-Linux-in-Docker?

Very cool indeed. Till next time!


You know the drill: hit the DEV.to Follow button, follow me on Twitter @alexoeducative, and definitely visit our blog. Chat or comment below if you'd like to add any Alpine or Docker knowledge!

Posted on by:

alexoeducative profile

Alex Ortiz

@alexoeducative

Lead of Developer Sales & Success at Educative. Finding the next 100 developers in the world to build content on our platform!

Discussion

pic
Editor guide
 

Hi,
I'm trying to run a flask application on a python 3.7 alpine image, My application uses bcrypt and it's in the requirements.txt. when ever I try to build the image it fails with an error (Package libffi was not found in the pkg-config search path). Any Ideas or suggessions are very much appreciated.

 

the alpine images arent built with the same c compiler/extensions. So a lot of that stuff doesnt work by default. That is why a lot of people use the "slim" builds like python:3.8-slim-buster. you may be able to do apk add libffi but i don't remember off hand. Also, other python libraries that use any native C extensions, like psycopg2 for postgres, wont work on alpine because there is no .whl (wheel) packages for them. So you would have to compile those on your own. theres ways around some of if but you would have to research that.

Honestly, if you're just learning docker or just want it to work, use the slim versions instead. As you learn more or you decide you want to optimize, you can look into using alpine more. The difference in speed for them start up for 99% of people is negligible.

 

Shawn, thanks for your reply.
I managed to get it working using the following in the Dockerfile:
RUN apk update
RUN apk add libffi-dev
RUN apk add mysql-client
RUN apk add gawk
It was a good learning experience, though.

Awesome! Glad you got it. One thing to keep in mind, is that each RUN command will result in another docker layer that can be cached. So its good to string them together like so:

RUN apk update \
    apk add libffi-dev \
    apk add mysql-client \
    apk add gawk

That way, that one layer can be cached for future builds. This can also effect image sizes. running api update and so on will download other metadata and those will stay on the image. Anything added in a layer will take up space in all future layers, unless its also removed in that same layer. So when using apk (or other package managers) in docker you want to clean up afterwards. Here is how I would re-write the above to do that

RUN apk add --no-cache --virtual .build-deps \
    # requirements you usually don't need to keep around but need to build libs
    gcc libc-dev make \
    # requirements you need to make your app run
    libffi-dev mysql-client gawk \
    # example installing anything that needs those build requirements
    && pip install --no-cache-dir uvicorn gunicorn \ 
    # this will remove the deps no longer needed, as well as clean up anything else
    && apk del .build-deps gcc libc-dev make

And all that will be cached in 1 layer, and those temporary deps installed by apk won't take up any space. this is the image I took this from
github.com/tiangolo/uvicorn-gunico...