Python Flask开发web应用

轻量化Flask Web框架总结

  • 介绍
  • 扩展
    • Flask-Restplus
    • Flask-HttPAuth
    • Flask-SQLAlchemy
    • OAuth2
  • 部署Web程序
    • gunicorn
    • Docker

介绍

  Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合MVC模式进行开发,开发人员分工合作,小型团队在短时间内就可以完成功能丰富的中小型网站或Web服务的实现。另外,Flask还有很强的定制性,用户可以根据自己的需求来添加相应的功能,在保持核心功能简单的同时实现功能的丰富与扩展,其强大的插件库可以让用户实现个性化的网站定制,开发出功能强大的网站。
  Flask是目前十分流行的web框架,采用Python编程语言来实现相关功能。它被称为微框架(microframework),“微”并不 是意味着把整个Web应用放入到一个Python文件,微框架中的“微”是指Flask旨在保持代码简洁且易于扩展,Flask框架的主要特征是核心构成比较简单,但具有很强的扩展性和兼容性,程序员可以使用Python语言快速实现一个网站或Web服务。一般情况下,它不会指定数据库和模板引擎等对象,用户可以根据需要自己选择各种数据库。Flask自身不会提供表单验证功能,在项目实施过程中可以自由配置,从而为应用程序开发提供数据库抽象层基础组件,支持进行表单数据合法性验证、文件上传处理、用户身份认证和数据库集成等功能。Flask主要包括Werkzeug和Jinja2两个核心函数库,它们分别负责业务处理和安全方面的功能,这些基础函数为web项目开发过程提供了丰富的基础组件。Werkzeug库十分强大,功能比较完善,支持URL路由请求集成,一次可以响应多个用户的访问请求;支持Cookie和会话管理,通过身份缓存数据建立长久连接关系,并提高用户访问速度;支持交互式Javascript调试,提高用户体验;可以处理HTTP基本事务,快速响应客户端推送过来的访问请求。Jinja2库支持自动HTML转移功能,能够很好控制外部黑客的脚本攻击。系统运行速度很快,页面加载过程会将源码进行编译形成python字节码,从而实现模板的高效运行;模板继承机制可以对模板内容进行修改和维护,为不同需求的用户提供相应的模板。目前Python的web框架有很多。除了Flask,还有django、Web2py等等。其中Diango是目前Python的框架中使用度最高的。但是Django如同java的EJB(EnterpriseJavaBeansJavaEE服务器端组件模型)多被用于大型网站的开发,但对于大多数的小型网站的开发,使用SSH(Struts+Spring+Hibernat的一个JavaEE集成框架)就可以满足,和其他的轻量级框架相比较,Flask框架有很好的扩展性,这是其他Web框架不可替代的。
  关于这些内容可以参考我的github:Flask-HTTPAuth.OAuth2作为一位开发者我觉得还是应该好好看一下。

扩展

Flask-Restplus

这里给出官网的Full Example。使用Flask-Restplus的原因是作为swagger文档比较好用,主要是在控制请求的输入输出以及异常处理返回都做得不错,但是好像听说github上面关于这个项目已经不再更新了。

from flask import Flask
from flask_restplus import Api, Resource, fields
from werkzeug.contrib.fixers import ProxyFix

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
api = Api(app, version='1.0', title='TodoMVC API',
    description='A simple TodoMVC API',
)

ns = api.namespace('todos', description='TODO operations')

todo = api.model('Todo', {
    'id': fields.Integer(readonly=True, description='The task unique identifier'),
    'task': fields.String(required=True, description='The task details')
})


class TodoDAO(object):
    def __init__(self):
        self.counter = 0
        self.todos = []

    def get(self, id):
        for todo in self.todos:
            if todo['id'] == id:
                return todo
        api.abort(404, "Todo {} doesn't exist".format(id))

    def create(self, data):
        todo = data
        todo['id'] = self.counter = self.counter + 1
        self.todos.append(todo)
        return todo

    def update(self, id, data):
        todo = self.get(id)
        todo.update(data)
        return todo

    def delete(self, id):
        todo = self.get(id)
        self.todos.remove(todo)


