Every single time I have to set up Docker for a new web server I forget a couple key things on how to set it up. This little post is mostly a note to my future self that this is how you do it and why.
For a detailed breakdown of common keywords and concepts of putting together a Dockerfile, check out my previous post on Dockerizing a simple python process.
I found myself needing to run a django server on Docker today because it turns out that you can't set memory limits using the python resource
library on processes on macOS since about version 10.14. Solution? Run it on a Python Docker Image that runs on Linux. Easy enough.
FROM python:3.11
RUN pip install poetry
COPY . .
RUN poetry install
CMD ["python", "./myapp/manage.py", "runserver"]
Quick and dirty. I could probably be more careful with what I'm copying, where, and such. But I'm in a rush and this will do the job.
docker build . -t myapp
Builds just fine, I see poetry installing the packages that it should. Beautiful!
However, when I try to run the image...
docker run -it -p 8000:8000 myapp
Traceback (most recent call last):
File "//./myapp/manage.py", line 11, in main
from django.core.management import execute_from_command_line
ModuleNotFoundError: No module named 'django'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "//./myapp/manage.py", line 22, in <module>
main()
File "//./myapp/manage.py", line 13, in main
raise ImportError(
ImportError: Couldn't import Django. Are you sure it's installed and available on your PYTHONPATH environment variable? Did you forget to activate a virtual environment?
That's annoying. I just saw poetry install all the libraries. And yes, I went and checked, and django
is present in my poetry.lock
and pyproject.toml
files. What gives? Seems like poetry is creating a virtualenv somewhere that needs to be activated, but that's really not necessary since Docker is already isolating the project and having a virtualenv is redundant. So let's try add a poetry config command to the Dockerfile.
FROM python:3.11
RUN pip install poetry
COPY . .
RUN poetry config virtualenvs.create false --local
RUN poetry install
CMD ["python", "./cohortcreation/manage.py", "runserver"]
Now, the --local tag isn't strictly necessary here, because, again, Docker is isolating this project. But just in case I copy paste this somewhere where it might mess with poetry configurations, let's make sure this config value stays local to whatever project I'm working with.
Next, rebuild the docker image. Loads just fine. Let's try running it.
docker run -it -p 8000:8000 myapp
2023-12-05 22:33:32,585 INFO autoreload django.utils.autoreload Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
December 05, 2023 - 22:33:32
Django version 4.2.4, using settings 'myapp.project.settings'
Starting ASGI/Daphne version 4.0.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
2023-12-05 22:33:32,806 INFO server daphne.server HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2023-12-05 22:33:32,806 INFO server daphne.server Configuring endpoint tcp:port=8000:interface=127.0.0.1
2023-12-05 22:33:32,807 INFO server daphne.server Listening on TCP address 127.0.0.1:8000
Hey! That looks like what I'd expect from running ./manage.py runserver
on an average django project. Success! Now I just need to open my browser and get on with my testing.
What? No. Why? I set up the ports when running the container. I even remembered this time to add the -p 8000:8000
flag to the run command the first time! I even remembered that the first port listed is for what port you want to access on the host, and the second port is the port you want Docker to forward, even though that always feels backwards to me.
Okay, let's check the container and see what's up.
docker ps
. . .
d8ccf5c1aa7a myapp "python ./myapp/ma…" 6 seconds ago Up 5 seconds 0.0.0.0:8000->8000/tcp priceless_pascal
That all seems correct. Should be able to access localhost:8000 to get to whatever port 8000 on the docker container is doing. So what's happening here?
I kill the container, run the webserver in a different terminal tab, just to make sure I'm not going crazy. It loads fine. I keep the local server running, but also start up the docker container again, and now I can't access the app front end through the browser anymore. So clearly docker is doing something with port 8000. But letting me access my server isn't it.
I'll spare you the roughly three thousand google searches I did before arriving at the answer. Long story ~short~ somewhat less long, Python thinks we're exposing our API at 127.0.0.1:8000
. This is what we want if we're just running the server locally. However, if we look at the ports output from docker ps
, we're not exposing 127.0.0.1:8000
. We're exposing 0.0.0.0:8000
. One last tweak to the Dockerfile should do the trick.
FROM python:3.11
RUN pip install poetry
COPY . .
RUN poetry config virtualenvs.create false --local
RUN poetry install
CMD ["python", "./myapp/manage.py", "runserver", "0.0.0.0:8000"]
Building the image works. Running the image works. Now I just need to cross my fingers and check my browser.
Thank god. Time for a nap.