Skip to content

Commit fbc9a81

Browse files
committed
✨ feat(Docker compose): Add Docker compose lectures
One in Docker section to run the app on its own, and one when we introduce a PostgreSQL database to spin up both the app and db.
1 parent c4768ad commit fbc9a81

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2034
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Run the REST API using Docker Compose
2+
3+
Now that we've got a Docker container for our REST API, we can set up Docker Compose to run the container.
4+
5+
Docker Compose is most useful to start multiple Docker containers at the same time, specifying configuration values for them and dependencies between them.
6+
7+
Later on, I'll show you how to use Docker Compose to start both a PostgreSQL database and the REST API. For now, we'll use it only for the REST API, to simplify starting its container up.
8+
9+
If you have Docker Desktop installed, you already have Docker Compose. If you want to install Docker Compose in a system without Docker Desktop, please refer to the [official installation instructions](https://docs.docker.com/compose/install/).
10+
11+
## How to write a `docker-compose.yml` file
12+
13+
Create a file called `docker-compose.yml` in the root of your project (alongside your `Dockerfile`). Inside it, add the following contents:
14+
15+
```yaml
16+
version: '3'
17+
services:
18+
web:
19+
build: .
20+
ports:
21+
- "5000:5000"
22+
volumes:
23+
- .:/app
24+
```
25+
26+
This small file is all you need to tell Docker Compose that you have a service, called `web`, which is built using the current directory (by default, that looks for a file called `Dockerfile`).
27+
28+
Other settings provided are:
29+
30+
- `ports`, used to map a port in your local computer to one in the container. Since our container runs the Flask app on port 5000, we're targeting that port so that any traffic we access in port 5000 of our computer is sent to the container's port 5000.
31+
- `volumes`, to map a local directory into a directory within the container. This makes it so you don't have to rebuild the image each time you make a code change.
32+
33+
## How to run the Docker Compose services
34+
35+
Simply type:
36+
37+
```
38+
docker compose up
39+
```
40+
41+
And that will start all your services. For now, there's just one service, but later on when we add a database, this command will start everything.
42+
43+
When the services are running, you'll start seeing logs appear. These are the same logs as for running the `Dockerfile` on its own, but preceded by the service name.
44+
45+
In our case, we'll see `web-1 | ...` and the logs saying the service is running on `http://127.0.0.1:5000`. When you access that URL, you'll see the request logs printed in the console.
46+
47+
Congratulations, you've ran your first Docker Compose service!
48+
49+
## Rebuilding the Docker image
50+
51+
If you need to rebuild the Docker image of your REST API service for whatever reason (e.g. configuration changes), you can run:
52+
53+
```
54+
docker compose up --build --force-recreate --no-deps web
55+
```
56+
57+
More information [here](https://stackoverflow.com/a/50802581).
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.10
2+
EXPOSE 5000
3+
WORKDIR /app
4+
RUN pip install flask
5+
COPY . .
6+
CMD ["flask", "run", "--host", "0.0.0.0"]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from flask import Flask, request
2+
3+
app = Flask(__name__)
4+
5+
stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}]
6+
7+
8+
@app.get("/store")
9+
def get_stores():
10+
return {"stores": stores}
11+
12+
13+
@app.post("/store")
14+
def create_store():
15+
request_data = request.get_json()
16+
new_store = {"name": request_data["name"], "items": []}
17+
stores.append(new_store)
18+
return new_store, 201
19+
20+
21+
@app.post("/store/<string:name>/item")
22+
def create_item(name):
23+
request_data = request.get_json()
24+
for store in stores:
25+
if store["name"] == name:
26+
new_item = {"name": request_data["name"], "price": request_data["price"]}
27+
store["items"].append(new_item)
28+
return new_item, 201
29+
return {"message": "Store not found"}, 404
30+
31+
32+
@app.get("/store/<string:name>")
33+
def get_store(name):
34+
for store in stores:
35+
if store["name"] == name:
36+
return store
37+
return {"message": "Store not found"}, 404
38+
39+
40+
@app.get("/store/<string:name>/item")
41+
def get_item_in_store(name):
42+
for store in stores:
43+
if store["name"] == name:
44+
return {"items": store["items"]}
45+
return {"message": "Store not found"}, 404
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: '3'
2+
services:
3+
web:
4+
build: .
5+
ports:
6+
- "5000:5000"
7+
volumes:
8+
- .:/app
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.10
2+
EXPOSE 5000
3+
WORKDIR /app
4+
RUN pip install flask
5+
COPY . .
6+
CMD ["flask", "run", "--host", "0.0.0.0"]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from flask import Flask, request
2+
3+
app = Flask(__name__)
4+
5+
stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}]
6+
7+
8+
@app.get("/store")
9+
def get_stores():
10+
return {"stores": stores}
11+
12+
13+
@app.post("/store")
14+
def create_store():
15+
request_data = request.get_json()
16+
new_store = {"name": request_data["name"], "items": []}
17+
stores.append(new_store)
18+
return new_store, 201
19+
20+
21+
@app.post("/store/<string:name>/item")
22+
def create_item(name):
23+
request_data = request.get_json()
24+
for store in stores:
25+
if store["name"] == name:
26+
new_item = {"name": request_data["name"], "price": request_data["price"]}
27+
store["items"].append(new_item)
28+
return new_item, 201
29+
return {"message": "Store not found"}, 404
30+
31+
32+
@app.get("/store/<string:name>")
33+
def get_store(name):
34+
for store in stores:
35+
if store["name"] == name:
36+
return store
37+
return {"message": "Store not found"}, 404
38+
39+
40+
@app.get("/store/<string:name>/item")
41+
def get_item_in_store(name):
42+
for store in stores:
43+
if store["name"] == name:
44+
return {"items": store["items"]}
45+
return {"message": "Store not found"}, 404
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# How to run the app and database with Docker Compose
2+
3+
Up until now we've been running `docker-compose up` to start the REST API container.
4+
5+
Now let's modify our `docker-compose.yml` file to include spinning up a new PostgreSQL database.
6+
7+
```yaml
8+
version: '3'
9+
services:
10+
web:
11+
build: .
12+
ports:
13+
- "5000:80"
14+
depends_on:
15+
- db
16+
env_file:
17+
- ./.env
18+
volumes:
19+
- .:/app
20+
db:
21+
image: postgres
22+
environment:
23+
- POSTGRES_PASSWORD=password
24+
- POSTGRES_DB=myapp
25+
volumes:
26+
- postgres_data:/var/lib/postgresql/data
27+
volumes:
28+
postgres_data:
29+
```
30+
31+
The `postgres` image accepts various environment variables, among them:
32+
33+
- `POSTGRES_PASSWORD`, defaulting to `postgres`
34+
- `POSTGERS_DB`, defaulting to `postgres`
35+
- `POSTGRES_USER`, defaulting to `postgres`
36+
- `POSTGRES_HOST`, defaulting to `localhost`
37+
- `POSTGRES_PORT`, defaulting to `5432`
38+
39+
We should at least set a secure password. Above we're changing the password and database to `password` and `myapp` respectively.
40+
41+
:::caution
42+
Remember to also change your `DATABASE_URL` in your `.env` file that the REST API container is using. It should look like this:
43+
44+
```
45+
DATABASE_URL=postgresql://postgres:password@db/myapp
46+
```
47+
48+
When Docker Compose runs, it creates a virtual network[^1] which allows you to connect to `db`, which connects to the running `db` service container.
49+
:::
50+
51+
## Named volumes in Docker Compose
52+
53+
You'll notice that our `docker-compose.yml` file has these lines:
54+
55+
```
56+
volumes:
57+
- postgres_data:/var/lib/postgresql/data
58+
volumes:
59+
postgres_data:
60+
```
61+
62+
The bottom two lines define a named volume. This is data that will be stored by Docker and can be reused across container runs. We're calling it `postgres_data`, but it isn't assigned to anything there.
63+
64+
In the top two lines, which are part of the `db` service definition, we say that the `postgres_data` named volume is mapped to `/var/lib/postgresql/data` in the container.
65+
66+
`/var/lib/postgresql/data` is where the `postgres` image saves PostgreSQL data (such as databases, tables, etc). Therefore, as you create databases, tables, and store data, the named volume `postgres_data` will contain them.
67+
68+
When you restart the container (or even rebuilt it), you can use the same named volume to keep access to old data.
69+
70+
If you want to delete the entire database content, you can do so by deleting the volume through Docker Desktop, or with this command:
71+
72+
```
73+
docker compose down -v
74+
```
75+
76+
## Starting the whole system
77+
78+
Now you're ready to start the Docker Compose system! If you need to rebuild the REST API container first, run:
79+
80+
```
81+
docker compose up --build --force-recreate --no-deps web
82+
```
83+
84+
You'll get an error due to no database being available. That's OK, as long as the container is rebuilt!
85+
86+
Then press `CTRL+C` to stop it, and start the whole system with:
87+
88+
```
89+
docker compose up
90+
```
91+
92+
Now you can make a request to your API on port 5000, and it should work, storing the data in the database!
93+
94+
## Running the system in background mode
95+
96+
When we run the system with `docker compose up`, it takes up the terminal until we stop it with `CTRL+C`.
97+
98+
If you want to run it in "Daemon" mode, or in the background, so you can use the terminal for other things, you can use:
99+
100+
```
101+
docker compose up -d
102+
```
103+
104+
Then to stop the system, use:
105+
106+
```
107+
docker compose down
108+
```
109+
110+
Note you must be in the folder that contains your `docker-compose.yml` file in order to bring the system up or down.
111+
112+
:::warning
113+
Running `docker compose down` will **not** delete your named volumes. You need to use the `-v` flag for that. Deleting the named volumes deletes the data in them irreversibly.
114+
:::
115+
116+
[^1]: [Networking in Compose (official docs)](https://docs.docker.com/compose/networking/)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATABASE_URL=
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_APP=app
2+
FLASK_DEBUG=True
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.env
2+
.venv
3+
.vscode
4+
__pycache__
5+
data.db
6+
*.pyc
7+
.DS_Store
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# CONTRIBUTING
2+
3+
## How to run the Dockerfile locally
4+
5+
```
6+
docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run"
7+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.10
2+
WORKDIR /app
3+
COPY requirements.txt .
4+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
5+
COPY . .
6+
CMD ["/bin/bash", "docker-entrypoint.sh"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# REST APIs Recording Project
2+
3+
Nothing here yet!

0 commit comments

Comments
 (0)