DAO = TodoDAO()
DAO.create({'task': 'Build an API'})
DAO.create({'task': '?????'})
DAO.create({'task': 'profit!'})


@ns.route('/')
class TodoList(Resource):
    '''Shows a list of all todos, and lets you POST to add new tasks'''
    @ns.doc('list_todos')
    @ns.marshal_list_with(todo)
    def get(self):
        '''List all tasks'''
        return DAO.todos

    @ns.doc('create_todo')
    @ns.expect(todo)
    @ns.marshal_with(todo, code=201)
    def post(self):
        '''Create a new task'''
        return DAO.create(api.payload), 201


@ns.route('/')
@ns.response(404, 'Todo not found')
@ns.param('id', 'The task identifier')
class Todo(Resource):
    '''Show a single todo item and lets you delete them'''
    @ns.doc('get_todo')
    @ns.marshal_with(todo)
    def get(self, id):
        '''Fetch a given resource'''
        return DAO.get(id)

    @ns.doc('delete_todo')
    @ns.response(204, 'Todo deleted')
    def delete(self, id):
        '''Delete a task given its identifier'''
        DAO.delete(id)
        return '', 204

    @ns.expect(todo)
    @ns.marshal_with(todo)
    def put(self, id):
        '''Update a task given its identifier'''
        return DAO.update(id, api.payload)


if __name__ == '__main__':
    app.run(debug=True)

Flask-HttPAuth

Welcome to Flask-HTTPAuth

  1. Basic authentication examples

The function decorated with the verify_password decorator receives the username and password sent by the client. If the credentials belong to a user, then the function should return the user object. If the credentials are invalid the function can return None or False. The user object can then be queried from the current_user() method of the authentication instance.


from flask import Flask
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
auth = HTTPBasicAuth()

users = {
    "john": generate_password_hash("hello"),
    "susan": generate_password_hash("bye")
}

@auth.verify_password
def verify_password(username, password):
    if username in users and \
            check_password_hash(users.get(username), password):
        return username

@app.route('/')
@auth.login_required
def index():
    return "Hello, {}!".format(auth.current_user())

if __name__ == '__main__':
    app.run()

  1. Digest authentication example

from flask import Flask
from flask_httpauth import HTTPDigestAuth

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret key here'
auth = HTTPDigestAuth()

users = {
    "john": "hello",
    "susan": "bye"
}

@auth.get_password
def get_pw(username):
    if username in users:
        return users.get(username)
    return None

@app.route('/')
@auth.login_required
def index():
    return "Hello, {}!".format(auth.username())

if __name__ == '__main__':
    app.run()

By default, Flask-HTTPAuth stores the challenge data in the Flask session. To make the authentication flow secure when using session storage, it is required that server-side sessions are used instead of the default Flask cookie based sessions, as this ensures that the challenge data is not at risk of being captured as it moves in a cookie between server and client. The Flask-Session and Flask-KVSession extensions are both very good options to implement server-side sessions.

As an alternative to using server-side sessions, an application can implement its own generation and storage of challenge data. To do this, there are four callback functions that the application needs to implement:


@auth.generate_nonce
def generate_nonce():
    """Return the nonce value to use for this client."""
    pass

@auth.generate_opaque
def generate_opaque():
    """Return the opaque value to use for this client."""
    pass

@auth.verify_nonce
def verify_nonce(nonce):
    """Verify that the nonce value sent by the client is correct."""
    pass

@auth.verify_opaque
def verify_opaque(opaque):
    """Verify that the opaque value sent by the client is correct."""
    pass

  1. Token Authentication Example

from flask import Flask, g
from flask_httpauth import HTTPTokenAuth

