# Workshop: Counter WebApp
WARNING
- Please prepare one folder for this Workshop
# Start Coding
# project structure
+-- webapp
| +-- src
| | +-- mongo_connector.py
| | +-- main.py
| +-- Dockerfile
+-- docker-compose.yml
1
2
3
4
5
6
2
3
4
5
6
- It is possible for one project would have more than one container. Therefore, I decided to create a new folder for WebApp container. Moverover, I decided to have another folder (src) for source code in WebApp container.
- However, you can define your own project structure by yourself. This is what I want to recommand this style for you.
./webapp/Dockerfile
FROM python:3.9.7-buster
WORKDIR /home/src
RUN pip install "fastapi==0.70.0"
RUN pip install uvicorn[standard]
RUN pip install "pymongo==3.12.0"
RUN pip install "mypy==0.910"
EXPOSE 8000
CMD uvicorn --host 0.0.0.0 main:app --forwarded-allow-ips '*' --reload 1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- CMD command from FastAPI (opens new window)
docker-compose.yml
version: "3"
services:
api:
build:
context: ./webapp
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./webapp/src:/home/src
mongo:
image: mongo:3.6.22-xenial
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: 1234
volumes:
- mongo-sad-lab6:/data/db
volumes:
mongo-sad-lab6:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- version: It is version of Docker-compose, not Docker engine. Please do not be confused. You can check this link (opens new window).
- services: We will define our container(s) here.
- api: This is HOSTNAME / SERVICE NAME for this container. You can name it anything.
build: To tell docker-compose how to build this container.
- context: Where is your Dockerfile?
- dockerfile: What is your Dockerfile name?
- PS. Actually you can name your own Dockerfile. For example,
- Dockerfile.dev for dev env
- Dockerfile.prod for production env
- Dockerfile.{something}
- {NAME} or uses another name that you want but it would be quite weird.
- PS. Actually you can name your own Dockerfile. For example,
ports: -p in docker run
volumes: -v in docker run
- mongo: This is HOSTNAME for my MongoDB.
- image: Since we can use their image. No need to build, just docker pull and docker run
- environment: -e in docker run. They are environment variable (opens new window).It likes export in some labs.
export VARIABLE=value1
- api: This is HOSTNAME / SERVICE NAME for this container. You can name it anything.
- volumes: If you have volume object please define it too. Docker compose will show error if they cannot find volume object.
./webapp/src/mongo_connector.py pymongo (opens new window)
import pymongo #type:ignore
from pymongo.mongo_client import MongoClient #type:ignore
import os
class Mongo:
username="root"
password=1234
hostname="mongo"
uri=f"mongodb://{username}:{password}@{hostname}:27017"
client:MongoClient
@staticmethod
def init():
if('MONGO_STRING_OPTIONS' in os.environ):
Mongo.uri+=os.environ['MONGO_STRING_OPTIONS']
temp=pymongo.MongoClient(Mongo.uri)
Mongo.client=temp
@staticmethod
def get_instance():
if not hasattr(Mongo,'client'):
Mongo.init()
return Mongo.client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- When we use Docker compose, you can use their HOSTNAME/SERVICE NAME for connecting with database instead of using IP address.
- PS. If you do not use Docker compose (ex. docker run for each container), you need to join them by yourself.
- PS2. You might see that we put secret password in our code and docker-compose.yml which are not good right? Hacker loves it. However, I will show a better way about how to keep our secret at the end of this lab.
./webapp/src/main.py FastAPI (opens new window)
from fastapi import FastAPI
from mongo_connector import Mongo
app = FastAPI()
@app.get("/")
def read_root():
data = Mongo.get_instance()['my-db']['my-collection'].find_one({"name":"counter"},{'_id':0})
return data
@app.get("/count")
def add_count():
data = Mongo.get_instance()['my-db']['my-collection'].update_one({"name":"counter"},{"$inc":{"value":1}})
return {"result":data.modified_count}
def init_obj():
data = [ d for d in Mongo.get_instance()['my-db']['my-collection'].find({"name":"counter"})]
if(not data):
new_data = {
"name":"counter",
"value": 0
}
Mongo.get_instance()['my-db']['my-collection'].insert_one(new_data)
init_obj()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# It's time! Let's create our containers.
docker-compose CLI (opens new window)
docker-compose up --build -d
1
- --build: make sure that docker-compose will build image again (before starts container(s) ).
- -d: Detached mode: Run containers in the background
# Let's see the result
docker ps
1
output
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba881e4f4208 mongo:3.6.22-xenial "docker-entrypoint.s…" 7 seconds ago Up 5 seconds 27017/tcp 006-counter_mongo_1
a987ca89fea0 006-counter_api "/bin/sh -c 'uvicorn…" 7 seconds ago Up 5 seconds 0.0.0.0:8000->8000/tcp 006-counter_api_1
1
2
3
2
3
docker-compose ps
1
output
NAME COMMAND SERVICE STATUS PORTS
006-counter_api_1 "/bin/sh -c 'uvicorn…" api running 0.0.0.0:8000->8000/tcp
006-counter_mongo_1 "docker-entrypoint.s…" mongo running 27017/tcp
1
2
3
2
3
If you use docker-compose ps, it would filter only containers for this project. However, if you use docker ps, you might see other container which is outside your project too.
go http://localhost:8000 (opens new window) to see the result
output
{
"name": "counter",
"value": 0
}
1
2
3
4
2
3
4
output
{
"result": 1
}
1
2
3
2
3
- go back http://localhost:8000 (opens new window) to see the result
output
{
"name": "counter",
"value": 1
}
1
2
3
4
2
3
4
# Optional command
- Basically, you can use another commands of docker via docker-compose
# Get inside container
docker-compose exec api bash
1
- If you use docker-compose not need -it.
ls
1
output
__pycache__ main.py mongo_connector.py
1
- don't forget to exit from container
exit
1
# looks at Logs
docker-compose logs -f api
1
output
api_1 | INFO: Will watch for changes in these directories: ['/home/src']
api_1 | INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
api_1 | INFO: Started reloader process [8] using watchgod
api_1 | INFO: Started server process [10]
api_1 | INFO: Waiting for application startup.
api_1 | INFO: Application startup complete.
api_1 | INFO: 172.28.0.1:39842 - "GET / HTTP/1.1" 200 OK
api_1 | INFO: 172.28.0.1:39862 - "GET /count HTTP/1.1" 200 OK
api_1 | INFO: 172.28.0.1:39870 - "GET / HTTP/1.1" 200 OK
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- ctrl+c for exit from logs -f
# Remove container and finish this lab
docker-compose down
1
output
[+] Running 3/3
- Container 006-counter_mongo_1 Removed 0.6s
- Container 006-counter_api_1 Removed 11.3s
- Network 006-counter_default Removed 0.2s
1
2
3
4
2
3
4
# How to manage secret
WARNING
- You should copy/paste previous workshop and refactor it with this tutorial.
# First way: ENV file
- Idea: Basically, we put it into another file and do not put it into your Git!
- However, .env is not the best solution because Docker does not do anything about encryption.
- Use case of env file
- configurate, setting, change some variables without re-build Image again.
# project structure
+-- webapp
| +-- src
| | +-- mongo_connector.py
| | +-- main.py
| +-- Dockerfile
+-- docker-compose.yml
+-- secret.env
1
2
3
4
5
6
7
2
3
4
5
6
7
secret.env
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=1234
1
2
2
docker-compose.yml
version: "3"
services:
api:
build:
context: ./webapp
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./webapp/src:/home/src
env_file:
- ./secret.env
mongo:
image: mongo:3.6.22-xenial
env_file:
- ./secret.env
volumes:
- mongo-sad-lab6:/data/db
volumes:
mongo-sad-lab6:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mongo_connector.py
import pymongo #type:ignore
from pymongo.mongo_client import MongoClient #type:ignore
import os
class Mongo:
username=os.environ['MONGO_INITDB_ROOT_USERNAME']
password=os.environ['MONGO_INITDB_ROOT_PASSWORD']
hostname="mongo"
uri=f"mongodb://{username}:{password}@{hostname}:27017"
client:MongoClient
@staticmethod
def init():
if('MONGO_STRING_OPTIONS' in os.environ):
Mongo.uri+=os.environ['MONGO_STRING_OPTIONS']
temp=pymongo.MongoClient(Mongo.uri)
Mongo.client=temp
@staticmethod
def get_instance():
if not hasattr(Mongo,'client'):
Mongo.init()
return Mongo.client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Second way: Docker Secrets (opens new window)
WARNING
- You should copy/paste previous workshop and refactor it with this tutorial AGAIN.
- Docker Secrets: Your secret(s) will be encrypted and sent to /run/secrets/{secret_name} in container(s).
# project structure
+-- webapp
| +-- src
| | +-- mongo_connector.py
| | +-- main.py
| +-- Dockerfile
+-- docker-compose.yml
+-- mongo_user.txt
+-- mongo_password.txt
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
mongo_user.txt
root
1
mongo_password.txt
1234
1
docker-compose.yml
version: "3"
services:
api:
build:
context: ./webapp
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./webapp/src:/home/src
secrets:
- mongo_user
- mongo_password
mongo:
image: mongo:3.6.22-xenial
secrets:
- mongo_user
- mongo_password
environment:
- MONGO_INITDB_ROOT_USERNAME_FILE=/run/secrets/mongo_user
- MONGO_INITDB_ROOT_PASSWORD_FILE=/run/secrets/mongo_password
volumes:
- mongo-sad-lab6:/data/db
volumes:
mongo-sad-lab6:
secrets:
mongo_user:
file: mongo_user.txt
mongo_password:
file: mongo_password.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
mongo_connector.py
import pymongo #type:ignore
from pymongo.mongo_client import MongoClient #type:ignore
import os
def read_mongo_user()->str:
with open("/run/secrets/mongo_user") as read_file:
return read_file.read()
def read_mongo_password()->str:
with open("/run/secrets/mongo_password") as read_file:
return read_file.read()
class Mongo:
username=read_mongo_user()
password=read_mongo_password()
hostname="mongo"
uri=f"mongodb://{username}:{password}@{hostname}:27017"
client:MongoClient
@staticmethod
def init():
if('MONGO_STRING_OPTIONS' in os.environ):
Mongo.uri+=os.environ['MONGO_STRING_OPTIONS']
temp=pymongo.MongoClient(Mongo.uri)
Mongo.client=temp
@staticmethod
def get_instance():
if not hasattr(Mongo,'client'):
Mongo.init()
return Mongo.client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31