Skip to content

Add Docker Compose to the course! #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
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/02_run_docker_container/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
45 changes: 45 additions & 0 deletions docs/docs/04_docker_intro/02_run_docker_container/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
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
Expand Up @@ -193,114 +193,7 @@ class UserModel(db.Model):

### Running our migration with string length changes

Now we want to create a new migration so that our changes to the `UserModel` will be applied. But because we're changing the length of a string column, we need to first make a modification to the Alembic configuration.

The changes we want to make are to add `compare_type=True`[^alembic_docs] in both `context.configure()` calls:

```python title="migrations/env.py"
from __future__ import with_statement

import logging
from logging.config import fileConfig

from flask import current_app

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline():
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
# highlight-start
compare_type=True,
# highlight-end
literal_binds=True
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""

# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')

connectable = current_app.extensions['migrate'].db.get_engine()

with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
# highlight-start
compare_type=True,
# highlight-end
**current_app.extensions['migrate'].configure_args,
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

```

Next, let's create the new migration:
Now we want to create a new migration so that our changes to the `UserModel` will be applied:

```bash
flask db migrate
Expand Down
Loading
Loading