Containerization basics

pull/2/head
Dmitry Ignatiev 12 months ago committed by Vladimir Protsenko
parent eb9abfc2c9
commit 9409f4c57e

@ -0,0 +1,5 @@
{
"extends": [
"development"
]
}

@ -1,30 +0,0 @@
from flask import Flask, render_template
import os
import random
app = Flask(__name__)
# list of cat images
images = [
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F0.gif?alt=media&token=0fff4b31-b3d8-44fb-be39-723f040e57fb",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F1.gif?alt=media&token=2328c855-572f-4a10-af8c-23a6e1db574c",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F10.gif?alt=media&token=647fd422-c8d1-4879-af3e-fea695da79b2",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F11.gif?alt=media&token=900cce1f-55c0-4e02-80c6-ee587d1e9b6e",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F2.gif?alt=media&token=8a108bd4-8dfc-4dbc-9b8c-0db0e626f65b",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F3.gif?alt=media&token=4e270d85-0be3-4048-99bd-696ece8070ea",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F4.gif?alt=media&token=e7daf297-e615-4dfc-aa19-bee959204774",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F5.gif?alt=media&token=a8e472e6-94da-45f9-aab8-d51ec499e5ed",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F7.gif?alt=media&token=9e449089-9f94-4002-a92a-3e44c6bd18a9",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F8.gif?alt=media&token=80a48714-7aaa-45fa-a36b-a7653dc3292b",
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F9.gif?alt=media&token=a57a1c71-a8af-4170-8fee-bfe11809f0b3",
]
@app.route("/")
def index():
url = random.choice(images)
return render_template("index.html", url=url)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))

@ -1,27 +0,0 @@
<html>
<head>
<style type="text/css">
body {
background: black;
color: white;
}
div.container {
max-width: 500px;
margin: 100px auto;
border: 20px solid white;
padding: 10px;
text-align: center;
}
h4 {
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="container">
<h4>Cat Gif of the day</h4>
<img src="{{url}}" />
<p><small>Courtesy: <a href="http://www.buzzfeed.com/copyranter/the-best-cat-gif-post-in-the-history-of-cat-gifs">Buzzfeed</a></small></p>
</div>
</body>
</html>

@ -0,0 +1,33 @@
from flask import Flask, render_template, make_response, send_file
import os
import random
app = Flask(__name__)
req_count = 0
static_dir = os.path.join(os.path.dirname(__file__), 'static/')
@app.route('/static/<path:path>')
def static_file(path):
return send_file(os.path.join(static_dir, path))
@app.route("/")
def index():
global req_count
cat_files = os.listdir(os.path.join(static_dir, 'img/'))
file = random.choice(cat_files)
is_stoner = os.path.basename(file) == 'stoner_cat.gif'
url = '/static/img/' + file
req_count += 1
return render_template("index.html", url=url, is_stoner=is_stoner)
@app.route("/request_count")
def request_count():
response = make_response(str(req_count), 200)
response.mimetype = "text/plain"
return response
if __name__ == "__main__":
app.config['TEMPLATES_AUTO_RELOAD'] = True
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))

@ -0,0 +1,3 @@
#!/bin/sh
pip install -r ./requirements.txt

@ -0,0 +1,51 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
#gzip on;
keepalive_timeout 65;
server {
listen 80;
location / {
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffers 8 64k;
proxy_busy_buffers_size 128k;
proxy_buffer_size 64k;
client_max_body_size 10m;
proxy_http_version 1.1;
proxy_pass http://cats_app:5000;
}
location /request_count {
deny all;
return 404;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

@ -0,0 +1,77 @@
<!DOCTYPE HTML>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cats</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<style lang="text/css">
.footer {
position: fixed;
bottom: 0;
}
.stoner-video {
position: fixed;
left: 0;
top: 0;
min-width: 100%;
min-height: 100%;
}
.stoner-wave {
animation: wave 5s cubic-bezier(0.36, 0.45, 0.63, 0.53) infinite;
animation-direction: alternate;
}
@keyframes wave {
0% {
transform: rotate(-20deg);
background-color: greenyellow;
}
100% {
transform: rotate(20deg);
background-color: fuchsia;
}
}
</style>
</head>
<body class="h-100">
{% if is_stoner %}
<video autoplay loop class="stoner-video">
<source src="/static/vid/stoner_cat.mp4" type="video/mp4" />
</video>
{% endif %}
<div class="h-100 d-flex flex-row align-items-center">
<div class="container">
<div class="row p-3 justify-content-center">
<div class="col col-lg-4">
<div class="card {{ 'stoner-wave' if is_stoner }}">
<img src="{{url}}" class="card-img-top" alt="cat">
<div class="card-body">
<button id="#next-button" class="btn btn-primary {{ 'bg-black' if is_stoner }}"
onclick="location.reload()">
<img src="/static/icons/icon-paw-32.png" alt="paw" />
Gimme another cat!
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<footer class="footer p-1">
<span>Icons by <a href="https://icons8.com">icons8.com</a></span>
</footer>
</body>
</html>

@ -1,14 +0,0 @@
#!/bin/bash
# build the flask container
docker build -t studX/foodtrucks-web ./
# create the network
docker network create foodtrucks-network
# start the ES container, specify password beforehand
docker run -d --net foodtrucks-network --name elastic -e ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS elasticsearch:8.4.3
# start the flask app container
# point to elastic host: https://elastic:9200 user: elastic password: v7SLsbtXticPLADei5vS
docker run -d -p 80:5000 --net foodtrucks-network studX/foodtrucks-web https elastic 9200 elastic v7SLsbtXticPLADei5vS

@ -1,13 +0,0 @@
version: "3"
services:
elastic:
image: elasticsearch:8.4.3
environment:
- ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS
foodtrucks-web:
image: stud15/foodtrucks-web
command: https elastic 9200 elastic v7SLsbtXticPLADei5vS
depends_on:
- elastic
ports:
- 80:5000

@ -1,135 +0,0 @@
from elasticsearch import Elasticsearch, exceptions
import os
import time
from flask import Flask, jsonify, request, render_template
import sys
import requests
es_scheme = sys.argv[1]
es_host = sys.argv[2]
es_port = int(sys.argv[3])
es_user = sys.argv[4]
es_password = sys.argv[5]
connection_string = f"{es_scheme}://{es_host}:{es_port}"
print(f"Elastic server: {es_scheme}://{es_host}:{es_port}, auth: {es_user}:{es_password}")
es = Elasticsearch(
connection_string,
basic_auth=(es_user, es_password),
verify_certs=False
)
app = Flask(__name__)
def load_data_in_es():
""" creates an index in elasticsearch """
url = "http://data.sfgov.org/resource/rqzj-sfat.json"
r = requests.get(url)
data = r.json()
print("Loading data in elasticsearch ...")
for id, truck in enumerate(data):
res = es.index(index="sfdata", id=id, body=truck)
print("Total trucks loaded: ", len(data))
def safe_check_index(index, retry=60):
""" connect to ES with retry """
if not retry:
print("Out of retries. Bailing out...")
sys.exit(1)
try:
status = es.indices.exists(index=index)
return status
except exceptions.ConnectionError as e:
print(f"Unable to connect to {connection_string}. Retrying in 5 secs...", flush=True)
time.sleep(5)
safe_check_index(index, retry-1)
def format_fooditems(string):
items = [x.strip().lower() for x in string.split(":")]
return items[1:] if items[0].find("cold truck") > -1 else items
def check_and_load_index():
""" checks if index exits and loads the data accordingly """
if not safe_check_index('sfdata'):
print("Index not found...")
load_data_in_es()
###########
### APP ###
###########
@app.route('/')
def index():
return render_template('index.html')
@app.route('/debug')
def test_es():
resp = {}
try:
msg = es.cat.indices()
resp["msg"] = msg
resp["status"] = "success"
except:
resp["status"] = "failure"
resp["msg"] = "Unable to reach ES"
return jsonify(resp)
@app.route('/search')
def search():
key = request.args.get('q')
if not key:
return jsonify({
"status": "failure",
"msg": "Please provide a query"
})
try:
res = es.search(
index="sfdata",
body={
"query": {"match": {"fooditems": key}},
"size": 750 # max document size
})
except Exception as e:
return jsonify({
"status": "failure",
"msg": "error in reaching elasticsearch"
})
# filtering results
vendors = set([x["_source"]["applicant"] for x in res["hits"]["hits"]])
temp = {v: [] for v in vendors}
fooditems = {v: "" for v in vendors}
for r in res["hits"]["hits"]:
applicant = r["_source"]["applicant"]
if "location" in r["_source"]:
truck = {
"hours" : r["_source"].get("dayshours", "NA"),
"schedule" : r["_source"].get("schedule", "NA"),
"address" : r["_source"].get("address", "NA"),
"location" : r["_source"]["location"]
}
fooditems[applicant] = r["_source"]["fooditems"]
temp[applicant].append(truck)
# building up results
results = {"trucks": []}
for v in temp:
results["trucks"].append({
"name": v,
"fooditems": format_fooditems(fooditems[v]),
"branches": temp[v],
"drinks": fooditems[v].find("COLD TRUCK") > -1
})
hits = len(results["trucks"])
locations = sum([len(r["branches"]) for r in results["trucks"]])
return jsonify({
"trucks": results["trucks"],
"hits": hits,
"locations": locations,
"status": "success"
})
if __name__ == "__main__":
ENVIRONMENT_DEBUG = os.environ.get("DEBUG", False)
check_and_load_index()
app.run(host='0.0.0.0', port=5000, debug=ENVIRONMENT_DEBUG)

@ -1,26 +0,0 @@
{
"name": "sf-food",
"version": "0.0.1",
"description": "SF food app",
"main": "index.js",
"scripts": {
"start": "webpack --progress --colors --watch",
"build": "NODE_ENV='production' webpack -p",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Prakhar Srivastav",
"license": "MIT",
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"superagent": "^5.2.2"
},
"devDependencies": {
"babel-core": "^6.3.26",
"babel-loader": "^6.2.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"webpack": "^1.12.9"
}
}

@ -1,3 +0,0 @@
elasticsearch===8.4.3
Flask==2.1.0
requests==2.23.0

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,41 +0,0 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

@ -1,50 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import Sidebar from "./components/Sidebar";
// setting up mapbox
mapboxgl.accessToken =
"pk.eyJ1IjoicHJha2hhciIsImEiOiJjaWZlbzQ1M2I3Nmt2cnhrbnlxcTQyN3VkIn0.uOaUAUqN2VS7dC7XKS0KkQ";
var map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/prakhar/cij2cpsn1004p8ykqqir34jm8",
center: [-122.44, 37.77],
zoom: 12,
});
ReactDOM.render(<Sidebar map={map} />, document.getElementById("sidebar"));
function formatHTMLforMarker(props) {
var { name, hours, address } = props;
var html =
'<div class="marker-title">' +
name +
"</div>" +
"<h4>Operating Hours</h4>" +
"<span>" +
hours +
"</span>" +
"<h4>Address</h4>" +
"<span>" +
address +
"</span>";
return html;
}
// setup popup display on the marker
map.on("click", function (e) {
var features = map.queryRenderedFeatures(
e.point,
{ layers: ['trucks', 'trucks-highlight'], radius: 10, includeGeometry: true }
);
if (!features.length) return;
var feature = features[0];
new mapboxgl.Popup()
.setLngLat(feature.geometry.coordinates)
.setHTML(formatHTMLforMarker(feature.properties))
.addTo(map);
});

