Skip to content

Commit a3b2d2f

Browse files
authored
Merge pull request #147 from tecladocode/jose/cou-323-add-information-on-docker-compose-to-the-course
Add Docker Compose to the course!
2 parents f817f50 + 369e132 commit a3b2d2f

File tree

105 files changed

+2118
-277
lines changed

Some content is hidden

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

105 files changed

+2118
-277
lines changed
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"]
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
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
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).
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"]
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
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
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"]
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

docs/docs/11_deploy_to_render/05_environment_variables_and_migrations/README.md

+1-108
Original file line numberDiff line numberDiff line change
@@ -193,114 +193,7 @@ class UserModel(db.Model):
193193

194194
### Running our migration with string length changes
195195

196-
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.
197-
198-
The changes we want to make are to add `compare_type=True`[^alembic_docs] in both `context.configure()` calls:
199-
200-
```python title="migrations/env.py"
201-
from __future__ import with_statement
202-
203-
import logging
204-
from logging.config import fileConfig
205-
206-
from flask import current_app
207-
208-
from alembic import context
209-
210-
# this is the Alembic Config object, which provides
211-
# access to the values within the .ini file in use.
212-
config = context.config
213-
214-
# Interpret the config file for Python logging.
215-
# This line sets up loggers basically.
216-
fileConfig(config.config_file_name)
217-
logger = logging.getLogger('alembic.env')
218-
219-
# add your model's MetaData object here
220-
# for 'autogenerate' support
221-
# from myapp import mymodel
222-
# target_metadata = mymodel.Base.metadata
223-
config.set_main_option(
224-
'sqlalchemy.url',
225-
str(current_app.extensions['migrate'].db.get_engine().url).replace(
226-
'%', '%%'))
227-
target_metadata = current_app.extensions['migrate'].db.metadata
228-
229-
# other values from the config, defined by the needs of env.py,
230-
# can be acquired:
231-
# my_important_option = config.get_main_option("my_important_option")
232-
# ... etc.
233-
234-
235-
def run_migrations_offline():
236-
"""Run migrations in 'offline' mode.
237-
238-
This configures the context with just a URL
239-
and not an Engine, though an Engine is acceptable
240-
here as well. By skipping the Engine creation
241-
we don't even need a DBAPI to be available.
242-
243-
Calls to context.execute() here emit the given string to the
244-
script output.
245-
246-
"""
247-
url = config.get_main_option("sqlalchemy.url")
248-
context.configure(
249-
url=url,
250-
target_metadata=target_metadata,
251-
# highlight-start
252-
compare_type=True,
253-
# highlight-end
254-
literal_binds=True
255-
)
256-
257-
with context.begin_transaction():
258-
context.run_migrations()
259-
260-
261-
def run_migrations_online():
262-
"""Run migrations in 'online' mode.
263-
264-
In this scenario we need to create an Engine
265-
and associate a connection with the context.
266-
267-
"""
268-
269-
# this callback is used to prevent an auto-migration from being generated
270-
# when there are no changes to the schema
271-
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
272-
def process_revision_directives(context, revision, directives):
273-
if getattr(config.cmd_opts, 'autogenerate', False):
274-
script = directives[0]
275-
if script.upgrade_ops.is_empty():
276-
directives[:] = []
277-
logger.info('No changes in schema detected.')
278-
279-
connectable = current_app.extensions['migrate'].db.get_engine()
280-
281-
with connectable.connect() as connection:
282-
context.configure(
283-
connection=connection,
284-
target_metadata=target_metadata,
285-
process_revision_directives=process_revision_directives,
286-
# highlight-start
287-
compare_type=True,
288-
# highlight-end
289-
**current_app.extensions['migrate'].configure_args,
290-
)
291-
292-
with context.begin_transaction():
293-
context.run_migrations()
294-
295-
296-
if context.is_offline_mode():
297-
run_migrations_offline()
298-
else:
299-
run_migrations_online()
300-
301-
```
302-
303-
Next, let's create the new migration:
196+
Now we want to create a new migration so that our changes to the `UserModel` will be applied:
304197

305198
```bash
306199
flask db migrate

0 commit comments

Comments
 (0)