Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
✨ 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.
  • Loading branch information
jslvtr committed Apr 4, 2024
commit fbc9a816b8f35cbbd65f2ee2484d4d4ed93d24ef
57 changes: 57 additions & 0 deletions docs/docs/04_docker_intro/04_run_with_docker_compose/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Run the REST API using Docker Compose

Now that we've got a Docker container for our REST API, we can set up Docker Compose to run the container.

Docker Compose is most useful to start multiple Docker containers at the same time, specifying configuration values for them and dependencies between them.

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.

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/).

## How to write a `docker-compose.yml` file

Create a file called `docker-compose.yml` in the root of your project (alongside your `Dockerfile`). Inside it, add the following contents:

```yaml
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/app
```

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`).

Other settings provided are:

- `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.
- `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.

## How to run the Docker Compose services

Simply type:

```
docker compose up
```

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.

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.

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.

Congratulations, you've ran your first Docker Compose service!

## Rebuilding the Docker image

If you need to rebuild the Docker image of your REST API service for whatever reason (e.g. configuration changes), you can run:

```
docker compose up --build --force-recreate --no-deps web
```

More information [here](https://stackoverflow.com/a/50802581).
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
45 changes: 45 additions & 0 deletions docs/docs/04_docker_intro/04_run_with_docker_compose/end/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from flask import Flask, request

app = Flask(__name__)

stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}]


@app.get("/store")
def get_stores():
return {"stores": stores}


@app.post("/store")
def create_store():
request_data = request.get_json()
new_store = {"name": request_data["name"], "items": []}
stores.append(new_store)
return new_store, 201


@app.post("/store/<string:name>/item")
def create_item(name):
request_data = request.get_json()
for store in stores:
if store["name"] == name:
new_item = {"name": request_data["name"], "price": request_data["price"]}
store["items"].append(new_item)
return new_item, 201
return {"message": "Store not found"}, 404


@app.get("/store/<string:name>")
def get_store(name):
for store in stores:
if store["name"] == name:
return store
return {"message": "Store not found"}, 404


@app.get("/store/<string:name>/item")
def get_item_in_store(name):
for store in stores:
if store["name"] == name:
return {"items": store["items"]}
return {"message": "Store not found"}, 404
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/app
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
45 changes: 45 additions & 0 deletions docs/docs/04_docker_intro/04_run_with_docker_compose/start/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from flask import Flask, request

app = Flask(__name__)

stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}]


@app.get("/store")
def get_stores():
return {"stores": stores}


@app.post("/store")
def create_store():
request_data = request.get_json()
new_store = {"name": request_data["name"], "items": []}
stores.append(new_store)
return new_store, 201


@app.post("/store/<string:name>/item")
def create_item(name):
request_data = request.get_json()
for store in stores:
if store["name"] == name:
new_item = {"name": request_data["name"], "price": request_data["price"]}
store["items"].append(new_item)
return new_item, 201
return {"message": "Store not found"}, 404


@app.get("/store/<string:name>")
def get_store(name):
for store in stores:
if store["name"] == name:
return store
return {"message": "Store not found"}, 404


@app.get("/store/<string:name>/item")
def get_item_in_store(name):
for store in stores:
if store["name"] == name:
return {"items": store["items"]}
return {"message": "Store not found"}, 404
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# How to run the app and database with Docker Compose

Up until now we've been running `docker-compose up` to start the REST API container.

Now let's modify our `docker-compose.yml` file to include spinning up a new PostgreSQL database.

```yaml
version: '3'
services:
web:
build: .
ports:
- "5000:80"
depends_on:
- db
env_file:
- ./.env
volumes:
- .:/app
db:
image: postgres
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```

The `postgres` image accepts various environment variables, among them:

- `POSTGRES_PASSWORD`, defaulting to `postgres`
- `POSTGERS_DB`, defaulting to `postgres`
- `POSTGRES_USER`, defaulting to `postgres`
- `POSTGRES_HOST`, defaulting to `localhost`
- `POSTGRES_PORT`, defaulting to `5432`

We should at least set a secure password. Above we're changing the password and database to `password` and `myapp` respectively.

:::caution
Remember to also change your `DATABASE_URL` in your `.env` file that the REST API container is using. It should look like this:

```
DATABASE_URL=postgresql://postgres:password@db/myapp
```

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.
:::

## Named volumes in Docker Compose

You'll notice that our `docker-compose.yml` file has these lines:

```
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```

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.

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.

`/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.

When you restart the container (or even rebuilt it), you can use the same named volume to keep access to old data.

If you want to delete the entire database content, you can do so by deleting the volume through Docker Desktop, or with this command:

```
docker compose down -v
```

## Starting the whole system

Now you're ready to start the Docker Compose system! If you need to rebuild the REST API container first, run:

```
docker compose up --build --force-recreate --no-deps web
```

You'll get an error due to no database being available. That's OK, as long as the container is rebuilt!

Then press `CTRL+C` to stop it, and start the whole system with:

```
docker compose up
```

Now you can make a request to your API on port 5000, and it should work, storing the data in the database!

## Running the system in background mode

When we run the system with `docker compose up`, it takes up the terminal until we stop it with `CTRL+C`.

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:

```
docker compose up -d
```

Then to stop the system, use:

```
docker compose down
```

Note you must be in the folder that contains your `docker-compose.yml` file in order to bring the system up or down.

:::warning
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.
:::

[^1]: [Networking in Compose (official docs)](https://docs.docker.com/compose/networking/)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL=
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FLASK_APP=app
FLASK_DEBUG=True
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.env
.venv
.vscode
__pycache__
data.db
*.pyc
.DS_Store
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CONTRIBUTING

## How to run the Dockerfile locally

```
docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.10
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["/bin/bash", "docker-entrypoint.sh"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# REST APIs Recording Project

Nothing here yet!
Loading