@ -1,36 +0,0 @@
import React from "react";
export default function Intro() {
return (
<div className="intro">
<h3>About</h3>
<p>
This is a fun application built to accompany the{" "}
<a href="http://prakhar.me/docker-curriculum">docker curriculum</a> - a
comprehensive tutorial on getting started with Docker targeted
especially at beginners.
</p>
<p>
The app is built with Flask on the backend and Elasticsearch is the
engine powering the search.
</p>
<p>
The frontend is hand-crafted with React and the beautiful maps are
courtesy of Mapbox.
</p>
<p>
If you find the design a bit ostentatious, blame{" "}
<a href="http://genius.com/Justin-bieber-baby-lyrics">Genius</a> for
giving me the idea of using this color scheme. If you love it, I smugly
take all the credit. (¯)
</p>
<p>
Lastly, the data for the food trucks is made available in public domain
by{" "}
<a href="https://data.sfgov.org/Economy-and-Community/Mobile-Food-Facility-Permit/rqzj-sfat">
SF Data
</a>
</p>
</div>
);
}

@ -1,205 +0,0 @@
import React from "react";
import request from "superagent";
import Intro from "./Intro";
import Vendor from "./Vendor";
class Sidebar extends React.Component {
constructor(props) {
super(props);
this.state = {
results: [],
query: "",
firstLoad: true,
};
this.onChange = this.onChange.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleHover = this.handleHover.bind(this);
}
fetchResults() {
request.get("/search?q=" + this.state.query).end((err, res) => {
if (err) {
alert("error in fetching response");
} else {
this.setState({
results: res.body,
firstLoad: false,
});
this.plotOnMap();
}
});
}
generateGeoJSON(markers) {
return {
type: "FeatureCollection",
features: markers.map((p) => ({
type: "Feature",
properties: {
name: p.name,
hours: p.hours,
address: p.address,
"point-color": "253,237,57,1",
},
geometry: {
type: "Point",
coordinates: [
parseFloat(p.location.longitude),
parseFloat(p.location.latitude),
],
},
})),
};
}
plotOnMap(vendor) {
const map = this.props.map;
const results = this.state.results;
const markers = [].concat.apply(
[],
results.trucks.map((t) =>
t.branches.map((b) => ({
location: b.location,
name: t.name,
schedule: b.schedule,
hours: b.hours,
address: b.address,
}))
)
);
var highlightMarkers, usualMarkers, usualgeoJSON, highlightgeoJSON;
if (vendor) {
highlightMarkers = markers.filter(
(m) => m.name.toLowerCase() === vendor.toLowerCase()
);
usualMarkers = markers.filter(
(m) => m.name.toLowerCase() !== vendor.toLowerCase()
);
} else {
usualMarkers = markers;
}
usualgeoJSON = this.generateGeoJSON(usualMarkers);
if (highlightMarkers) {
highlightgeoJSON = this.generateGeoJSON(highlightMarkers);
}
// clearing layers
if (map.getLayer("trucks")) {
map.removeLayer("trucks");
}
if (map.getSource("trucks")) {
map.removeSource("trucks");
}
if (map.getLayer("trucks-highlight")) {
map.removeLayer("trucks-highlight");
}
if (map.getSource("trucks-highlight")) {
map.removeSource("trucks-highlight");
}
map
.addSource("trucks", {
type: "geojson",
data: usualgeoJSON,
})
.addLayer({
id: "trucks",
type: "circle",
interactive: true,
source: "trucks",
paint: {
"circle-radius": 8,
"circle-color": "rgba(253,237,57,1)",
},
});
if (highlightMarkers) {
map
.addSource("trucks-highlight", {
type: "geojson",
data: highlightgeoJSON,
})
.addLayer({
id: "trucks-highlight",
type: "circle",
interactive: true,
source: "trucks-highlight",
paint: {
"circle-radius": 8,
"circle-color": "rgba(164,65,99,1)",
},
});
}
}
handleSearch(e) {
e.preventDefault();
this.fetchResults();
}
onChange(e) {
this.setState({ query: e.target.value });
}
handleHover(vendorName) {
this.plotOnMap(vendorName);
}
render() {
if (this.state.firstLoad) {
return (
<div>
<div id="search-area">
<form onSubmit={this.handleSearch}>
<input
type="text"
value={this.state.query}
onChange={this.onChange}
placeholder="Burgers, Tacos or Wraps?"
/>
<button>Search!</button>
</form>
</div>
<Intro />
</div>
);
}
const query = this.state.query;
const resultsCount = this.state.results.hits || 0;
const locationsCount = this.state.results.locations || 0;
const results = this.state.results.trucks || [];
const renderedResults = results.map((r, i) => (
<Vendor key={i} data={r} handleHover={this.handleHover} />
));
return (
<div>
<div id="search-area">
<form onSubmit={this.handleSearch}>
<input
type="text"
value={query}
onChange={this.onChange}
placeholder="Burgers, Tacos or Wraps?"
/>
<button>Search!</button>
</form>
</div>
{resultsCount > 0 ? (
<div id="results-area">
<h5>
Found <span className="highlight">{resultsCount}</span> vendors in{" "}
<span className="highlight">{locationsCount}</span> different
locations
</h5>
<ul> {renderedResults} </ul>
</div>
) : null}
</div>
);
}
}
export default Sidebar;

@ -1,70 +0,0 @@
import React from "react";
export default class Vendor extends React.Component {
constructor(props) {
super(props);
this.state = {
isExpanded: false,
};
this.toggleExpand = this.toggleExpand.bind(this);
}
formatFoodItems(items) {
if (this.state.isExpanded) {
return items.join(", ");
}
const summary = items.join(", ").substr(0, 80);
if (summary.length > 70) {
const indexOfLastSpace =
summary.split("").reverse().join("").indexOf(",") + 1;
return summary.substr(0, 80 - indexOfLastSpace) + " & more...";
}
return summary;
}
toggleExpand() {
this.setState({
isExpanded: !this.state.isExpanded,
});
}
render() {
const { name, branches, fooditems, drinks } = this.props.data;
const servesDrinks = (
<div className="row">
<div className="icons">
{" "}
<i className="ion-wineglass"></i>{" "}
</div>
<div className="content">Serves Cold Drinks</div>
</div>
);
return (
<li
onMouseEnter={this.props.handleHover.bind(null, name)}
onClick={this.toggleExpand}
>
<p className="truck-name">{name}</p>
<div className="row">
<div className="icons">
{" "}
<i className="ion-android-pin"></i>{" "}
</div>
<div className="content"> {branches.length} locations </div>
</div>
{drinks ? servesDrinks : null}
<div className="row">
<div className="icons">
{" "}
<i className="ion-fork"></i> <i className="ion-spoon"></i>
</div>
<div className="content">
Serves {this.formatFoodItems(fooditems)}
</div>
</div>
</li>
);
}
}

@ -1,202 +0,0 @@
html,
body {
padding: 0;
color: #aaa;
box-sizing: border-box;
font-family: "Titillium Web", sans-serif;
margin: 0;
}
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
i {
color: #3a3a3a;
}
div.intro a,
div.intro a:visited {
color: #fded39;
}
div.intro {
padding: 10px;
}
div.intro h3 {
color: #fded39;
text-transform: uppercase;
}
h1 i {
color: black;
text-shadow: none;
font-size: 21px;
}
textarea,
input,
button {
outline: none;
}
.container {
height: 90vh;
display: flex;
}
#map {
flex: 3;
}
#sidebar {
background: #1a1a1a;
border-left: 1px solid #444444;
width: 320px;
flex: 1;
overflow-y: scroll;
}
#sidebar div#results-area {
overflow-y: scroll;
}
div#heading {
background: #fded39;
margin: 0;
height: 10vh;
text-align: center;
}
div#heading h1 {
font-size: 25px;
text-transform: uppercase;
text-shadow: -2px 2px black;
margin: 0;
color: #fded39;
}
div#heading p {
color: black;
font-style: italic;
font-size: 12px;
margin: 0;
}
div#search-area {
padding: 10px;
border-bottom: 1px solid #444444;
}
div#search-area input,
div#search-area button {
padding: 10px 8px;
border: none;
}
div#search-area input {
width: 195px;
}
div#search-area button {
background: #fded39;
font-weight: 600;
text-transform: uppercase;
}
div#results-area {
padding: 10px;
}
div#results-area h5 {
font-weight: 200;
font-style: italic;
margin: 0;
color: #ddd;
}
div#results-area h5 span.highlight {
color: #fded39;
font-weight: 600;
}
div#results-area ul {
padding: 0;
list-style-type: none;
}
div#results-area ul li {
border: 1px solid #444;
padding: 5px 10px;
margin-bottom: 10px;
cursor: pointer;
}
div#results-area ul li:hover {
border: 1px solid #fded39;
}
div#results-area ul li p {
margin: 0;
font-size: 14px;
}
div#results-area ul li p.truck-name {
color: #fded39;
text-transform: uppercase;
margin: 0;
font-size: 16px;
margin-bottom: 5px;
}
div#results-area ul li div.row {
display: flex;
flex-direction: row;
}
div#results-area ul li div.row div.icons {
flex-shrink: 0;
width: 16px;
}
div#results-area ul li div.row div.content {
margin-left: 8px;
}
.mapboxgl-popup-content {
background: black;
font-family: "Titillium Web", sans-serif;
}
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
border-top-color: black;
}
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
border-bottom-color: black;
}
.mapboxgl-popup-close-button {
color: white;
}
.mapboxgl-popup-content .marker-title {
color: #fded39;
text-transform: uppercase;
font-size: 14px;
}
.mapboxgl-popup-content h4 {
margin: 0;
margin-top: 10px;
}

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title>SF Food Trucks</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<link href='https://fonts.googleapis.com/css?family=Titillium+Web:400,700' rel='stylesheet' type='text/css'>
<script src='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.css' rel='stylesheet' />
<link href='/static/styles/main.css' rel='stylesheet' />
<link rel="stylesheet" href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" />
<link rel="apple-touch-icon" sizes="57x57" href="/static/icons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/static/icons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/static/icons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/icons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/static/icons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/icons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/static/icons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/icons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/icons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/icons//android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/static/icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16x16.png">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<!-- awesome svg octocat thanks to http://tholman.com/ -->
<a href="https://github.com/prakhar1989/FoodTrucks/" class="github-corner" title="Fork me on Github">
<svg width="72" height="72" viewBox="0 0 250 250" style="fill:#000; color:#FDED39; position: absolute; top: 0; border: 0; left: 0; transform: scale(-1, 1);">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
<div id="heading">
<h1>SF F <i class="ion-pizza"></i> <i class="ion-icecream"></i> d Trucks</h1>
<p>San Francisco's finger-licking street food now at your fingertips.</p>
</div>
<div class="container">
<div id='map'></div>
<div id="sidebar"> </div>
</div>
<script src='/static/build/main.js'></script>
</body>
</html>