app = Flask(__name__)
auth = HTTPTokenAuth(scheme='Bearer')

tokens = {
    "secret-token-1": "john",
    "secret-token-2": "susan"
}

@auth.verify_token
def verify_token(token):
    if token in tokens:
        return tokens[token]

@app.route('/')
@auth.login_required
def index():
    return "Hello, {}!".format(auth.current_user())

if __name__ == '__main__':
    app.run()

  1. API Document

class flask_httpauth.HTTPBasicAuth

This class handles HTTP Basic authentication for Flask routes.

__init__(scheme=None, realm=None)

Create a basic authentication object.

If the optional scheme argument is provided, it will be used instead of the standard “Basic” scheme in the WWW-Authenticate response. A fairly common practice is to use a custom scheme to prevent browsers from prompting the user to login.

The realm argument can be used to provide an application defined realm with the WWW-Authenticate header.

verify_password(verify_password_callback)

If defined, this callback function will be called by the framework to verify that the username and password combination provided by the client are valid. The callback function takes two arguments, the username and the password. It must return the user object if credentials are valid, or True if a user object is not available. In case of failed authentication, it should return None or False. Example usage:

@auth.verify_password
  def verify_password(username, password):
    user = User.query.filter_by(username).first()
    if user and passlib.hash.sha256_crypt.verify(password, user.password_hash):
        return user

get_user_roles(roles_callback)

If defined, this callback function will be called by the framework to obtain the roles assigned to a given user. The callback function takes a single argument, the user for which roles are requested. The user object passed to this function will be the one returned by the verify_callback function. The function should return the role or list of roles that belong to the user. Example:

@auth.get_user_roles
def get_user_roles(user):
    return user.get_roles()

get_password(password_callback)

Deprecated This callback function will be called by the framework to obtain the password for a given user. Example:

@auth.get_password
def get_password(username):
    return db.get_user_password(username)

hash_password(hash_password_callback)

Deprecated If defined, this callback function will be called by the framework to apply a custom hashing algorithm to the password provided by the client. If this callback isn’t provided the password will be checked unchanged. The callback can take one or two arguments. The one argument version receives the password to hash, while the two argument version receives the username and the password in that order. Example single argument callback:

@auth.hash_password
def hash_password(password):
    return md5(password).hexdigest()
    
@auth.hash_password
def hash_pw(username, password):
    salt = get_salt(username)
    return hash(password, salt)


error_handler(error_callback)

If defined, this callback function will be called by the framework when it is necessary to send an authentication error back to the client. The function can take one argument, the status code of the error, which can be 401 (incorrect credentials) or 403 (correct, but insufficient credentials). To preserve compatiiblity with older releases of this package, the function can also be defined without arguments. The return value from this function must by any accepted response type in Flask routes. If this callback isn’t provided a default error response is generated. Example:


@auth.error_handler
def auth_error(status):
    return "Access Denied", status

login_required(view_function_callback)

This callback function will be called when authentication is successful. This will typically be a Flask view function. Example:

@app.route('/private')
@auth.login_required
def private_page():
    return "Only for authorized people!"

@app.route('/private')
@auth.login_required(role='admin')
def private_page():
    return "Only for admins!"


current_user()


@app.route('/')
@auth.login_required
def index():
    user = auth.current_user()
    return "Hello, {}!".format(user.name)

username()

@app.route('/')
@auth.login_required
def index():
    return "Hello, {}!".format(auth.username())

class flask_httpauth.HTTPDigestAuth

This class handles HTTP Digest authentication for Flask routes. The SECRET_KEY configuration must be set in the Flask application to enable the session to work. Flask by default stores user sessions in the client as secure cookies, so the client must be able to handle cookies. To make this authentication method secure, a session interface that writes sessions in the server must be used.

init(self, scheme=None, realm=None, use_ha1_pw=False)

Create a digest authentication object.

