├── README.md ├── dev-environment ├── Dockerfile ├── README.md ├── docker-compose.yml ├── requirements.txt └── src │ └── main.py ├── example1 ├── Dockerfile ├── README.md └── main.py └── example2 ├── .dockerignore ├── Dockerfile ├── README.md ├── app └── main.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Python Docker Tutorials 2 | 3 | Learn how use Docker for Python development. 4 | 5 | Accompanying repo to my talks at the Docker Community All Hands: 6 | 7 | ## How to containerize Python applications with Docker 8 | 9 | This talk contains two examples: 10 | - [Example 1: Dockerize a Python script](/example1/) 11 | - [Example 2: Dockerize a web app](/example2/) 12 | 13 | [![Alt text](https://img.youtube.com/vi/0UG2x2iWerk/hqdefault.jpg)](https://youtu.be/0UG2x2iWerk) 14 | 15 | ## How to containerize Python applications with Docker 16 | 17 | Coming soon... -------------------------------------------------------------------------------- /dev-environment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | # set the working directory 4 | WORKDIR /code 5 | 6 | # install dependencies 7 | COPY ./requirements.txt ./ 8 | RUN pip install --no-cache-dir --upgrade -r requirements.txt 9 | 10 | # copy the src to the folder 11 | COPY ./src ./src 12 | 13 | # start the server 14 | CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "80", "--reload"] 15 | -------------------------------------------------------------------------------- /dev-environment/README.md: -------------------------------------------------------------------------------- 1 | # How to create a great local Python development environment with Docker 2 | 3 | Work in progress 4 | 5 | 1. Why Docker? 6 | 2. Dockerize an app 7 | 3. Immediate file changes (volumes) 8 | 4. Use IDE in Docker 9 | 5. Docker Compose 10 | 6. Add more services (Redis) 11 | 7. Debug Python code inside a container 12 | 13 | ## 1. Why Docker? 14 | Python versions, more than just a virtual env, ... 15 | ## 2. Dockerize an app 16 | 17 | ```Dockerfile 18 | FROM python:3.10-slim 19 | WORKDIR /code 20 | COPY ./requirements.txt ./ 21 | RUN pip install --no-cache-dir --upgrade -r requirements.txt 22 | COPY ./src ./src 23 | CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "80", "--reload"] 24 | ``` 25 | 26 | Use `Dockerfile` and the code in scr directory 27 | 28 | (Use slim or alpine for smaller versions) 29 | 30 | ```console 31 | docker build -t fastapi-image . 32 | docker run --name fastapi-container -p 80:80 fastapi-image 33 | docker run -d --name fastapi-container -p 80:80 fastapi-image 34 | ``` 35 | ## 3. Immediate file changes (volumes) 36 | 37 | ```console 38 | docker stop fastapi-container 39 | docker ps -a 40 | docker rm fastapi-container 41 | 42 | docker run -d --name fastapi-container -p 80:80 -v $(pwd):/code fastapi-image 43 | ``` 44 | ## 4. Use IDE in Docker 45 | ## 5. Docker Compose 46 | 47 | ```yml 48 | services: 49 | app: 50 | build: . 51 | container_name: python-server 52 | command: uvicorn src.main:app --host 0.0.0.0 --port 80 --reload 53 | ports: 54 | - 80:80 55 | - 5678:5678 56 | volumes: 57 | - .:/code 58 | ``` 59 | 60 | Use `docker-compose.yml` 61 | 62 | ```console 63 | docker-compose up 64 | docker-compose down 65 | ``` 66 | 67 | ## 6. Add more services (Redis) 68 | 69 | ```yml 70 | services: 71 | app: 72 | ... 73 | depends_on: 74 | - redis 75 | 76 | redis: 77 | image: redis:alpine 78 | ``` 79 | ## 7. Debug Python code inside a container 80 | 81 | Add this in the code 82 | 83 | ```python 84 | import debugpy 85 | 86 | debugpy.listen(("0.0.0.0", 5678)) 87 | 88 | # print("Waiting for client to attach...") 89 | # debugpy.wait_for_client() 90 | ``` 91 | 92 | And the port in yml: 93 | 94 | ```yml 95 | services: 96 | app: 97 | ... 98 | ports: 99 | - 80:80 100 | - 5678:5678 101 | ``` 102 | 103 | Attach to running container 104 | 105 | ## Try a Python version easily with Docker 106 | 107 | ```console 108 | docker pull python:3.11-slim 109 | docker run -d -i --name python_dev python:3.11-slim 110 | docker exec -it python_dev /bin/sh 111 | ``` 112 | 113 | ## Further Resources: 114 | 115 | - https://towardsdatascience.com/debugging-for-dockerized-ml-applications-in-python-2f7dec30573d 116 | - https://github.com/Wyntuition/try-python-flask-redis-docker-compose 117 | - https://docs.docker.com/compose/gettingstarted/ 118 | - https://www.docker.com/blog/containerized-python-development-part-1/ 119 | 120 | 121 | ## Other commands for cleaning up 122 | 123 | ```console 124 | docker rm container_name 125 | docker image rm image_name 126 | docker system prune 127 | docker images prune 128 | ``` 129 | 130 | Check folder size: 131 | 132 | ```console 133 | du -sh * 134 | ``` 135 | -------------------------------------------------------------------------------- /dev-environment/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | container_name: python-server 5 | command: uvicorn src.main:app --host 0.0.0.0 --port 80 --reload 6 | ports: 7 | - 80:80 8 | - 5678:5678 9 | volumes: 10 | - .:/code 11 | depends_on: 12 | - redis 13 | 14 | redis: 15 | image: redis:alpine -------------------------------------------------------------------------------- /dev-environment/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | pydantic 3 | uvicorn 4 | redis 5 | debugpy -------------------------------------------------------------------------------- /dev-environment/src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | import redis 4 | 5 | r = redis.Redis(host="redis123", port=6379) 6 | app = FastAPI() 7 | 8 | # import debugpy 9 | 10 | # debugpy.listen(("0.0.0.0", 5678)) 11 | # print("Waiting for client to attach...") 12 | # debugpy.wait_for_client() 13 | 14 | 15 | @app.get("/") 16 | def read_root(): 17 | return {"Hello": "World"} 18 | 19 | 20 | @app.get("/hits") 21 | def read_root(): 22 | r.set("foo", "bar") 23 | r.incr("hits") 24 | return {"Number of hits:": r.get("hits"), "foo": r.get("foo")} 25 | 26 | -------------------------------------------------------------------------------- /example1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | ADD main.py . 4 | 5 | RUN pip install requests beautifulsoup4 6 | 7 | CMD ["python", "./main.py"] -------------------------------------------------------------------------------- /example1/README.md: -------------------------------------------------------------------------------- 1 | # Dockerize a simple Python script with third-party modules 2 | 3 | We differ between: 4 | 5 | - Dockerfile: Blueprint for building images 6 | - Image: Template for running containers 7 | - Container: Running process with the packaged project 8 | 9 | ## 1. Build the Docker image 10 | 11 | ```console 12 | $ docker build -t python-imdb . 13 | ``` 14 | 15 | ## 2. Run the Docker image (starts the container) 16 | 17 | Without user input: 18 | 19 | ```console 20 | $ docker run python-imdb 21 | ``` 22 | 23 | If you want user input (comment out the `break` in [main.py](./main.py)): 24 | 25 | ```console 26 | $ docker run -t -i python-imdb 27 | ``` 28 | 29 | -i: interactive, -t: pseudo terminal 30 | -------------------------------------------------------------------------------- /example1/main.py: -------------------------------------------------------------------------------- 1 | import random 2 | import requests 3 | from bs4 import BeautifulSoup 4 | 5 | # crawl IMDB Top 250 and randomly select a movie 6 | 7 | URL = 'http://www.imdb.com/chart/top' 8 | 9 | def main(): 10 | response = requests.get(URL) 11 | 12 | soup = BeautifulSoup(response.text, 'html.parser') 13 | 14 | # print(soup.prettify()) 15 | 16 | movietags = soup.select('td.titleColumn') 17 | inner_movietags = soup.select('td.titleColumn a') 18 | ratingtags = soup.select('td.posterColumn span[name=ir]') 19 | 20 | def get_year(movie_tag): 21 | moviesplit = movie_tag.text.split() 22 | year = moviesplit[-1] # last item 23 | return year 24 | 25 | years = [get_year(tag) for tag in movietags] 26 | actors_list =[tag['title'] for tag in inner_movietags] # access attribute 'title' 27 | titles = [tag.text for tag in inner_movietags] 28 | ratings = [float(tag['data-value']) for tag in ratingtags] # access attribute 'data-value' 29 | 30 | n_movies = len(titles) 31 | 32 | while(True): 33 | idx = random.randrange(0, n_movies) 34 | 35 | print(f'{titles[idx]} {years[idx]}, Rating: {ratings[idx]:.1f}, Starring: {actors_list[idx]}') 36 | 37 | # comment the next line out to test user input with docker run -t -i 38 | break 39 | 40 | user_input = input('Do you want another movie (y/[n])? ') 41 | if user_input != 'y': 42 | break 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /example2/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | .Python 6 | env/ 7 | venv/ 8 | pip-log.txt 9 | pip-delete-this-directory.txt 10 | .tox/ 11 | .coverage 12 | .coverage.* 13 | .cache 14 | nosetests.xml 15 | coverage.xml 16 | *.cover 17 | *.log 18 | .git 19 | .gitignore 20 | .mypy_cache 21 | .pytest_cache 22 | .hypothesis 23 | .idea -------------------------------------------------------------------------------- /example2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /code 4 | 5 | COPY ./requirements.txt /code/requirements.txt 6 | 7 | RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt 8 | 9 | COPY ./app /code/app 10 | 11 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -------------------------------------------------------------------------------- /example2/README.md: -------------------------------------------------------------------------------- 1 | # Dockerize a Python web app 2 | 3 | ## 1. Create the app locally 4 | 5 | ```console 6 | $ python3 -m venv venv 7 | $ . venv/bin/activate 8 | $ pip install fastapi uvicorn 9 | ``` 10 | 11 | Try it: 12 | 13 | ```console 14 | $ uvicorn app.main:app --port 80 15 | ``` 16 | 17 | Save dependencies: 18 | 19 | ```console 20 | $ pip freeze > requirements.txt 21 | ``` 22 | 23 | ## 2. Build the Docker image 24 | 25 | ```console 26 | $ docker build -t fastapi-image . 27 | ``` 28 | 29 | Note that we use a `.dockerignore` file to ignore certain files/folders. 30 | 31 | ## 3. Run the Docker image 32 | 33 | Normal: 34 | 35 | ```console 36 | $ docker run -p 80:80 fastapi-image 37 | ``` 38 | 39 | Run in background and give a name: 40 | 41 | ```console 42 | $ docker run -d --name myfastapicontainer -p 80:80 fastapi-image 43 | ``` 44 | 45 | `-p 80:80`: Map the port from outside to the port from the container 46 | 47 | Host in Dockerfile must be: 48 | 49 | `host: 0.0.0.0`: "placeholder", it tells a server to listen for and accept connections from any IP address ("all IPv4 addresses on the local machine"). 50 | -------------------------------------------------------------------------------- /example2/app/main.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import FastAPI 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.get("/") 9 | def read_root(): 10 | return {"Hello": "World"} 11 | 12 | 13 | @app.get("/items/{item_id}") 14 | def read_item(item_id: int, q: Optional[str] = None): 15 | return {"item_id": item_id, "q": q} 16 | -------------------------------------------------------------------------------- /example2/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | asgiref==3.5.0 3 | certifi==2021.10.8 4 | charset-normalizer==2.0.12 5 | click==8.0.4 6 | fastapi==0.75.0 7 | h11==0.13.0 8 | idna==3.3 9 | pydantic==1.9.0 10 | requests==2.27.1 11 | sniffio==1.2.0 12 | soupsieve==2.3.1 13 | starlette==0.17.1 14 | typing_extensions==4.1.1 15 | urllib3==1.26.9 16 | uvicorn==0.17.6 17 | --------------------------------------------------------------------------------