@ -1,19 +0,0 @@
module.exports = {
cache: true,
entry: './static/src/app.js',
output: {
filename: './static/build/main.js'
},
devtool: 'source-map',
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
},
]
}
};

@ -1,46 +0,0 @@
import json
import requests
def getData(url):
r = requests.get(url)
return r.json()
def convertData(data, msymbol="restaurant", msize="medium"):
data_dict = []
for d in data:
if d.get('longitude') and d.get("latitude"):
data_dict.append({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [float(d["longitude"]),
float(d["latitude"])]
},
"properties": {
"name": d.get("applicant", ""),
"marker-symbol": msymbol,
"marker-size": msize,
"marker-color": "#CC0033",
"fooditems": d.get('fooditems', ""),
"address": d.get("address", "")
}
})
return data_dict
def writeToFile(data, filename="data.geojson"):
template = {
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
},
},
"features": data }
with open(filename, "w") as f:
json.dump(template, f, indent=2)
print "Geojson generated"
if __name__ == "__main__":
data = getData("http://data.sfgov.org/resource/rqzj-sfat.json")
writeToFile(convertData(data[:350]), filename="trucks.geojson")

@ -69,8 +69,18 @@ Docker Hub - это общедоступная служба реестра, по
В вашей компании вы можете поднять собственный реестр на основе одного из общедоступных образов реестров. При выборе образа реестра для производственной среды нужно учесть ряд требований к хранилищу данных, к аутентификации и авторизации, и к требованиям выдвигаемым другими задача обслуживания.
### 5. Volumes
### 5. Механизмы обеспечения персистентности в Docker
Docker реализует три механизма обеспечения постоянства хранения данных - тома данных, tmpfs и монтирование внешней директории.
Тома данных(volumes) - механизм Docker для обеспечения постоянного хранилища данных в контейнерах и разделения хранилища между ними.
Второй механизм - это хранилище `tmpfs`, хранилища такого типа находятся в оперативной памяти хост-системы, и их нельзя разделять между контейнерами.
Docker также поддерживает возможность монтирования локальной директории в контейнер.
Все эти возможности, кроме обеспечения персистентности при хранении данных, также обеспечивают более высокую производительность при записи в файловую \
систему контейнера, по сравнению с верхним слоем `UnionFS`, доступным на запись и чтение.
## Релевантные источники
- Nemeth E. et al. UNIX and Linux system administration handbook. Chapter 25.

@ -3,9 +3,16 @@
1. Чем контейнеризация отличается от виртуализации?
2. Что такое образ контейнера?
3. С помощью какого параметра можно пробросить порт внутрь контейнера?
4. Как запустить контейнер в режиме демона?
4. Как запустить контейнер отвязав его от терминала?
5. Как вывести список работающих контейнеров?
6. Как посмотреть логи контейнера?
8. Какие механизмы ядра используются Docker?
9. Какой демон процесс отвечает за работу контейнеров и общение с реестром Docker?
10. Какой конфигурационный файл используется для построения образа?
7. Какие механизмы ядра используются Docker?
8. Что такое реестр контейнеров и для чего он используется?
9. Какой файл используется для построения образа?
10. Чем отличаются CMD и ENTRYPOINT?
11. И в CMD и в ENTRYPOINT можно задавать параметры через `["command", "arg1", ...]` или `command arg1 ...`. В чем отличие?
12. Какие есть способы добавить файлы в файловую систему контейнера?
13. Какие есть способы оставить файл в файловой системе контейнера, даже после его остановки, удаления и перезапуска?
14. Чем отличаются `docker export` и `docker save`?
15. Какие виды сетей поддерживает Docker?
16. Как удалить все неиспользуемые образы, контейнеры и сети из Docker?

@ -177,3 +177,275 @@ sudo docker diff hello_with_file
````bash
sudo docker exec hello_with_file rm /hello.txt
````
### 3. Volumes
#### 1.
Для монтирования директории:
````bash
docker run --mount type=bind,source=/home/stud,target=/home/stud -it busybox
````
Внутри контейнера
````bash
ls /home/stud
````
#### 2.
````bash
docker volume create my-volume
````
Для просмотра свойств:
````bash
docker volume inspect my-volume
````
#### 3.
Вывод всех томов:
````bash
docker voume ls
````
#### 4.
````bash
docker run -it --mount type=volume,source=my-volume,target=/data busybox
````
#### 5.
Внутри контейнера:
````bash
echo 'Hello, world!' > /data/hello.txt
````
Потом - остановить контейнер, далее сделать например `docker container prune`, повторить запуск как на \
предыдущем шаге, и внутри контейнера сделать:
````bash
cat /data/hello.txt
````
#### 6.
````bash
docker container prune -f && docker volume prune -a -f
````
### 4. Сеть
#### 1.
````bash
docker network create -d bridge --subnet=172.168.0.0/16 --gateway=172.168.0.1 my-network
````
#### 2.
Просмотр всех сетей:
````bash
docker network ls
````
Просмотр свойств:
```bash
docker network inspect my-network
```
#### 3.
````bash
docker run --name cats_app --mount type=bind,source=./cats_app,target=/app --network my-network -it python:3.12 /bin/bash
````
#### 4.
Внутри контейнера:
````bash
cd /app && ./install.sh && ./run.sh
````
#### 5.
````bash
docker run --network my-network --publish 8080:80 --mount type=bind,source=./cats_app/nginx.conf,target=/etc/nginx/nginx.conf nginx
````
#### 6.
````bash
docker run --name cats_app --mount type=bind,source=./cats_app,target=/app --network my-network -p 127.0.0.1:5001:5000 -it python:3.12 /bin/bash
````
Внутри контейнера:
````bash
cd /app && ./install.sh && ./run.sh
````
#### 7.
````bash
docker run --mount type=bind,source=./cats_app,target=/app --network host -it python:3.12 /bin/bash -c 'cd /app && ./install.sh && ./run.sh'
````
### 5. Работа с образами
#### 1.
````Dockerfile
from busybox
entrypoint /bin/echo 'Hello, World!'
````
#### 2.
В директории с Dockerfile:
````bash
docker build -t hello .
````
#### 3.
````bash
docker image ls
````
Для просмотра свойств:
````bash
docker image inspect hello
````
#### 4.
````bash
docker run hello
````
#### 5.
````Dockerfile
from busybox
env name 'World'
entrypoint /bin/echo "Hello, $world!"
````
В директории с Dockerfile:
````bash
docker build -t hello . && docker run -e name=Student hello
````
Здесь будет интересно если студенты наткнутся на разницу в
````Dockerfile
entrypoint /bin/echo "Hello, $world!"
````
и
````Dockerfile
entrypoint ["/bin/echo", "Hello, $world!"]
````
#### 6.
````bash
docker tag hello hello:1.0.0
````
#### 7.
Сохранение:
````bash
docker save hello -o hello.tar
````
Удаление:
````bash
docker rmi -f $(docker images -q -f 'reference=hello:*')
````
Восстановление:
````bash
docker load -i hello.tar
````
#### 8.
Предположим что Dockerfile находится в этой же директории, а исходники \
приложения лежат в `./cats_app`
````Dockerfile
from python:3.12
copy ./cats_app /app
workdir /app
run ./install.sh
entrypoint ["python3", "app.py"]
````
Создание образа:
````bash
docker build -t cats_app .
````
Запуск:
````bash
docker run -p 5000:5000 cats_app
````
9.
````bash
docker run --name registry --detach -p 127.0.0.1:12345:5000 --mount type=bind,source=./registry,target=/var/lib/registry registry:2
````
Инструкции для более сложного сетапа можно посмотреть тут:
https://medium.com/@ManagedKube/docker-registry-2-setup-with-tls-basic-auth-and-persistent-data-8b98a2a73eec
10.
Создание тегов для локального реестра:
````bash
docker tag hello localhost:12345/hello
docker tag hello:1.0.0 localhost:12345/hello:1.0.0
docker tag cats_app localhost:12345/cats_app
````
Загрузка образов:
````bash
docker push -a localhost:12345/hello
docker push -a localhost:12345/cats_app
````
Удаление из локальной инсталляции докера:
````bash
docker rmi -f $(docker images -q -f 'reference=*/hello:*')
docker rmi -f $(docker images -q -f 'reference=hello:*')
docker rmi -f $(docker images -q -f 'reference=*/cats_app:*')
docker rmi -f $(docker images -q -f 'reference=cats_app:*')
````
Запуск:
````bash
docker run -p 8080:5000 localhost:12345/cats_app
````
````bash
docker run localhost:12345/hello
````