If the optional scheme argument is provided, it will be used instead of the “Digest” scheme in the WWW-Authenticate response. A fairly common practice is to use a custom scheme to prevent browsers from prompting the user to login.

The realm argument can be used to provide an application defined realm with the WWW-Authenticate header.

If use_ha1_pw is False, then the get_password callback needs to return the plain text password for the given user. If use_ha1_pw is True, the get_password callback needs to return the HA1 value for the given user. The advantage of setting use_ha1_pw to True is that it allows the application to store the HA1 hash of the password in the user database.

generate_ha1(username, password)

Generate the HA1 hash that can be stored in the user database when use_ha1_pw is set to True in the constructor.

generate_nonce(nonce_making_callback)

If defined, this callback function will be called by the framework to generate a nonce. If this is defined, verify_nonce should also be defined.

This can be used to use a state storage mechanism other than the session.

verify_nonce(nonce_verify_callback)

If defined, this callback function will be called by the framework to verify that a nonce is valid. It will be called with a single argument: the nonce to be verified.

This can be used to use a state storage mechanism other than the session.

generate_opaque(opaque_making_callback)

If defined, this callback function will be called by the framework to generate an opaque value. If this is defined, verify_opaque should also be defined.

This can be used to use a state storage mechanism other than the session.

verify_opaque(opaque_verify_callback)

If defined, this callback function will be called by the framework to verify that an opaque value is valid. It will be called with a single argument: the opaque value to be verified.

This can be used to use a state storage mechanism other than the session.

get_password(password_callback)

See basic authentication for documentation and examples.

get_user_roles(roles_callback)

See basic authentication for documentation and examples.

error_handler(error_callback)

See basic authentication for documentation and examples.

login_required(view_function_callback)

See basic authentication for documentation and examples.

current_user()

See basic authentication for documentation and examples.

username()

See basic authentication for documentation and examples.

class flask_httpauth.HTTPTokenAuth

This class handles HTTP authentication with custom schemes for Flask routes.

init(scheme=‘Bearer’, realm=None, header=None)

Create a token authentication object.

The scheme argument can be use to specify the scheme to be used in the WWW-Authenticate response. The Authorization header sent by the client must include this scheme followed by the token. Example:

Authorization: Bearer this-is-my-token
The realm argument can be used to provide an application defined realm with the WWW-Authenticate header.

The header argument can be used to specify a custom header instead of Authorization from where to obtain the token. If a custom header is used, the scheme should not be included. Example:

X-API-Key: this-is-my-token

verify_token(verify_token_callback)

This callback function will be called by the framework to verify that the credentials sent by the client with the Authorization header are valid. The callback function takes one argument, the token provided by the client. The function must return the user object if the token is valid, or True if a user object is not available. In case of a failed authentication, the function should return None or False. Example usage:

@auth.verify_token
def verify_token(token):
    return User.query.filter_by(token=token).first()

Note that a verify_token callback is required when using this class.

get_user_roles(roles_callback)

See basic authentication for documentation and examples.

error_handler(error_callback)

See basic authentication for documentation and examples.

login_required(view_function_callback)

See basic authentication for documentation and examples.

current_user()

See basic authentication for documentation and examples.

class flask_httpauth.HTTPMultiAuth

This class handles HTTP authentication with custom schemes for Flask routes.

init(auth_object, …)

Create a multiple authentication object.

The arguments are one or more instances of HTTPBasicAuth, HTTPDigestAuth or HTTPTokenAuth. A route protected with this authentication method will try all the given authentication objects until one succeeds.

login_required(view_function_callback)

See basic authentication for documentation and examples.

current_user()

See basic authentication for documentation and examples.

Flask-SQLAlchemy

这个类似于EF,我还是不太喜欢用这个,例如操作pgsql直接用的是psycopg2,这样可以更加方便调试。

一个简单的应用