@ -48,6 +48,91 @@
### 3. Volumes
1. Запустите контейнер с busybox в интерактивном режиме и примонтируйте к нему какую-либо локальную директорию, \
например домашнюю директорию пользователя. \
Просмотрите содержимое директории с помощью, например, команды `ls`.
2. Создайте именованный том данных(volume).
3. Выведите список всех томов данных. Убедитесь что созданный вами том существует. Просмотрите свойства своего тома.
4. Запустите контейнер с busybox, при этом подключив к нему созданный вами именованный том.
5. Создайте в директории тома, внутри контейнера, какой-либо файл. Теперь остановите контейнер, удалите его и запустите снова,\
опять же примонтировав к нему созданный вами том. Убедитесь что файл находится на месте.
6. Остановите контейнеры и удалите все неиспользуемые тома данных.
### 4. Сеть
### 5. Создание образов
1. Создайте именованную сеть Docker типа `bridge`, адреса которой находятся в диапазоне `172.168.0.0/16` и \
шлюз которой находится по адресу `172.168.0.1`.
2. Выведите список всех существующих сетей Docker. Просмотрите параметры вашей сети, убедитесь что она создана правильно.
3. В полученной вами директории `cats_app` находится веб-приложение на Python. \
Запустите контейнер с именем `cats_app` из образа `python:3.12` в интерактивном режиме, примонтировав к нему \
директорию с этим приложением, так чтобы внутри образа директория была доступна как `/app`, и также \
чтобы команда, запускающаяся при старте образа была `/bin/bash`, чтобы вам таким образом был доступен шелл, \
и также, чтобы контейнер был подключен к созданной вами сети.
4. В директории `/app` внутри контейнера находятся два скрипта - `install.sh` и `run.sh`,
которые скачивают зависимости приложения, и запускают приложение соответственно. \
Сделайте `cd` в эту директорию и последовательно вызовите сначала `install.sh` а затем `run.sh`.
5. Приложение недоступно извне. Для его запуска необходимо запустить обратный прокси в этой же сети. \
В полученной вами директории `cats_app` находится файл конфигурации `nginx`. \
В отдельном терминале, запустите контейнер из образа `nginx`, подключив его к созданной вами сети, и пробросив наружу \
порт 80(на локальный порт 8080, к примеру), при этом примонтировав упомянутый файл конфигурации \
на место файла `/etc/nginx/nginx.conf` внутри контейнера.
6. Приложение отслеживает количество запросов, эту статистику можно получить через URL `/request_count`. \
Но nginx настроен таким образом, чтобы URL `/request_count` не был доступен извне. \
Перезапустите контейнер `cats_app` и приложение в нем так, чтобы этот метод был доступен на порту 5001, \
и только с локальной машины, на которой запущен Docker. \
Подсказка: само приложение внутри контейнера работает на порту 5000.
7. Запустите контейнер `cats_app`, так чтобы оно было доступно из `host` сети безо всяких ограничений. \
N.B.: это работает только на Linux, и не поддерживается, например в Docker Desktop для Windows или Mac.
### 5. Работа с образами
1. Напишите Dockerfile на основе busybox, который печает `Hello, World` при запуске.
2. Соберите образ из полученного Dockerfile, и присвойте ему имя `hello`.
3. Выведите список всех созданных и загруженных образов и убедитесь что среди них есть ваш образ. \
Выведите на экран его свойства и просмотрите их.
4. Запустите контейнер созданный из вашего образа, и убедитесь что он печатает `Hello, World` при запуске.
5. Перепишите Dockerfile так, чтобы на печать в результате запуска контейнера вызывалось бы `Hello, $name`, \
при этом чтобы значение `$name` бралось из переменной среды, и по умолчанию оно было бы равно `World`. \
Снова соберите образ `hello`. \
Запустите контейнер, при этом установив при запуске переменную `name` в значение, например, `Student`. \
Убедитесь, что контейнер напечатает `Hello, Student`.
6. Добавьте тэг к вашему образу, например тэг `hello:1.0.0`.
7. Сохраните образ `hello` и все его тэги в tar-архив. Удалите все теги этого образа из Docker. \
Восстановите образы из tar-архива. Убедитесь что все работает.
8. Приложение `cats_app` хорошо подходит для контейнеризации. \
Напишите для него докер-файл, отталкиваясь от образа `python:3.12`. \
Этап установки зависимостей, т.е. запуск скрипта `install.sh` должен происходить \
при сборке образа, а не при его запуске. \
Внутри контейнера, приложение должно находиться в директории `/app`.
Создайте из написанного Dockerfile образ с именем `cats_app`. \
Запустите контейнер на основе этого образа, разрешив доступ к нему по порту 5000. \
Убедитесь, что приложение работает.
9. Запустите контейнер из образа `registry:2`, примонтировав куда-либо директорию контейнера \
`/var/lib/registry` чтобы использовать ее как постоянное хранилище, и открыв порт 5000 для этого контейнера, \
перенаправив его на порт 1235 для локальной машины(перенаправьте именно для локальной машины \
дабы реестр контейнеров не был доступен извне). \
10. Теперь у вас на машине, а именно на `localhost:12345`, запущен Docker Registry, и вы можете сохранять образы туда. \
Создайте для образов `hello` и `cats_app` соответствующие этому реестру теги. \
Загрузите туда образы этих приложений, включая все их теги. \
Удалите их из Docker. \
Теперь загрузите их из registry, проверьте что все работает.