from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:tmp/test.db'
db = SQLAlchemy(app)


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)

    def __init__(self, username, email):
        self.username = username
        self.email = email

    def __repr__(self):
        return '' % self.username

# 创建数据库

from yourapplication import db
db.create_all()

# 创建用户

>>> from yourapplication import User
>>> admin = User('admin', '[email protected]')
>>> guest = User('guest', '[email protected]')

# 写入用户
>>> db.session.add(admin)
>>> db.session.add(guest)
>>> db.session.commit()

# 简单的关系

from datetime import datetime


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80))
    body = db.Column(db.Text)
    pub_date = db.Column(db.DateTime)

    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
    category = db.relationship('Category',
        backref=db.backref('posts', lazy='dynamic'))

    def __init__(self, title, body, category, pub_date=None):
        self.title = title
        self.body = body
        if pub_date is None:
            pub_date = datetime.utcnow()
        self.pub_date = pub_date
        self.category = category

    def __repr__(self):
        return '' % self.title


class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return '' % self.name

配置参数说明:

配置 说明
SQLALCHEMY_DATABASE_URI 用于连接数据的数据库。例如:sqlite:tmp/test.db mysql://username:password@server/db
SQLALCHEMY_BINDS 一个映射绑定 (bind) 键到 SQLAlchemy 连接 URIs 的字典。 更多的信息请参阅 绑定多个数据库。
SQLALCHEMY_ECHO 如果设置成 True,SQLAlchemy 将会记录所有 发到标准输出(stderr)的语句,这对调试很有帮助。
SQLALCHEMY_RECORD_QUERIES 可以用于显式地禁用或者启用查询记录。查询记录 在调试或者测试模式下自动启用。更多信息请参阅 get_debug_queries()。
SQLALCHEMY_NATIVE_UNICODE 可以用于显式地禁用支持原生的 unicode。这是 某些数据库适配器必须的(像在 Ubuntu 某些版本上的 PostgreSQL),当使用不合适的指定无编码的数据库 默认值时。
SQLALCHEMY_POOL_SIZE 数据库连接池的大小。默认是数据库引擎的默认值 (通常是 5)。
SQLALCHEMY_POOL_TIMEOUT 指定数据库连接池的超时时间。默认是 10。
SQLALCHEMY_POOL_RECYCLE 自动回收连接的秒数。这对 MySQL 是必须的,默认 情况下 MySQL 会自动移除闲置 8 小时或者以上的连接。 需要注意地是如果使用 MySQL 的话, Flask-SQLAlchemy 会自动地设置这个值为 2 小时。
SQLALCHEMY_MAX_OVERFLOW 控制在连接池达到最大值后可以创建的连接数。当这些额外的 连接回收到连接池后将会被断开和抛弃。
SQLALCHEMY_TRACK_MODIFICATIONS 如果设置成 True (默认情况),Flask-SQLAlchemy 将会追踪对象的修改并且发送信号。这需要额外的内存, 如果不必要的可以禁用它。

连接URI格式

dialect+driver://username:password@host:port/database

声明模型

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)

    def __init__(self, username, email):
        self.username = username
        self.email = email

    def __repr__(self):
        return '' % self.username
类型 说明
Integer 一个整数
String (size) 有长度限制的字符串
Text 一些较长的 unicode 文本
DateTime 表示为 Python datetime 对象的 时间和日期
Float 存储浮点值
Boolean 存储布尔值
PickleType 存储为一个持久化的 Python 对象
LargeBinary 存储一个任意大的二进制数据

one to many

class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    addresses = db.relationship('Address', backref='person',
                                lazy='dynamic')

class Address(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(50))
    person_id = db.Column(db.Integer, db.ForeignKey('person.id'))

many to many

tags = db.Table('tags',
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')),
    db.Column('page_id', db.Integer, db.ForeignKey('page.id'))
)