@ -1,905 +0,0 @@
# Задания
## 1. Установка Docker
Далее приведены инструкции с https://docs.docker.com/engine/install/debian/. Используйте виртуальную машину `studX.myoffice.ru`.
Настройте репозиторий Docker
```
$ sudo apt-get update
$ sudo apt-get install ca-certificates curl gnupg lsb-release
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```
Проверьте, что в файле `/etc/apt/sources.list.d/docker.list` строка репозитория правильная, соответствует вашему дистрибутиву (bullseye в примере ниже)
```
deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bullseye stable
```
Установите Docker
```
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
```
Проверьте, всё ли установлено корректно:
```
$ sudo docker run hello-world
Hello from Docker.
This message shows that your installation appears to be working correctly.
```
## 2. Играем с BusyBox
Теперь, когда всё подготовлено, пора приняться за дело. В этом разделе нашей целью будет запуск контейнера Busybox и освоение команды docker run.
Для начала, запустите следующую команду:
```
$ sudo docker pull busybox
```
**Внимание.** В зависимости от того, как вы установили Docker, вы можете увидеть сообщение permission denied (доступ запрещён) в ответ на вызов выше приведённой команды. Если вы на Mac, убедитесь, что Docker движок запущен. Если на Линукс, вам может потребоваться повысить права доступа с помощью команды `sudo`. В качестве альтернативного варианта вы можете добавить пользователя в Docker группу для решения этой проблемы.
Команда `pull` скачивает образ `busybox` из Docker реестра и сохраняет его в систему. Вы можете использовать команду `docker images` для вывода в консоль списка образов находящихся в вашей системе.
```
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest 00f017a8c2a6 2 weeks ago 1.11 MB
hello-world latest 48b5124b2768 2 months ago 1.84 kB
```
### 1.1 Запуск Docker
Великолепно! Теперь перейдём к запуску контейнера на основе этого образа. Для этого мы воспользуемся всемогущей командой `docker run`.
```
$ sudo docker run busybox
```
Постойте, но ничего не произошло! Это баг? Ну, нет. Под капотом произошло много всего. Когда вы запустили команду `run`, клиент Docker нашёл образ (в нашем случае, `busybox`), загрузил контейнер и запустил команду внутри этого контейнера. Мы не указали никаких аргументов, так что контейнер загрузился, выполнил команду `sh` и процесс контейнера завершился. Ну, да, как-то обидно. Попробуем сделать что-нибудь поинтереснее.
```
$ sudo docker run busybox echo "hello from busybox"
hello from busybox
```
Ура, наконец-то что-то вывелось. Теперь Docker запустил команду `echo` внутри контейнера, а затем вышел из него. Вы, наверное, заметили, что всё произошло очень быстро. А теперь представьте себе процесс загрузки виртуальной машины, выполнения в ней команды и её выключения. Ясно, почему говорят, что контейнеры быстрые! Чтобы узнать время выполнения попробуйте запустить последнюю команду со словом `time` в начале.
Давайте взглянем на команду `docker ps`. Она выводит на экран список всех запущенных контейнеров.
```
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
В силу того, что ни один контейнер не запущен, выводится пустая строка. Попробуем более информативный вариант `docker ps -a`:
```
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
56fdebcf3df0 busybox "echo hi" About a minute ago Exited (0) About a minute ago jovial_wozniak
5f585bdd9545 busybox "echo hi" About a minute ago Exited (0) About a minute ago focused_golick
ad64717b0d60 busybox "sh" 8 minutes ago Exited (0) 8 minutes ago determined_hugle
c73ceb428f23 hello-world "/hello" 25 minutes ago Exited (0) 25 minutes ago sad_mestorf
```
То, что мы видим в выдаче — список всех контейнеров, которые были запущены ранее. Обратите внимание, что колонка STATUS показывает, что эти контейнеры остановились несколько минут назад.
Наверное вы думаете, существует ли способ запустить более одной команды в контейнере. Давайте попробуем:
```
$ sudo docker run -it busybox sh
/ # ls
bin dev etc home proc root sys tmp usr var
/ # uptime
05:45:21 up 5:58, 0 users, load average: 0.00, 0.01, 0.04
```
Выполнение команды `run` с флагами `-it` подключает нас к интерактивному терминалу tty в контейнере. Теперь мы можем запустить столько команд, сколько захотим. Уделите немного времени запуску ваших любимых команд в этой консоли.
**Опасная зона.** Если вы любите рисковать, вы можете попробовать выполнить команду `rm -rf bin`. Убедитесь, что выполняете команду в контейнере, а не в основной операционной системе. Удалив данной командой папку `bin` не даст возможности запускать команды как `ls`, `echo`. После того, как всё перестанет работать, вы можете выйти из контейнера (выполним команду `exit`), а затем запустить контейнер заново `docker run -it busybox sh`. Так как Docker каждый раз создаёт новый контейнер, папка `bin` и команды должны быть опять доступны.
Создание слоя файловой системы и запуск контейнера могут быть разделены. Проделайте аналогичный выше пример, но с командами `create`, `start`. Запуск команды `docker create -it busybox sh` создаст слой файловой системы контейнера. Затем командой `docker start -ia <id_контейнера>` вы можете запустить выполнение контейнера в интерактивном режиме. Проанализируйте, что произошло при повторном запуске.
На этом тур по возможностям команды `docker run` завершён. Скорее всего, вы будете использовать эту команду довольно часто. Так что важно, чтобы мы поняли как с ней обращаться. Чтобы узнать больше о run, используйте `docker run --help`, и увидите полный список поддерживаемых флагов. Скоро мы увидим еще несколько способов использования `docker run`.
Давайте вкратце рассмотрим удаление контейнеров. Мы видели выше, что с помощью команды `docker ps -a` всё ещё можно увидеть остатки завершённых контейнеров. На протяжении этого занятия, вы будете запускать `docker run` несколько раз, и оставшиеся, покинутые контейнеры будут съедать дисковое пространство. Так что если они больше вам не понадобятся, вы можете взять за правило удалять контейнеры после завершения работы с ними. Для этого используется команда `docker rm`. Просто скопируйте ID (можно несколько) из вывода выше и передайте параметрами в команду.
```
$ sudo docker rm 305297d7a235 ff0a5c3750b9
305297d7a235
ff0a5c3750b9
```
При удалении идентификаторы будут снова выведены на экран. Если нужно удалить много контейнеров, то вместо ручного копирования и вставки можно сделать так:
```
$ sudo docker rm $(docker ps -a -q -f status=exited)
```
Эта команда удаляет все контейнеры, у которых статус `exited`. Флаг `-q` возвращает только численные ID, а флаг `-f` фильтрует вывод на основе предоставленных условий. Последняя полезная деталь — команде docker run можно передать флаг `--rm`, тогда контейнер будет автоматически удаляться при завершении. Это очень удобно для разовых запусков и экспериментов с Docker.
По образу и подобию можно удалять ненужные образы командой `docker rmi`.
### 1.2 Терминология
В предыдущем разделе мы использовали много специфичного для Docker жаргона, и многих это может запутать. Перед тем, как продолжать, давайте разберем некоторые термины, которые часто используются в экосистеме Docker.
**Image (образ)** - файловая система и параметры, с которыми будет произведён запуск. Образ не содержит изменяемого состояния и никогда не изменяется. В примере выше мы использовали команду docker pull чтобы скачать образ busybox.
**Container (контейнер)** - Создаётся на основе образа и запускает само приложение. Мы создали контейнер командой docker run, и использовали образ busybox, скачанный ранее. Список запущенных контейнеров можно увидеть с помощью команды docker ps.
**Docker Daemon (Docker демон)** - Фоновый сервис, запущенный в хост операционной системе, который отвечает за создание, запуск и уничтожение Docker контейнеров. Демон — это процесс, который запущен в операционной системе, с которой взаимодействует клиент.
**Docker Client (Docker клиент)** - Утилита командной строки, которая позволяет пользователю взаимодействовать с демоном. Существуют другие формы клиента, например, Kitematic, с графическим интерфейсом.
**Docker Hub** - Реестр Docker образов. Грубо говоря, архив всех доступных образов. Если нужно, то можно содержать собственный реестр и использовать его для получения образов.
## 2 Веб-приложения в Docker
Супер! Мы научились работать с `docker run`, поиграли с несколькими контейнерами и разобрались в терминологии. Вооруженные этими знаниями, мы готовы переходить к реальным задачам: разворачиванию веб-приложений с Docker!
### 2.1 Статичный сайт
Давайте начнем с малого. Вначале рассмотрим самый простой статический веб-сайт на nginx. Скачаем образ из Docker Hub, запустим контейнер и посмотрим, насколько легко будет запустить веб-сервер.
Поехали. Для одностраничного сайта нам понадобится заранее созданный образ и размещённый в реестре - `nginx:latest`. Можно запустить образ напрямую командой `docker run`.
```
$ sudo docker run nginx:latest
```
Так как образа не существует локально, клиент сначала скачает образ из реестра, а потом запустит его. Если всё пройдёт без проблем, то вы увидите в терминале сообщения о запуске nginx. Теперь сервер запущен. Как увидеть сайт в действии? На каком порту работает сервер? И, что самое важное, как напрямую достучаться до контейнера из хост системы?
В нашем случае клиент не открывает никакие порты, так что нужно будет перезапустить команду `docker run` чтобы сделать порты публичными. Заодно давайте сделаем так, чтобы терминал не был прикреплен к запущенному контейнеру. В таком случае можно будет спокойно закрыть терминал, а контейнер продолжит работу. Этот режим называется `detached`.
```
$ sudo docker run -d -P --name static-site nginx:latest
e61d12292d69556eabe2a44c16cbd54486b2527e2ce4f95438e504afb7b02810
```
Флаг `-d` открепит (`--detach`) терминал, флаг `-P` сделает все открытые порты публичными и случайными, и, наконец, флаг `--name` это имя, которое мы хотим дать контейнеру. Теперь можно увидеть порты с помощью команды `docker port [CONTAINER]`.
```
$ sudo docker port static-site
80/tcp -> 0.0.0.0:49153
80/tcp -> :::49153
```
Вы можете открыть http://localhost:49153 в своём браузере, создав предварительно туннель `ssh -L 49153:localhost:49153 studX.myoffice.ru` или протестировать сайт командой `curl`.
Вы также можете назначить свой порт, на который Docker клиент будет перенаправлять запросы на соединение к контейнеру.
```
$ sudo docker run -p 8888:80 --name static-site nginx:latest
```
Ключ `-p` устанавливает соответствие между портом хост операционной системы (8888) с портом контейнера (80).
Чтобы остановить контейнер запустите `docker stop` и укажите идентификатор (ID) контейнера.
Согласитесь, все было очень просто. Теперь, когда вы увидели, как запускать контейнеризованный веб-сервер, вам, наверное, интересно — а как создать свой Docker образ? Следующий раздел посвящён этой теме.
### 2.2 Docker образы
Мы касались образов ранее, но в этом разделе мы заглянем глубже: что такое Docker образы и как создавать собственные образы. Наконец, мы используем собственный образ чтобы запустить приложение и показать друзьям. Круто? Круто! Давайте начнем.
Образы это основы для контейнеров. В прошлом примере мы скачали (команда `pull`) образ под названием Busybox из регистра, и попросили клиент Docker запустить контейнер, основанный на этом образе. Чтобы увидеть список доступных локально образов, используйте команду `docker images`.
```
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest bc01a3326866 4 days ago 1.24MB
nginx latest 76c69feac34e 5 days ago 142MB
debian latest 43d28810c1b4 6 weeks ago 124MB
ubuntu latest 2dc39ba059dc 8 weeks ago 77.8MB
hello-world latest feb5d9fea6a5 13 months ago 13.3kB
```
Это список образов, скачанных из реестра, а также тех, что сделаны самостоятельно (скоро увидим, как это делать). TAG — это конкретный снимок (snapshot) образа, а IMAGE ID — это соответствующий уникальный идентификатор образа.
Для простоты, можно относиться к образу как к git-репозиторию. Образы можно коммитить с изменениями, и можно иметь несколько версий. Если не указывать конкретную версию, то клиент по умолчанию использует latest. Например, можно скачать определенную версию образа ubuntu:
```
$ docker pull ubuntu:12.04
```
Чтобы получить новый Docker образ, можно скачать его из реестра (такого, как Docker Hub) или создать собственный. На Docker Hub есть десятки тысяч образов. Поиск среди них доступен как на сайте, так и из командной строки с помощью `docker search`.
Важно понимать разницу между базовыми и дочерними образами.
**Base image (базовый образ)** — это образ, который не имеет родительского образа. Обычно это образы с операционной системой, такие как ubuntu, busybox или debian.
**Child image (дочерний образ)** — это образ, построенный на базовых образах и обладающий дополнительной функциональностью.
Существуют официальные и пользовательские образы, и любые из них могут быть базовыми и дочерними.
**Официальные образы** — это образы, которые официально поддерживаются командой Docker. Обычно в их названии одно слово. В списке выше `python`, `ubuntu`, `busybox` и `hello-world` — базовые образы.
**Пользовательские образы** — образы, созданные простыми пользователями вроде нас. Они построены на базовых образах. Обычно, они называются по формату `user/image-name`.
### 2.3 Наш первый образ
Теперь, когда мы лучше понимаем, что такое образы и какие они бывают, самое время создать собственный образ. Цель этого раздела — создать образ с простым приложением на Flask. Для этого задания подготовлено маленькое приложение `cats-app`, которое выводит случайную картинку с кошкой. Склонируйте этот репозиторий к себе на локальную машину `git clone <адрес-репозитория>` и перейдите в папку с приложением.
Следующим шагом является создание образа с данным веб-приложением. Как говорилось выше, все пользовательские образы базируются на базовых образах. Так как приложение написано на Python, базовый образ следует выбрать с предустановленным Python 3. Точнее мы собираемся использовать `python:3.8` версию python образа.
Теперь у нас есть все ингредиенты для создания собственных образов - работающее веб-приложение и базовый образ. Как мы будем подходить к этой задаче? Ответ — `Dockerfile`.
### 2.4 Dockerfile
**Dockerfile** — это простой текстовый файл, в котором содержится список команд Docker клиента. Это простой способ автоматизировать процесс создания образа. Самое классное, что команды в Dockerfile почти идентичны своим аналогам в Linux. Это значит, что в принципе не нужно изучать новый синтаксис, чтобы начать работать с докер-файлами.
В директории с приложением, нам нужно его создать. Создайте пустой файл в любимом текстовом редакторе, и сохраните его в той же директории `cats-app`. Назовите файл Dockerfile.
Для начала укажем базовый образ. Для этого нужно использовать ключевое слово `FROM`.
```Dockerfile
FROM python:3.8
```
Следующим шагом обычно указывают команды для копирования файлов из текущей папки в файловую систему образа и установки зависимостей.
```Dockerfile
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
```
Дальше нам нужно указать порт, который следует открыть. Наше приложение работает на порту 5000, поэтому укажем его:
```Dockerfile
EXPOSE 5000
```
Последний шаг — указать команду по-умолчанию для запуска приложения. Это просто `python ./app.py`. Для этого используем команду `CMD`:
```Dockerfile
CMD ["python", "./app.py"]
```
Главное предназначение `CMD` — это сообщить контейнеру какие команды нужно выполнить при старте. Теперь наш `Dockerfile` готов. Вот как он выглядит:
```Dockerfile
FROM python:3.8
# set a directory for the app
WORKDIR /usr/src/app
# copy all the files to the container
COPY . .
# install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# tell the port number the container should expose
EXPOSE 5000
# run the command
CMD ["python", "./app.py"]
```
Теперь можно создать образ. Команда `docker build` занимается сложной задачей создания образа на основе `Dockerfile`.
Листинг ниже демонстрирует процесс. Перед тем, как запустите команду сами (не забудьте путь к `cats-app` в конце), проверьте, чтобы там был ваш `имя_пользователя`. Имя пользователя должено соответствовать тому, что использовалось при регистрации на Docker Hub. Если вы используете частный реестр, то к тэгу добавляется в начало доменное имя хоста и опциональный порт, например
```Dockerfile
# -t доменное_имя_реестра:порт/имя_пользователя/имя_образа:тэг_образа
-t myregistryhost:5000/fedora/httpd:version1.0
```
Команда `docker build` довольно проста: она принимает опциональный тег с флагом `-t имя_пользователя/имя_образа` и путь до директории, в которой лежит `Dockerfile`.
```
$ docker build -t studX/catsapp ./
Sending build context to Docker daemon 8.704 kB
Step 1 : FROM python:3-onbuild
# Executing 3 build triggers...
Step 1 : COPY requirements.txt /usr/src/app/
---> Using cache
Step 1 : RUN pip install --no-cache-dir -r requirements.txt
---> Using cache
Step 1 : COPY . /usr/src/app
---> 1d61f639ef9e
Removing intermediate container 4de6ddf5528c
Step 2 : EXPOSE 5000
---> Running in 12cfcf6d67ee
---> f423c2f179d1
Removing intermediate container 12cfcf6d67ee
Step 3 : CMD python ./app.py
---> Running in f01401a5ace9
---> 13e87ed1fbc2
Removing intermediate container f01401a5ace9
Successfully built 13e87ed1fbc2
Successfully tagged studX/catsapp:latest
```
Если у вас нет образа `python:3.8`, то клиент сначала скачает его, а потом возьмётся за создание вашего образа. Так что, вывод на экран может отличаться от приведённого выше. Если всё прошло хорошо, то образ готов! Запустите `docker images` и увидите свой образ в списке.
Последний шаг — запустить образ и проверить его работоспособность:
```
$ sudo docker run -p 80:5000 --name catsapp studX/catsapp
* Serving Flask app 'app' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.3:5000
Press CTRL+C to quit
```
Зайдите на http://studX.myoffice.ru и увидите приложение в работе.
Поздравляем! Вы успешно создали свой первый Docker образ!
## 3 Многоконтейнерные окружения
В прошлом заданиях мы увидели, как легко и просто запускать приложения с помощью Docker. Мы начали с простого статического сайта, а потом запустили Flask-приложение. Оба варианта можно было запускать локально или в облаке, несколькими командами. И то, и другое приложения работали в одном контейнере.
Современные приложения не такие простые. Как правило всегда используется база данных или другой тип постоянного хранилища. Системы Redis и Memcached стали практически обязательной частью архитектуры веб-приложения. Поэтому, в этом разделе мы научимся контейнеризировать приложения, которым требуется несколько запущенных сервисов.
В частности, мы увидим, как запускать и управлять многоконтейнерными Docker-окружениями. Почему нужно несколько контейнеров, спросите вы? Ну, одна из главных идей Docker в том, что он предоставляет изоляцию. Идея совмещения процесса и его зависимостей в одной песочнице (называемой контейнером) и делает Docker мощным инструментом.
Когда монолитное приложение становится слишком сложным для поддержки в распределённой среде мудрым решением является произвести его декомпозицию на компоненты-сервисы. Хорошей идеей является содержание сервисов в отдельных контейнерах. Разным компонентам скорее всего потребуются разные ресурсы, и необходимость в новых ресурсах может возникать в разной степени. Помещая компоненты в отдельные контейнеры, мы можем выделять наиболее подходящий тип ресурсов для каждой части приложения. Это также созвучно со всем микросервисным движением. Это одна из причин, по которой Docker (и любая другая технология контейнеризации) находится на передовой современных микросервисных архитектур.
### 3.1 Приложение для поиска фургонов c едой в Сан-Франциско
Приложение, которое мы переведём в Docker, называется `foodtrucks-app`. Приложение создавалось с целью сделать что-то похожее на реально эксплуатируемое приложение и приносещее пользу, но не слишком сложное.
Серверная часть написана на Python (Flask framework), а для поиска используется Elasticsearch. Как и всё остальное в этом обучении, код проекта находится на Github. Мы используем это приложение, чтобы научиться запускать и разворачивать многоконтейнерное окружение.
Код проекта вы можете найти в папке с заданием.
Теперь, когда вы воодушевлены (будем надеятся), давайте подумаем, как будет выглядеть этот процесс. В нашем приложении есть бэкенд на Flask и сервис Elasticsearch. Естественным образом можно разделить приложение на два контейнера: один для Flask, другой для Elasticsearch (ES). Если приложение станет популярным, мы сможем масштабировать приложение добавлением новых контейнеров для тех компонентов, которые станут узким местом.
Отлично, значит нам нужно два контейнера. Это не сложно, правда? Мы уже создавали Flask-контейнер в прошлый раз. А для Elasticsearch... давайте посмотрим, есть ли что-нибудь в репозитории. Зайдём на сайт реестра Docker https://hub.docker.com/ и наберём имя в поиске.
Не удивительно, что существует официальный образ для Elasticsearch. Чтобы запустит ES, нужно всего лишь выполнить `docker run`, и вскоре у нас будет локальный, работающий контейнер с одним узлом ES. Некоторые компании считают плохой практикой выкладывать образы с тегом `latest`, в том числе Elastic, поэтому выберем и укажеи тег явно.
**Замечание.** Если контейнер не запускается, попробуйте подключиться в интерактивном режиме (с ключом `-it`) и выяснить причину ошибки. Одной из причин может быть нехватка памяти для Elasticsearch. Для версии старше 5 требуется минимум 2 Гб. Если вы увеличили оббъём памяти в виртуальной машине, не забудьте остановить и запустить виртуальную машину, чтобы изменения применились.
Проблема также может быть в максимальное количество кусочков памяти `vm.max_map_count`, которые может иметь процесс . Увеличьте количество доступных кусочков памяти процессу выполнением команды `sysctl -w vm.max_map_count=262144`. Обратите внимание, что это модифицирует свойство системы, в которой может выполняться множество других контейнеров.
```
$ sudo docker run -d -p 9200:9200 --name elastic elasticsearch:8.4.3
d582e031a005f41eea704cdc6b21e62e7a8a42021297ce7ce123b945ae3d3763
```
Проверьте, что в логах нет ошибок.
```
$ sudo docker logs elastic
```
При запуске в режиме демона сервис не генерирует автоматически пароль для пользователя `elastic`. Мы можем сделать это сами после запуска, чтобы протестировать сервис.
```
$ sudo docker exec -ti elastic /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
WARNING: Owner of file [/usr/share/elasticsearch/config/users] used to be [root], but now is [elasticsearch]
WARNING: Owner of file [/usr/share/elasticsearch/config/users_roles] used to be [root], but now is [elasticsearch]
This tool will reset the password of the [elastic] user to an autogenerated value.
The password will be printed in the console.
Please confirm that you would like to continue [y/N]y
Password for the [elastic] user successfully reset.
New value: 36EBVQtPjbiFPMh9Bk7X
```
```
$ curl https://localhost:9200 --insecure --user elastic:36EBVQtPjbiFPMh9Bk7X
{
"name" : "1d5f2c03f376",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "GjsgucoGTyOG5m8tWgItpA",
"version" : {
"number" : "8.4.3",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
"build_date" : "2022-10-04T07:17:24.662462378Z",
"build_snapshot" : false,
"lucene_version" : "9.3.0",
"minimum_wire_compatibility_version" : "7.17.0",
"minimum_index_compatibility_version" : "7.0.0"
},
"tagline" : "You Know, for Search"
}
```
Заодно давайте запустим контейнер с Flask. Но вначале нужен Dockerfile. В прошлой раз мы использовали образ `python:3.8` в качестве базового. Однако, в этом раз, кроме установки зависимостей через `pip`, нам нужно, чтобы приложение генерировало Javascript файл. Для этого потребуется Nodejs. В связи с появлением дополнительных файлов для работы контейнера нам нужно построить новый образ. Начнем с базового образа `ubuntu:focal`.
**Замечание.** Если оказывается, что существующий образ не подходит для вашей задачи, то спокойно создавайте свой образ на основе другого базового образа. В большинстве случаев, для образов на Docker Hub можно найти соответствующий Dockerfile на Github. Почитайте существующие докерфайлы — это один из лучших способов научиться делать свои образы.
Наш Dockerfile для приложения `foodtrucks-app` выглядит следующим образом:
```
# start from base
FROM ubuntu:focal
# install system-wide deps for python and node
RUN apt update
RUN apt -y install python3 python3-pip curl
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash
RUN apt-get install -yq nodejs
# copy our application code
ADD flask-app /opt/flask-app
WORKDIR /opt/flask-app
# fetch app specific deps
RUN npm install
RUN npm run build
RUN pip3 install -r requirements.txt
# expose port
EXPOSE 5000
# start app
ENTRYPOINT [ "python3", "./app.py" ]
```
Тут много всего нового. Вначале указан `focal` образ Ubuntu, потом используется пакетный менеджер `apt` для установки зависимостей, в частности: Python и Node. Флаг `y` нужен автоматического выбора "Yes" во всех диалогах. Также создается символическая ссылка для бинарного файла node. Это нужно для решения проблем обратной совместимости.
Потом мы используем команду ADD для копирования приложения в нужную директорию в контейнере — `/opt/flask-app`. Здесь будет находиться весь наш код. Мы также устанавливаем эту директорию в качестве рабочей, так что следующие команды будут выполняться в контексте этой локации. Теперь, когда наши системные зависимости установлены, пора установить зависимости уровня приложения. Начнем с Node, установки пакетов из npm и запуска команды сборки, как указано в нашем `package.json` файле. В конце устанавливаем пакеты Python, открываем порт и определяем запуск приложения с помощь ENTRYPOINT. ENTRYPOINT отличается от CMD тем, что при запуске можно передвать в контейнер параметры, которые добавятся к ENTRYPOINT. В нашем случае это будет параметры elasticsearch сервера: протокол, доменное имя, порт, имя пользователя, пароль.
Наконец, можно собрать образ и запустить контейнер.
```
$ sudo docker build -t studX/foodtrucks-web.
```
При первом запуске нужно будет больше времени, так как клиент Докера будет скачивать образ ubuntu, запускать все команды и готовить образ. Повторный запуск `docker build` после последующих изменений будет практически моментальным. Давайте попробуем запустить приложение.
```
$ docker run -P studX/foodtrucks-web https localhost 9200 elastic 36EBVQtPjbiFPMh9Bk7X
Unable to connect to ES. Retying in 5 secs...
Unable to connect to ES. Retying in 5 secs...
Unable to connect to ES. Retying in 5 secs...
Out of retries. Bailing out...
```
Упс! Наше приложение не смогло запуститься, потому что оно не может подключиться к Elasticsearch. Как сообщить одному контейнеру о другом и как заставить их взаимодействовать друг с другом? Ответ в следующей секции.
### 3.2 Сетевая инфраструктура Docker
Перед тем, как обсудить возможности Docker для решения описанной задачи, давайте посмотрим на возможные варианты обхода проблемы. Думаю, это поможет нам оценить удобство той функциональности, которую мы вскоре изучим.
Ладно, давайте запустим docker ps. Что тут у нас:
```
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c31bf3beb299 elasticsearch "/docker-entrypoin..." 2 hours ago Up 2 hours 0.0.0.0:9200->9200/tcp, 9300/tcp tender_wilson
```
Итак, у нас есть контейнер `elastic`, который слушает по любому локальному адресу (0.0.0.0) и порту 9200, и мы можем напрямую обращаться к нему с хоста. Если можно было бы сообщить нашему приложению как подключиться к этому адресу, то оно сможет общаться с `elastic`, верно? Давайте взглянем на код на Python, туда, где описано подключение и вспомним, переданные в контейнер аргументы.
```python
es_scheme = sys.argv[1]
es_host = sys.argv[2]
es_port = int(sys.argv[3])
es_user = sys.argv[4]
es_password = sys.argv[5]
connection_string = f"{es_scheme}://{es_host}:{es_port}"
es = Elasticsearch(
connection_string,
basic_auth=(es_user, es_password),
verify_certs=False
)
```
```
https localhost 9200 elastic 36EBVQtPjbiFPMh9Bk7X
```
Для того, чтобы это заработало, нужно запустить `foodttrucks-web` контейнер на том же хосте, что и контейнер `elastic`, и всё заработает, да? К сожалению, нет, потому что контейнер `elastic` доступен по адресу хост-машины только с хост-машины. Другой контейнер не сможет обратиться по этому адресу. Ладно, если не этот адрес, то какой тогда адрес нужно использовать для работы с контейнером `elastic`? Хорошо, что вы спросили.
Подошло время, чтобы изучить работу сети в Docker. После установки, Docker автоматически создает три сети:
```
$ sudo docker network ls
NETWORK ID NAME DRIVER
075b9f628ccc none null
be0f7178486c host host
8022115322ec bridge bridge
```
Сеть bridge — это сеть, в которой контейнеры запущены по умолчанию (программный роутер). Это значит, что когда вы запускаете контейнер `elastic`, он работает в bridge сети. Чтобы удостовериться, давайте проверим:
```
$ sudo docker network inspect bridge
[
{
"Name": "bridge",
"Id": "5c06e23b03c1d834ee7b3ac998c68c7020c959af3822db7c2629e9cd842f3de3",
"Created": "2022-10-24T21:47:51.003825084+04:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"1ea1508b85bc9cf7edc3a3401fdcd69f82ed86e9d88f5d52732eb2040ba64896": {
"Name": "relaxed_gates",
"EndpointID": "14ce3acff72602eac537644f9ea7d2e2c7b1986156a3cf05313830d880eb50e8",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
},
"a739a33489d98a4857c0d9bceefd03d930df5015c8f3ac04180d2d8b81fb050d": {
"Name": "elastic",
"EndpointID": "1db7a2319e6e890c1d547b6a8eb717a0628dfa4276d339686c0b6dc9f3dcb677",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
```
Видно, что контейнер `e931ab24dedc` находится в секции Containers. Также виден IP-адрес, выданный этому контейнеру — `172.17.0.2`. Именно этот адрес мы и искали? Давайте проверим: запустим `foodtrucks-web` приложение и попробуем обратиться к нему по IP:
```
$ sudo docker run -it --rm studX/foodtrucks-web --name flaskapp bash
root@35180ccc206a:/opt/flask-app# curl https://172.17.0.2:9200 --insecure --user elastic:36EBVQtPjbiFPMh9Bk7X
{
"name" : "a739a33489d9",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "WFQj_5KiSW2shK9eLDCyOQ",
"version" : {
"number" : "8.4.3",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
"build_date" : "2022-10-04T07:17:24.662462378Z",
"build_snapshot" : false,
"lucene_version" : "9.3.0",
"minimum_wire_compatibility_version" : "7.17.0",
"minimum_index_compatibility_version" : "7.0.0"
},
"tagline" : "You Know, for Search"
}
root@35180ccc206a:/opt/flask-app# exit
```
Cейчас всё должно стать понятно. Мы запустили контейнер в интерактивном режиме с процессом `bash`. Флаг `--rm` нужен для удобства. Благодаря нему контейнер автоматически удаляется после выхода. Мы попробуем `curl`, но нужно сначала установить его. После этого можно удостовериться, что по адресу `172.17.0.2:9200` на самом деле можно обращаться к `elastic`! Супер!
Не смотря на то, что мы нашли способ наладить связь между контейнерами, существует несколько проблем с этим подходом.
Придется добавлять записи в файл `/etc/hosts` (локальный DNS) внутри `foodtrucks-web` контейнера, чтобы приложение понимало, что имя хоста `elastic` означает `172.17.0.2`. Если IP-адрес меняется, то придется вручную менять запись.
Так как сеть bridge используется всеми контейнерами по умолчанию, этот метод не безопасен.
Но есть хорошие новости: в Docker есть отличное решение этой проблемы. Docker позволяет создавать собственные изолированные сети. Это решение также помогает справиться с проблемой `/etc/hosts`, сейчас увидим как.
Во-первых, давайте создадим свою сеть.
```
$ sudo docker network create foodtrucks-network
1a3386375797001999732cb4c4e97b88172d983b08cd0addfcb161eed0c18d89
```
```
$ sudo docker network ls
NETWORK ID NAME DRIVER
1a3386375797 foodtrucks-network bridge
8022115322ec bridge bridge
075b9f628ccc none null
be0f7178486c host host
```
Команда `network create` создаёт новую сеть `bridge`. Нами сейчас нужен именно этот тип сети. Существуют и другие типы, о которых вы можете прочитать в официальной документации.
Теперь у нас есть сеть. Можно запустить наши контейнеры внутри сети с помощью флага `--net`. Давайте так и сделаем, но сначала остановим контейнер с ElasticSearch, который был запущен в сети `bridge` по умолчанию.
```
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e931ab24dedc elasticsearch "/docker-entrypoint.s" 4 hours ago Up 4 hours 0.0.0.0:9200->9200/tcp, 9300/tcp cocky_spence
```
```
$ sudo docker stop e931ab24dedc
e931ab24dedc
$ sudo docker rm e931ab24dedc
e931ab24dedc
```
```
$ sudo docker run -d -p 9200:9200 --net foodtrucks-network --name elastic elasticsearch:8.4.3
2c0b96f9b8030f038e40abea44c2d17b0a8edda1354a08166c33e6d351d0c651
```
Сгенерируем пароль для Elasticsearch, как делали ранее:
```
$ sudo docker exec -ti elastic /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
...
Password for the [elastic] user successfully reset.
New value: 9gllhwpGufZq2GfkfZ9c
```
Мы сделали то же, что и раньше, но на этот раз присоединили контейнер к сети `foodtrucks-network`.
```
$ sudo docker network inspect foodtrucks-network
[
{
"Name": "foodtrucks-network",
"Id": "de0befdc7aac3df9561249b3d1315c72f2b4a8de1073f95cccb576f74757cd1b",
"Created": "2022-10-31T13:05:00.846504587+04:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"fe09955428cce79b76a590074049c8ade89fd0ead603082a4ca667320dac8c9a": {
"Name": "elastic",
"EndpointID": "e142fcf3ff129db8a6aed7fa73399378d3a05c20a0697930324f30ff8506dea0",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
```
Перед тем, как запускать контейнер с приложением, давайте проверим что происходит, когда запуск происходит в сети.
```
$ sudo docker run -it --rm --net foodtrucks-network --entrypoint /bin/bash studX/foodtrucks-web
root@53af252b771a:/opt/flask-app# curl https://elastic:9200 --insecure --user elastic:9gllhwpGufZq2GfkfZ9c
{
"name" : "fe09955428cc",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "-2SKtN0ISFCJ0jm6Z7TrZQ",
"version" : {
"number" : "8.4.3",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
"build_date" : "2022-10-04T07:17:24.662462378Z",
"build_snapshot" : false,
"lucene_version" : "9.3.0",
"minimum_wire_compatibility_version" : "7.17.0",
"minimum_index_compatibility_version" : "7.0.0"
},
"tagline" : "You Know, for Search"
}
```
Файлы приложения скопировались внутрь.
```
root@53af252b771a:/opt/flask-app# ls
app.py node_modules package.json requirements.txt static templates webpack.config.js
```
Попробуем запустить.
```
root@53af252b771a:/opt/flask-app# python3 app.py https elastic 9200 elastic v7SLsbtXticPLADei5vS
...
Total trucks loaded: 478
* Serving Flask app 'app' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.21.0.3:5000
Press CTRL+C to quit
```
```
root@53af252b771a:/opt/flask-app# exit
```
Ура! Работает! Магическим образом Docker теперь разрешает имя `elastic` в нужный ip, и поэтому `https://elastic:9200` можно использовать в приложении — этот адрес корректно направляет запросы в контейнер `elastic`. Отлично! Давайте теперь запустим `foodtrucks-web` контейнер по-настоящему:
```
$ sudo docker run -d --net foodtrucks-network -p 80:5000 --name foodtrucks-web studX/foodtrucks-web
2a1b77e066e646686f669bab4759ec1611db359362a031667cacbe45c3ddb413
```
```
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2a1b77e066e6 prakhar1989/foodtrucks-web "python ./app.py" 2 seconds ago Up 1 seconds 0.0.0.0:5000->5000/tcp foodtrucks-web
2c0b96f9b803 elasticsearch "/docker-entrypoint.s" 21 minutes ago Up 21 minutes 0.0.0.0:9200->9200/tcp, 9300/tcp elastic
```
Зайдите на http://studX.myoffice.ru, и увидите приложение в работе. Опять же, может показаться, что было много работы, но на самом деле мы ввели всего 4 команды чтобы с нуля дойти до работающего приложения. Все команды, которые мы проделали собраны в bash скрипте.
```
#!/bin/bash
# build the flask container
docker build -t studX/foodtrucks-web ./
# create the network
docker network create foodtrucks-network
# start the ES container, specify password beforehand
docker run -d --net foodtrucks-network --name elastic -e ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS elasticsearch:8.4.3
# start the flask app container
# point to elastic host: https://elastic:9200 user: elastic password: v7SLsbtXticPLADei5vS
docker run -d -p 80:5000 --net foodtrucks-network studX/foodtrucks-web https elastic 9200 elastic v7SLsbtXticPLADei5vS
```
Теперь представьте, что хотите поделиться приложением с другом. Или хотите запустить его на сервере, где установлен Docker. Можно запустить всю систему с помощью одной команды!
```
$ git clone https://github.com/studX.myoffice/foodtrucks-app
$ cd foodtrucks-app
$ ./build_and_run.sh
```
Вот и всё! По-моему, это невероятно крутой и мощный способ распространять и запускать приложения!
### 3.3 Docker Compose
До этого момента мы изучали клиент Docker. Но в Docker экосистеме есть несколько других инструментов с открытым исходным кодом, которые хорошо взаимодействуют с Docker. Некоторые из них это:
**Docker Compose** — инструмент для определения и запуска многоконтейнерных приложений.
**Docker Swarm** — решение для кластерных приложений.
В этом разделе мы поговорим об одном из этих инструментов — Docker Compose, и узнаем, как он может упростить работу с несколькими контейнерами.
Итак, зачем используется Compose? Это инструмент для простого определения и запуска многоконтейнерных Docker приложений. В нем есть файл `docker-compose.yml`, и с его помощью можно одной командой поднять приложение с набором сервисов. `docker-compose.yml` - это замена скрипту, без этапа построения образа.
Давайте посмотрим, сможем ли мы создать файл `docker-compose.yml` для нашего приложения и проверим, способен ли он на то, что обещает.
Однако вначале нужно установить Docker Compose. Есть у вас Windows или Mac, то Docker Compose уже установлен — он идет в комплекте с Docker Toolbox. На Linux можно установить Docker Compose, следуя простым инструкциям на сайте документации https://docs.docker.com/compose/install/other/.
```
$ sudo curl -SL https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
```
Проверить работоспособность так:
```
$ sudo docker-compose version
Docker Compose version v2.12.2
```
Теперь можно перейти к следующему шагу, то есть к созданию файла `docker-compose.yml`. Синтаксис yml-файлов очень простой (но отступы коварны):
```yaml
version: "3"
services:
elastic:
image: elasticsearch:8.4.3
environment:
- ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS
foodtrucks-web:
image: stud15/foodtrucks-web
command: https elastic 9200 elastic v7SLsbtXticPLADei5vS
depends_on:
- elastic
ports:
- 80:5000
```
Давайте разберём это подробнее. На родительском уровне мы задали название неймспейса для наших сервисов: `elastic` и `foodtrucks-web`. К каждому сервису можно добавить дополнительные параметры, среди которых image — обязательный. Для elastic мы указываем доступный на Docker Hub образ elasticsearch. Для `foodtrucks-web` приложения — тот образ, который мы создали самостоятельно до этого.
С помощью других параметров вроде `command` и `ports` можно предоставить информацию о контейнере. Подробнее о параметрах и возможных значениях можно прочитать в документации.
**Замечание.** Нужно находиться в директории с файлом `docker-compose.yml` чтобы запускать большую часть команд Compose.
Отлично! Файл готов, давайте посмотрим на `docker-compose` в действии. Но вначале нужно удостовериться, что порты свободны. Так что если у вас запущены контейнеры `foodtrucks-web` и `elastic`, то пора их остановить:
```
$ sudo docker stop $(docker ps -q)
39a2f5df14ef
2a1b77e066e6
```
Теперь можно запускать `docker-compose`. Перейдите в директорию с приложением Foodtrucks и выполните команду `docker-compose up`.
```
$ sudo docker-compose up
[+] Running 2/0
⠿ Container elastic Created 0.0s
⠿ Container foodtrucks-web Created 0.0s
Attaching to elastic, foodtrucks-web
elastic | {"@timestamp":"2022-10-31T12:01:38.948Z", "log.level": "INFO", "message":"version[8.4.3], pid[118], build[docker/42f05b9372a9a4a470db3b52817899b99a76ee73/2022-10-04T07:17:24.662462378Z], OS[Linux/5.10.0-16-amd64/amd64], JVM[Oracle Corporation/OpenJDK 64-Bit Server VM/18.0.2.1/18.0.2.1+1-1]", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"main","log.logger":"org.elasticsearch.node.Node","elasticsearch.node.name":"5f10b96ad01d","elasticsearch.cluster.name":"docker-cluster"}
...
foodtrucks-web | warnings.warn(
foodtrucks-web | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
foodtrucks-web | * Running on all addresses (0.0.0.0)
server instead.
foodtrucks-web | * Running on all addresses (0.0.0.0)
foodtrucks-web | * Running on http://127.0.0.1:5000
foodtrucks-web | * Running on http://172.19.0.3:5000
foodtrucks-web | Press CTRL+C to quit
foodtrucks-web | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET / HTTP/1.1" 200 -
foodtrucks-web | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET /static/styles/main.css HTTP/1.1" 200 -
foodtrucks-web | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET /static/build/main.js HTTP/1.1" 200 -
foodtrucks-web | 172.19.0.1 - - [31/Oct/2022 11:56:14] "GET /static/build/main.js.map HTTP/1.1" 200 -
foodtrucks-web | 172.19.0.1 - - [31/Oct/2022 11:56:16] "GET /static/build/main.js.map HTTP/1.1" 304 -
^CGracefully stopping... (press Ctrl+C again to force)
```
Перейдите по IP чтобы увидеть приложение. Круто, да? Всего лишь пара строк конфигурации и несколько Докер-контейнеров работают в унисон. Давайте остановим сервисы и перезапустим в detached mode:
```
[+] Running 2/2
⠿ Container foodtrucks-web Stopped 10.9s
⠿ Container elastic Stopped
```
```
$ sudo docker-compose up -d
[+] Running 2/2
⠿ Container elastic Started 0.7s
⠿ Container foodtrucks-web Started 1.4s
```
```
$ sudo docker-compose ps
elastic "/bin/tini -- /usr/l…" elastic running 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp, 9300/tcp
foodtrucks-web "python3 ./app.py ht…" foodtrucks-web running 0.0.0.0:8000->5000/tcp, :::8000->5000/tcp
```
Проверим, создались ли какие-нибудь сети:
```
$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
5c06e23b03c1 bridge bridge local
de0befdc7aac foodtrucks bridge local
429ee73da4ea foodtrucks_default bridge local
e9a9ee381df4 host host local
7e9bc47cad68 none null local
```
Видно, что Compose самостоятельно создал сеть `foodtrucks_default` и подсоединил оба сервиса в эту сеть, так, чтобы они могли общаться друг с другом. Каждый контейнер для сервиса подключен к сети, и оба контейнера доступны другим контейнерам в сети. Они доступны по hostname, который совпадает с названием контейнера. Давайте проверим, находится ли эта информация в `/etc/hosts`.
```
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bb72dcebd379 prakhar1989/foodtrucks-web "python app.py" 20 hours ago Up 19 hours 0.0.0.0:5000->5000/tcp foodtrucks_web_1
3338fc79be4b elasticsearch "/docker-entrypoint.s" 20 hours ago Up 19 hours 9200/tcp, 9300/tcp foodtrucks_es_1
```
```
$ sudo docker exec -it bb72dcebd379 bash
root@bb72dcebd379:/opt/flask-app# cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.18.0.2 bb72dcebd379
```
Упс! Оказывается, файл понятия не имеет о `elastic`. Как же наше приложение работает? Давайте попингуем его по названию хоста:
```
root@bb72dcebd379:/opt/flask-app# ping elastic
PING es (172.18.0.2) 56(84) bytes of data.
64 bytes from foodtrucks_es_1.foodtrucks_default (172.18.0.2): icmp_seq=1 ttl=64 time=0.049 ms
64 bytes from foodtrucks_es_1.foodtrucks_default (172.18.0.2): icmp_seq=2 ttl=64 time=0.064 ms
^C
--- es ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.049/0.056/0.064/0.010 ms
```
Вуаля! Работает! Каким-то магическим образом контейнер смог сделать пинг хоста `elastic`. Оказывается, Docker 1.10 добавили новую сетевую систему, которая производит обнаружение сервисов через DNS-сервер. Если интересно, то почитайте подробнее о предложении в release notes.
На этом наш тур по Docker Compose завершен. С этим инструментом можно ставить сервисы на паузу, запускать отдельные команды в контейнере и даже масштабировать систему, то есть увеличивать количество контейнеров.
Надеюсь, проделанные нами действия продемонстрировали как на самом деле просто управлять многоконтейнерной средой с Compose.
## 4 Заключение
Мы подошли к концу. После длинного, изматывающего, но интересного пособия вы готовы захватить мир контейнеров! Если вы следовали пособию до самого конца, то можете заслуженно гордиться собой. Вы научились устанавливать Docker, запускать свои контейнеры, запускать статические и динамические веб-сайты.
## Релевантные источники
- https://github.com/prakhar1989/docker-curriculum
- Nemeth E. et al. UNIX and Linux system administration handbook. Chapter 25.
Loading…
Cancel
Save