class Page(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    tags = db.relationship('Tag', secondary=tags,
        backref=db.backref('pages', lazy='dynamic'))

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)

API Document

http://www.pythondoc.com/flask-sqlalchemy/api.html

OAuth2

OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将服务提供者上的用户的用户密码提供给第三方移动应用或分享他们数据的所有内容。

图来源:OAuth2详解

Python Flask开发web应用_第1张图片

QAuth运作流程:1. 用户(Resource Owner)打开客户端(User Agent),客户端要求用户给与第三方服务提供方(HTTP service)授权并附上client id和URI;2. 第三方服务提供方即授权方接收到请求后,向用户询问是否进行授权。方法是让用户提供用户名和密码。3. 用户同意授权后,授权方重定向到客户端提供和的URL并附上授权码(Authorization code)。4. 客户端拿到授权码后向认证服务端(Authorization server)申请令牌。5. 认证服务端确认客户端的请求无误后向客户端发放令牌。5. 客户端拿到令牌后即可在资源服务器(Resource server)申请资源。
Python Flask开发web应用_第2张图片

授权模式

  • 授权码模式:功能最完整,流程最严密的授权模式

    (1)用户访问客户端,后者将前者导向认证服务器,假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。

    (2)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌:GET /oauth/token?response_type=code&client_id=test&redirect_uri=重定向页面链接。请求成功返回code授权码,一般有效时间是10分钟。

    (3)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。POST /oauth/token?response_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=重定向页面链接。

  • 简化模式

    Python Flask开发web应用_第3张图片

  • 用户名密码模式

    Python Flask开发web应用_第4张图片

  • 客户端模式

    Python Flask开发web应用_第5张图片

部署Web程序

gunicorn

gunicorn.config

# 并行工作进程数
workers = 2
# 指定每个工作者的线程数
threads = 2
bind = '0.0.0.0:8080'
# 设置守护进程,将进程交给supervisor管理
daemon = 'false'
# 工作模式协程
# worker_class = 'gevent'
# 设置最大并发量
worker_connections = 2000
# 设置进程文件目录
# pidfile = '/var/run/gunicorn.pid'
# 设置访问日志和错误信息日志路径
accesslog = './logs/gunicorn_acess.log'
errorlog = './logs/gunicorn_error.log'
# 设置日志记录水平
loglevel = 'warning'

Docker

Dockerfile

FROM centos:7
ENV TZ="Asia/Shanghai" LANG="C.UTF-8"
ENV PATH $PATH:/usr/local/python3/bin/
ENV PYTHONIOENCODING utf-8
WORKDIR /app
RUN set -ex \
	&& mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup \
	&& curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
	&& yum makecache \
	&& sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo \
	&& yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make wget \
	&& yum clean all \
	&& mkdir /usr/local/python3 \
	&& wget https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tar.xz \
	&& tar -xvJf  Python-3.6.8.tar.xz && rm -f Python-3.6.8.tar.xz \
	&& cd Python-3.6.8 \
	&& ./configure prefix=/usr/local/python3 \
	&& make && make install \
	&& cd .. \
	&& rm -rf Python-3.6.8
# # opencv dependency
RUN yum -y install mesa-libGL-devel mesa-libGLU-devel
# RUN apt-get install -y python3-opencv
COPY requirements.txt ./
RUN python3 -m pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.mirrors.ustc.edu.cn/simple/
COPY . .
# CMD python3 app_swagger.py
CMD gunicorn -c gunicorn.conf app_swagger:app

docker-conpose

version: "3"

services:
  web1:
    image: test:latest
    build:
      context: .
      dockerfile: Dockerfile
    container_name: test1
    deploy:
      restart_policy:
        condition: any
  
  web2:
    image:  test:latest
    container_name: test2
    deploy:
      restart_policy:
        condition: any

关于更多的dockercmpose可以关注我的博客:DockerCompose用法

你可能感兴趣的:(#,python,flask,python)