1669001489
In this tutorial, we'll look at how to set up Flask with Postgres and Docker. For production environments, we'll add on Gunicorn, Traefik, and Let's Encrypt.
Start by creating a project directory:
$ mkdir flask-docker-traefik && cd flask-docker-traefik
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$
Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.
Then, create the following files and folders:
└── services
└── web
├── manage.py
├── project
│ └── __init__.py
└── requirements.txt
Add Flask to requirements.txt:
Flask==2.0.1
Install the package from "services/web":
(venv)$ pip install -r requirements.txt
Next, let's create a simple Flask application in __init.py__:
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/")
def read_root():
return jsonify(hello="world")
Then, to configure the Flask CLI tool to run and manage the app from the command line, add the following to services/web/manage.py:
from flask.cli import FlaskGroup
from project import app
cli = FlaskGroup(app)
if __name__ == "__main__":
cli()
Here, we created a new FlaskGroup
instance to extend the normal CLI with commands related to the Flask app.
Run the server from the "web" directory:
(venv)$ export FLASK_APP=project/__init__.py
(venv)$ python manage.py run
Navigate to 127.0.0.1:5000, you should see:
{
"hello": "world"
}
Kill the server once done. Exit from the virtual environment, and remove it as well.
Install Docker, if you don't already have it, then add a Dockerfile to the "web" directory:
# pull the official docker image
FROM python:3.9.5-slim
# set work directory
WORKDIR /app
# set env variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
So, we started with a slim
-based Docker image for Python 3.9.5. We then set a working directory along with two environment variables:
PYTHONDONTWRITEBYTECODE
: Prevents Python from writing pyc files to disc (equivalent to python -B
option)PYTHONUNBUFFERED
: Prevents Python from buffering stdout and stderr (equivalent to python -u
option)Finally, we copied over the requirements.txt file, installed the dependencies, and copied over the Flask app itself.
Review Docker for Python Developers for more on structuring Dockerfiles as well as some best practices for configuring Docker for Python-based development.
Next, add a docker-compose.yml file to the project root:
version: '3.8'
services:
web:
build: ./services/web
command: python manage.py run -h 0.0.0.0
volumes:
- ./services/web/:/app
ports:
- 5000:5000
environment:
- FLASK_APP=project/__init__.py
- FLASK_ENV=development
Review the Compose file reference for info on how this file works.
Build the image:
$ docker-compose build
Once the image is built, run the container:
$ docker-compose up -d
Navigate to http://127.0.0.1:5000/ to again view the hello world sanity check.
Check for errors in the logs if this doesn't work via
docker-compose logs -f
.
To configure Postgres, we need to add a new service to the docker-compose.yml file, set up Flask-SQLAlchemy, and install Psycopg2.
First, add a new service called db
to docker-compose.yml:
version: '3.8'
services:
web:
build: ./services/web
command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; python manage.py run -h 0.0.0.0'
volumes:
- ./services/web/:/app
ports:
- 5000:5000
environment:
- FLASK_APP=project/__init__.py
- FLASK_ENV=development
- DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_dev
depends_on:
- db
db:
image: postgres:13-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_flask
- POSTGRES_PASSWORD=hello_flask
- POSTGRES_DB=hello_flask_dev
volumes:
postgres_data:
To persist the data beyond the life of the container we configured a volume. This config will bind postgres_data
to the "/var/lib/postgresql/data/" directory in the container.
We also added an environment key to define a name for the default database and set a username and password.
Review the "Environment Variables" section of the Postgres Docker Hub page for more info.
Take note of the new command in the web
service:
bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; python manage.py run -h 0.0.0.0'
while !</dev/tcp/db/5432; do sleep 1
will continue until Postgres is up. Once up, python manage.py run -h 0.0.0.0
runs.
Then, add a new file called config.py to the "project" directory, where we'll define environment-specific configuration variables:
import os
class Config(object):
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite://")
SQLALCHEMY_TRACK_MODIFICATIONS = False
Here, the database is configured based on the DATABASE_URL
environment variable that we just defined. Take note of the default value.
Update __init__.py to pull in the config on init:
from flask import Flask, jsonify
app = Flask(__name__)
app.config.from_object("project.config.Config")
@app.get("/")
def read_root():
return jsonify(hello="world")
Add Flask-SQLAlchemy and Psycopg2 to requirements.txt:
Flask==2.0.1
Flask-SQLAlchemy==2.5.1
psycopg2-binary==2.8.6
Update __init__.py again to create a new SQLAlchemy
instance and define a database model:
from dataclasses import dataclass
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_object("project.config.Config")
db = SQLAlchemy(app)
@dataclass
class User(db.Model):
id: int = db.Column(db.Integer, primary_key=True)
email: str = db.Column(db.String(120), unique=True, nullable=False)
active: bool = db.Column(db.Boolean(), default=True, nullable=False)
def __init__(self, email: str) -> None:
self.email = email
@app.get("/")
def read_root():
users = User.query.all()
return jsonify(users)
Using the dataclass decorator on the database model helps us serialize the database objects.
Finally, update manage.py:
from flask.cli import FlaskGroup
from project import app, db
cli = FlaskGroup(app)
@cli.command("create_db")
def create_db():
db.drop_all()
db.create_all()
db.session.commit()
if __name__ == "__main__":
cli()
This registers a new command, create_db
, to the CLI so that we can run it from the command line, which we'll use shortly to apply the model to the database.
Build the new image and spin up the two containers:
$ docker-compose up -d --build
Create the table:
$ docker-compose exec web python manage.py create_db
Get the following error?
sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) FATAL: database "hello_flask_dev" does not exist
Run
docker-compose down -v
to remove the volumes along with the containers. Then, re-build the images, run the containers, and apply the migrations.
Ensure the users
table was created:
$ docker-compose exec db psql --username=hello_flask --dbname=hello_flask_dev
psql (13.3)
Type "help" for help.
hello_flask_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------------+-------------+----------+------------+------------+-----------------------------
hello_flask_dev | hello_flask | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_flask | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_flask | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_flask +
| | | | | hello_flask=CTc/hello_flask
template1 | hello_flask | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_flask +
| | | | | hello_flask=CTc/hello_flask
(4 rows)
hello_flask_dev=# \c hello_flask_dev
You are now connected to database "hello_flask_dev" as user "hello_flask".
hello_flask_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+------+-------+-------------
public | user | table | hello_flask
(1 row)
hello_flask_dev=# \q
You can check that the volume was created as well by running:
$ docker volume inspect flask-docker-traefik_postgres_data
You should see something similar to:
[
{
"CreatedAt": "2021-06-05T14:12:52Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "flask-docker-traefik",
"com.docker.compose.version": "1.29.1",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/flask-docker-traefik_postgres_data/_data",
"Name": "flask-docker-traefik_postgres_data",
"Options": null,
"Scope": "local"
}
]
Navigate to http://127.0.0.1:5000. The sanity check shows an empty list. That's because we haven't populated the users
table. Let's add a CLI seed command for adding sample users
to the users table in manage.py:
from flask.cli import FlaskGroup
from project import User, app, db
cli = FlaskGroup(app)
@cli.command("create_db")
def create_db():
db.drop_all()
db.create_all()
db.session.commit()
@cli.command("seed_db") # new
def seed_db():
db.session.add(User(email="michael@mherman.org"))
db.session.add(User(email="test@example.com"))
db.session.commit()
if __name__ == "__main__":
cli()
Try it out:
$ docker-compose exec web python manage.py seed_db
Navigate to http://127.0.0.1:5000 again. You should now see:
[
{
"active": true,
"email": "michael@mherman.org",
"id": 1
},
{
"active": true,
"email": "test@example.com",
"id": 2
}
]
Moving along, for production environments, let's add Gunicorn, a production-grade WSGI server, to the requirements file:
Flask==2.0.1
Flask-SQLAlchemy==2.5.1
gunicorn==20.1.0
psycopg2-binary==2.8.6
Since we still want to use Flask's built-in server in development, create a new compose file in the project root called docker-compose.prod.yml for production:
version: '3.8'
services:
web:
build: ./services/web
command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn --bind 0.0.0.0:5000 manage:app'
ports:
- 5000:5000
environment:
- FLASK_APP=project/__init__.py
- FLASK_ENV=production
- DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_prod
depends_on:
- db
db:
image: postgres:13-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_flask
- POSTGRES_PASSWORD=hello_flask
- POSTGRES_DB=hello_flask_prod
volumes:
postgres_data_prod:
If you have multiple environments, you may want to look at using a docker-compose.override.yml configuration file. With this approach, you'd add your base config to a docker-compose.yml file and then use a docker-compose.override.yml file to override those config settings based on the environment.
Take note of the default command
. We're running Gunicorn rather than the Flask development server. We also removed the volume from the web
service since we don't need it in production.
Bring down the development containers (and the associated volumes with the -v flag):
$ docker-compose down -v
Then, build the production images and spin up the containers:
$ docker-compose -f docker-compose.prod.yml up -d --build
Create the table and apply the seed:
$ docker-compose -f docker-compose.prod.yml exec web python manage.py create_db
$ docker-compose -f docker-compose.prod.yml exec web python manage.py seed_db
Verify that the hello_flask_prod
database was created along with the users
table. Test out http://127.0.0.1:5000/.
Again, if the container fails to start, check for errors in the logs via
docker-compose -f docker-compose.prod.yml logs -f
.
Create a new Dockerfile in the "web " directory called Dockerfile.prod for use with production builds:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.9.5-slim as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.1
COPY . .
RUN flake8 --ignore=E501,F401 .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.9.5-slim
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup --system app && adduser --system --group app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends netcat
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
Here, we used a Docker multi-stage build to reduce the final image size. Essentially, builder
is a temporary image that's used for building the Python wheels. The wheels are then copied over to the final production image and the builder
image is discarded.
You could take the multi-stage build approach a step further and use a single Dockerfile instead of creating two Dockerfiles. Think of the pros and cons of using this approach over two different files.
Did you notice that we created a non-root user? By default, Docker runs container processes as root inside of a container. This is a bad practice since attackers can gain root access to the Docker host if they manage to break out of the container. If you're root in the container, you'll be root on the host.
Update the web
service within the docker-compose.prod.yml file to build with Dockerfile.prod:
web:
build:
context: ./services/web
dockerfile: Dockerfile.prod
command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn --bind 0.0.0.0:5000 manage:app'
ports:
- 5000:5000
environment:
- FLASK_APP=project/__init__.py
- FLASK_ENV=production
- DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_prod
depends_on:
- db
Try it out:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py create_db
$ docker-compose -f docker-compose.prod.yml exec web python manage.py seed_db
Next, let's add Traefik, a reverse proxy, into the mix.
New to Traefik? Check out the offical Getting Started guide.
Traefik vs Nginx: Traefik is a modern, HTTP reverse proxy and load balancer. It's often compared to Nginx, a web server and reverse proxy. Since Nginx is primarily a webserver, it can be used to serve up a webpage as well as serve as a reverse proxy and load balancer. In general, Traefik is simpler to get up and running while Nginx is more versatile.
Traefik:
- Reverse proxy and load balancer
- Automatically issues and renews SSL certificates, via Let's Encrypt, out-of-the-box
- Use Traefik for simple, Docker-based microservices
Nginx:
- Web server, reverse proxy, and load balancer
- Slightly faster than Traefik
- Use Nginx for complex services
Add a new folder called "traefik" to the "services" directory along with the following files:
traefik
├── Dockerfile.traefik
├── traefik.dev.toml
└── traefik.prod.toml
Your project structure should now look like this:
├── docker-compose.prod.yml
├── docker-compose.yml
└── services
├── traefik
│ ├── Dockerfile.traefik
│ ├── traefik.dev.toml
│ └── traefik.prod.toml
└── web
├── Dockerfile
├── Dockerfile.prod
├── manage.py
├── project
│ ├── __init__.py
│ └── config.py
└── requirements.txt
Add the following to traefik.dev.toml:
# listen on port 80
[entryPoints]
[entryPoints.web]
address = ":80"
# Traefik dashboard over http
[api]
insecure = true
[log]
level = "DEBUG"
[accessLog]
# containers are not discovered automatically
[providers]
[providers.docker]
exposedByDefault = false
Here, since we don't want to expose the db
service, we set exposedByDefault to false
. To manually expose a service we can add the "traefik.enable=true"
label to the Docker Compose file.
Next, update the docker-compose.yml file so that our web
service is discovered by Traefik and add a new traefik
service:
version: '3.8'
services:
web:
build: ./services/web
command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; python manage.py run -h 0.0.0.0'
volumes:
- ./services/web/:/app
expose: # new
- 5000
environment:
- FLASK_APP=project/__init__.py
- FLASK_ENV=development
- DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_dev
depends_on:
- db
labels: # new
- "traefik.enable=true"
- "traefik.http.routers.flask.rule=Host(`flask.localhost`)"
db:
image: postgres:13-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_flask
- POSTGRES_PASSWORD=hello_flask
- POSTGRES_DB=hello_flask_dev
traefik: # new
image: traefik:v2.2
ports:
- 80:80
- 8081:8080
volumes:
- "./services/traefik/traefik.dev.toml:/etc/traefik/traefik.toml"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
volumes:
postgres_data:
First, the web
service is only exposed to other containers on port 5000
. We also added the following labels to the web
service:
traefik.enable=true
enables Traefik to discover the servicetraefik.http.routers.flask.rule=Host(`flask.localhost`)
when the request has Host=flask.localhost
, the request is redirected to this serviceTake note of the volumes within the traefik
service:
./services/traefik/traefik.dev.toml:/etc/traefik/traefik.toml
maps the local config file to the config file in the container so that the settings are kept in sync/var/run/docker.sock:/var/run/docker.sock:ro
enables Traefik to discover other containersTo test, first bring down any existing containers:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml down -v
Build the new development images and spin up the containers:
$ docker-compose up -d --build
Create the table and apply the seed:
$ docker-compose exec web python manage.py create_db
$ docker-compose exec web python manage.py seed_db
Navigate to http://flask.localhost. You should see:
[
{
"active": true,
"email": "michael@mherman.org",
"id": 1
},
{
"active": true,
"email": "test@example.com",
"id": 2
}
]
You can test via cURL as well:
$ curl -H Host:flask.localhost http://0.0.0.0
Next, checkout the dashboard at http://flask.localhost:8081:
Bring the containers and volumes down once done:
$ docker-compose down -v
We've successfully created a working example of Flask, Docker, and Traefik in development mode. For production, you'll want to configure Traefik to manage TLS certificates via Let's Encrypt. In short, Traefik will automatically contact the certificate authority to issue and renew certificates.
Since Let's Encrypt won't issue certificates for localhost
, you'll need to spin up your production containers on a cloud compute instance (like a DigitalOcean droplet or an AWS EC2 instance). You'll also need a valid domain name. If you don't have one, you can create a free domain at Freenom.
We used a DigitalOcean droplet along with Docker machine to quickly provision a compute instance with Docker and deployed the production containers to test out the Traefik config. Check out the DigitalOcean example from the Docker docs for more on using Docker Machine to provision a droplet.
Assuming you configured a compute instance and set up a free domain, you're now ready to set up Traefik in production mode.
Start by adding a production version of the Traefik config to traefik.prod.toml:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.http]
[entryPoints.web.http.redirections]
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
[entryPoints.websecure]
address = ":443"
[accessLog]
[api]
dashboard = true
[providers]
[providers.docker]
exposedByDefault = false
[certificatesResolvers.letsencrypt.acme]
email = "your@email.com"
storage = "/certificates/acme.json"
[certificatesResolvers.letsencrypt.acme.httpChallenge]
entryPoint = "web"
Make sure to replace
your@email.com
with your actual email address.
What's happening here:
entryPoints.web
sets the entry point for our insecure HTTP application to port 80entryPoints.websecure
sets the entry point for our secure HTTPS application to port 443entryPoints.web.http.redirections.entryPoint
redirects all insecure requests to the secure portexposedByDefault = false
unexposes all servicesdashboard = true
enables the monitoring dashboardFinally, take note of:
[certificatesResolvers.letsencrypt.acme]
email = "your@email.com"
storage = "/certificates/acme.json"
[certificatesResolvers.letsencrypt.acme.httpChallenge]
entryPoint = "web"
This is where the Let's Encrypt config lives. We defined where the certificates will be stored along with the verification type, which is an HTTP Challenge.
Next, assuming you updated your domain name's DNS records, create two new A records that both point at your compute instance's public IP:
flask-traefik.your-domain.com
- for the web servicedashboard-flask-traefik.your-domain.com
- for the Traefik dashboardMake sure to replace
your-domain.com
with your actual domain.
Next, update docker-compose.prod.yml like so:
version: '3.8'
services:
web:
build:
context: ./services/web
dockerfile: Dockerfile.prod
command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn --bind 0.0.0.0:5000 manage:app'
expose: # new
- 5000
environment:
- FLASK_APP=project/__init__.py
- FLASK_ENV=production
- DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_prod
depends_on:
- db
labels: # new
- "traefik.enable=true"
- "traefik.http.routers.flask.rule=Host(`flask-traefik.your-domain.com`)"
- "traefik.http.routers.flask.tls=true"
- "traefik.http.routers.flask.tls.certresolver=letsencrypt"
db:
image: postgres:13-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_flask
- POSTGRES_PASSWORD=hello_flask
- POSTGRES_DB=hello_flask_prod
traefik: # new
build:
context: ./services/traefik
dockerfile: Dockerfile.traefik
ports:
- 80:80
- 443:443
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik-public-certificates:/certificates"
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`dashboard-flask-traefik.your-domain.com`)"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=testuser:$$apr1$$jIKW.bdS$$eKXe4Lxjgy/rH65wP1iQe1"
volumes:
postgres_data_prod:
traefik-public-certificates:
Again, make sure to replace
your-domain.com
with your actual domain.
What's new here?
In the web
service, we added the following labels:
traefik.http.routers.flask.rule=Host(`flask-traefik.your-domain.com`)
changes the host to the actual domaintraefik.http.routers.flask.tls=true
enables HTTPStraefik.http.routers.flask.tls.certresolver=letsencrypt
sets the certificate issuer as Let's EncryptNext, for the traefik
service, we added the appropriate ports and a volume for the certificates directory. The volume ensures that the certificates persist even if the container is brought down.
As for the labels:
traefik.http.routers.dashboard.rule=Host(`dashboard-flask-traefik.your-domain.com`)
defines the dashboard host, so it can can be accessed at $Host/dashboard/
traefik.http.routers.dashboard.tls=true
enables HTTPStraefik.http.routers.dashboard.tls.certresolver=letsencrypt
sets the certificate resolver to Let's Encrypttraefik.http.routers.dashboard.middlewares=auth
enables HTTP BasicAuth
middlewaretraefik.http.middlewares.auth.basicauth.users
defines the username and hashed password for logging inYou can create a new password hash using the htpasswd utility:
# username: testuser
# password: password
$ echo $(htpasswd -nb testuser password) | sed -e s/\\$/\\$\\$/g
testuser:$$apr1$$jIKW.bdS$$eKXe4Lxjgy/rH65wP1iQe1
Feel free to use an env_file
to store the username and password as environment variables
USERNAME=testuser
HASHED_PASSWORD=$$apr1$$jIKW.bdS$$eKXe4Lxjgy/rH65wP1iQe1
Update Dockerfile.traefik:
FROM traefik:v2.2
COPY ./traefik.prod.toml ./etc/traefik/traefik.toml
Next, spin up the new container:
$ docker-compose -f docker-compose.prod.yml up -d --build
Create the table and apply the seed:
$ docker-compose -f docker-compose.prod.yml exec web python manage.py create_db
$ docker-compose -f docker-compose.prod.yml exec web python manage.py seed_db
Ensure the two URLs work:
Also, make sure that when you access the HTTP versions of the above URLs, you're redirected to the HTTPS versions.
Finally, Let's Encrypt certificates have a validity of 90 days. Treafik will automatically handle renewing the certificates for you behind the scenes, so that's one less thing you'll have to worry about!
In this tutorial, we walked through how to containerize a Flask application with Postgres for development. We also created a production-ready Docker Compose file, set up Traefik and Let's Encrypt to serve the application via HTTPS, and enabled a secure dashboard to monitor our services.
In terms of actual deployment to a production environment, you'll probably want to use a:
You can find the code in the flask-docker-traefik repo.
Original article source at: https://testdriven.io/
1662632280
In this Python article, let's learn about Servers: 6 Python Servers Libraries Useful for Developers
Python Server Pages (PSP) is a name used by several different implementations of server-side script engines for creating dynamically-generated web pages by embedding Python in HTML. For example, an implementation of Python Server Pages was released with mod_python 3.1 in 2004.
Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. It's a pre-fork worker model ported from Ruby's Unicorn project. The Gunicorn server is broadly compatible with various web frameworks, simply implemented, light on server resource usage, and fairly speedy.
Feel free to join us in #gunicorn on Libera.chat.
The documentation is hosted at https://docs.gunicorn.org.
Gunicorn requires Python 3.x >= 3.5.
Install from PyPI:
$ pip install gunicorn
Basic usage:
$ gunicorn [OPTIONS] APP_MODULE
Where APP_MODULE
is of the pattern $(MODULE_NAME):$(VARIABLE_NAME)
. The module name can be a full dotted path. The variable name refers to a WSGI callable that should be found in the specified module.
Example with test app:
$ cd examples
$ gunicorn --workers=2 test:app
Read the documentation online at http://www.gevent.org.
Post issues on the bug tracker, discuss and ask open ended questions on the mailing list, and find announcements and information on the blog and twitter (@gevent).
An ASGI web server, for Python.
Requirements: Python 3.7+ (For Python 3.6 support, install version 0.16.0.)
Uvicorn is an ASGI web server implementation for Python.
Until recently Python has lacked a minimal low-level server/application interface for async frameworks. The ASGI specification fills this gap, and means we're now able to start building a common set of tooling usable across all async frameworks.
Uvicorn supports HTTP/1.1 and WebSockets.
Install using pip
:
$ pip install uvicorn
This will install uvicorn with minimal (pure Python) dependencies.
$ pip install uvicorn[standard]
This will install uvicorn with "Cython-based" dependencies (where possible) and other "optional extras".
In this context, "Cython-based" means the following:
uvloop
will be installed and used if possible.httptools
if possible.Moreover, "optional extras" means that:
websockets
(should you want to use wsproto
you'd need to install it manually) if possible.--reload
flag in development mode will use watchfiles
.colorama
installed for the colored logs.python-dotenv
will be installed should you want to use the --env-file
option.PyYAML
will be installed to allow you to provide a .yaml
file to --log-config
, if desired.Create an application, in example.py
:
async def app(scope, receive, send):
assert scope['type'] == 'http'
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
})
await send({
'type': 'http.response.body',
'body': b'Hello, world!',
})
Run the server:
$ uvicorn example:app
Radically simplified static file serving for Python web apps
With a couple of lines of config WhiteNoise allows your web app to serve its own static files, making it a self-contained unit that can be deployed anywhere without relying on nginx, Amazon S3 or any other external service. (Especially useful on Heroku, OpenShift and other PaaS providers.)
It's designed to work nicely with a CDN for high-traffic sites so you don't have to sacrifice performance to benefit from simplicity.
WhiteNoise works with any WSGI-compatible app but has some special auto-configuration features for Django.
WhiteNoise takes care of best-practices for you, for instance:
Worried that serving static files with Python is horribly inefficient? Still think you should be using Amazon S3? Have a look at the Infrequently Asked Questions.
ASGI is a standard for Python asynchronous web apps and servers to communicate with each other, and positioned as an asynchronous successor to WSGI. You can read more at https://asgi.readthedocs.io/en/latest/
This package includes ASGI base libraries, such as:
asgiref.sync
asgiref.server
asgiref.wsgi
These allow you to wrap or decorate async or sync functions to call them from the other style (so you can call async functions from a synchronous thread, or vice-versa).
In particular:
The idea is to make it easier to call synchronous APIs from async code and asynchronous APIs from synchronous code so it's easier to transition code from one style to the other. In the case of Channels, we wrap the (synchronous) Django view system with SyncToAsync to allow it to run inside the (asynchronous) ASGI server.
Note that exactly what threads things run in is very specific, and aimed to keep maximum compatibility with old synchronous code. See "Synchronous code & Threads" below for a full explanation. By default, sync_to_async
will run all synchronous code in the program in the same thread for safety reasons; you can disable this for more performance with @sync_to_async(thread_sensitive=False)
, but make sure that your code does not rely on anything bound to threads (like database connections) when you do.
Reload webpages on changes, without hitting refresh in your browser.
LiveReload is for web developers who know Python. It is available on PyPI.
$ pip install livereload
How To Create a Simple Web Server Using Python and the http.server Module
#python #Servers
1661119860
This is a step-by-step tutorial that details how to configure Masonite, a Python-based web framework, to run on Docker with Postgres. For production environments, we'll add Nginx and Gunicorn. We'll also take a look at how to serve static and user-uploaded media files via Nginx.
Source: https://testdriven.io
1661112600
Đây là hướng dẫn từng bước trình bày chi tiết cách định cấu hình Masonite, một khuôn khổ web dựa trên Python, để chạy trên Docker với Postgres. Đối với môi trường sản xuất, chúng tôi sẽ thêm Nginx và Gunicorn. Chúng tôi cũng sẽ xem xét cách phân phát các tệp phương tiện tĩnh và do người dùng tải lên thông qua Nginx.
Sự phụ thuộc :
Tạo một thư mục dự án, cài đặt Masonite và tạo một dự án Masonite mới:
$ mkdir masonite-on-docker && cd masonite-on-docker
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install masonite==4.16.2
(env)$ project start web
(env)$ cd web
(env)$ project install
(env)$ python craft serve
Điều hướng đến http: // localhost: 8000 / để xem màn hình chào mừng Masonite. Diệt máy chủ và thoát khỏi môi trường ảo sau khi hoàn tất. Tiếp tục và xóa môi trường ảo. Bây giờ chúng tôi có một dự án Masonite đơn giản để làm việc.
Tiếp theo, trước khi thêm Docker, hãy dọn dẹp cấu trúc dự án một chút:
Cấu trúc dự án của bạn bây giờ sẽ giống như sau:
├── .env.dev
└── web
├── .env.testing
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Cài đặt Docker , nếu bạn chưa có, sau đó thêm Dockerfile vào thư mục "web":
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Vì vậy, chúng tôi đã bắt đầu với hình ảnh Docker dựa trên Alpine cho Python 3.10.5. Sau đó, chúng tôi đặt một thư mục làm việc cùng với ba biến môi trường:
PYTHONDONTWRITEBYTECODE
: Ngăn Python ghi tệp pyc vào đĩa (tương đương với python -B
tùy chọn )PYTHONUNBUFFERED
: Ngăn Python lưu vào bộ đệm stdout và stderr (tương đương với python -u
tùy chọn )TZ=UTC
đặt múi giờ trong vùng chứa thành UTC, múi giờ này được yêu cầu để ghi nhật kýTiếp theo, chúng tôi đã cài đặt một số phụ thuộc cấp hệ thống cần thiết cho Python. Hãy lưu ý đến openssl-dev
và các cargo
phụ thuộc. Đây là những yêu cầu bắt buộc vì thư viện mật mã bây giờ phụ thuộc vào Rust . Để biết thêm, hãy xem lại Xây dựng mật mã trên Linux .
Cuối cùng, chúng tôi đã cập nhật Pip, sao chép qua tệp tin request.txt , cài đặt các phần phụ thuộc và sao chép qua chính ứng dụng Masonite.
Xem lại Các phương pháp hay nhất của Docker dành cho nhà phát triển Python để biết thêm về cấu trúc Dockerfiles cũng như một số phương pháp hay nhất để định cấu hình Docker cho phát triển dựa trên Python.
Tiếp theo, thêm tệp docker-compost.yml vào thư mục gốc của dự án:
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
Xem lại tài liệu tham khảo Soạn tệp để biết thông tin về cách tệp này hoạt động.
Hãy đơn giản hóa .env.dev bằng cách loại bỏ bất kỳ biến nào không sử dụng:
APP_DEBUG=True
APP_ENV=local
APP_KEY=zWDMwC0aNfVk8Ao1NyVJC_LiGD9tHJtVn_uCPeaaTNY=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=sqlite
SQLITE_DB_DATABASE=masonite.sqlite3
DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=masonite
DB_PORT=3306
DB_LOG=True
QUEUE_DRIVER=async
Xây dựng hình ảnh:
$ docker-compose build
Sau khi hình ảnh được tạo, hãy chạy vùng chứa:
$ docker-compose up -d
Điều hướng đến http: // localhost: 8000 / để xem lại màn hình chào mừng.
Kiểm tra lỗi trong nhật ký nếu điều này không hoạt động thông qua
docker-compose logs -f
.
Để kiểm tra tính năng tự động tải lại, trước tiên hãy mở nhật ký Docker - docker-compose logs -f
- và sau đó thực hiện thay đổi đối với web / route / web.py cục bộ:
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show")
]
Ngay sau khi lưu, bạn sẽ thấy ứng dụng tải lại trong thiết bị đầu cuối của mình như sau:
* Detected change in '/usr/src/app/routes/web.py', reloading
* Restarting with watchdog (inotify)
Đảm bảo http: // localhost: 8000 / sample hoạt động như mong đợi.
Để định cấu hình Postgres, chúng tôi sẽ cần thêm một dịch vụ mới vào tệp docker -compos.yml , cập nhật các biến môi trường và cài đặt Psycopg2 .
Đầu tiên, hãy thêm một dịch vụ mới có tên là db
docker -omp.yml :
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_dev:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_masonite
- POSTGRES_PASSWORD=hello_masonite
- POSTGRES_DB=hello_masonite_dev
volumes:
postgres_data_dev:
Để duy trì dữ liệu ngoài vòng đời của vùng chứa, chúng tôi đã định cấu hình một ổ đĩa. Cấu hình này sẽ liên kết postgres_data_dev
với thư mục "/ var / lib / postgresql / data /" trong vùng chứa.
Chúng tôi cũng đã thêm một khóa môi trường để xác định tên cho cơ sở dữ liệu mặc định và đặt tên người dùng và mật khẩu.
Xem lại phần "Biến môi trường" của trang Postgres Docker Hub để biết thêm thông tin.
Cũng cập nhật các biến môi trường liên quan đến cơ sở dữ liệu sau trong tệp .env.dev :
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_dev
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
Xem lại tệp web / config / database.py để biết thông tin về cách cấu hình cơ sở dữ liệu dựa trên các biến môi trường đã xác định cho dự án Masonite.
Cập nhật Dockerfile để cài đặt các gói thích hợp cần thiết cho Psycopg2:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Thêm Psycopg2 vào web / request.txt :
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
Xem lại Sự cố GitHub này để biết thêm thông tin về cách cài đặt Psycopg2 trong Hình ảnh Docker dựa trên Alpine.
Xây dựng hình ảnh mới và xoay hai vùng chứa:
$ docker-compose up -d --build
Áp dụng di chuyển (từ thư mục "web / cơ sở dữ liệu / di chuyển"):
$ docker-compose exec web python craft migrate
Bạn nên thấy:
Migrating: 2021_01_09_033202_create_password_reset_table
Migrated: 2021_01_09_033202_create_password_reset_table (0.01s)
Migrating: 2021_01_09_043202_create_users_table
Migrated: 2021_01_09_043202_create_users_table (0.02s)
Đảm bảo users
bảng đã được tạo:
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------------+----------------+----------+------------+------------+-----------------------------------
hello_masonite_dev | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
template1 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
(4 rows)
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------+-------+----------------
public | migrations | table | hello_masonite
public | password_resets | table | hello_masonite
public | users | table | hello_masonite
(3 rows)
hello_masonite_dev=# \q
Bạn có thể kiểm tra xem ổ đĩa đã được tạo hay chưa bằng cách chạy:
$ docker volume inspect masonite-on-docker_postgres_data_dev
Bạn sẽ thấy một cái gì đó tương tự như:
[
{
"CreatedAt": "2022-07-22T20:07:50Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "masonite-on-docker",
"com.docker.compose.version": "2.6.1",
"com.docker.compose.volume": "postgres_data_dev"
},
"Mountpoint": "/var/lib/docker/volumes/masonite-on-docker_postgres_data_dev/_data",
"Name": "masonite-on-docker_postgres_data_dev",
"Options": null,
"Scope": "local"
}
]
Tiếp theo, thêm tệp entrypoint.sh vào thư mục "web" để xác minh rằng Postgres đã hoạt động tốt trước khi áp dụng di chuyển và chạy máy chủ phát triển Masonite:
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python craft migrate:refresh # you may want to remove this
python craft migrate
exec "$@"
Lưu ý các biến môi trường.
Sau đó, cập nhật Dockerfile để chạy tệp entrypoint.sh dưới dạng lệnh Docker entrypoint :
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Cập nhật các quyền đối với tệp cục bộ:
$ chmod +x web/entrypoint.sh
Kiểm tra nó một lần nữa:
Bạn muốn gieo một số người dùng?
$ docker-compose exec web python craft seed:run
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# select count(*) from users;
count
-------
1
(1 row)
hello_masonite_dev=# \q
Tiếp theo, đối với môi trường sản xuất, hãy thêm Gunicorn , một máy chủ WSGI cấp sản xuất, vào tệp yêu cầu:
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
gunicorn==20.1.0
Vì chúng tôi vẫn muốn sử dụng máy chủ tích hợp của Masonite trong quá trình phát triển, hãy tạo một tệp soạn mới có tên là docker-compos.prod.yml để sản xuất:
version: '3.8'
services:
web:
build: ./web
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
volumes:
postgres_data_prod:
Nếu bạn có nhiều môi trường, bạn có thể muốn xem xét bằng cách sử dụng tệp cấu hình docker-compos.override.yml . Với cách tiếp cận này, bạn sẽ thêm cấu hình cơ sở của mình vào tệp docker-compos.yml và sau đó sử dụng tệp docker-compile.override.yml để ghi đè các cài đặt cấu hình đó dựa trên môi trường.
Hãy lưu ý về mặc định command
. Chúng tôi đang chạy Gunicorn thay vì máy chủ phát triển Masonite. Chúng tôi cũng đã xóa khối lượng khỏi web
dịch vụ vì chúng tôi không cần nó trong quá trình sản xuất. Cuối cùng, chúng tôi đang sử dụng các tệp biến môi trường riêng biệt để xác định các biến môi trường cho cả hai dịch vụ sẽ được chuyển đến vùng chứa trong thời gian chạy.
.env.prod:
APP_DEBUG=False
APP_ENV=prod
APP_KEY=GM28x-FeI1sM72tgtsgikLcT-AryyVOiY8etOGr7q7o=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_prod
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
DB_LOG=True
QUEUE_DRIVER=async
.env.prod.db :
POSTGRES_USER=hello_masonite
POSTGRES_PASSWORD=hello_masonite
POSTGRES_DB=hello_masonite_prod
Thêm hai tệp vào thư mục gốc của dự án. Có thể bạn sẽ muốn giữ chúng ngoài tầm kiểm soát phiên bản, vì vậy hãy thêm chúng vào tệp .gitignore .
Đưa xuống các vùng chứa phát triển (và các tập được liên kết với -v
cờ):
$ docker-compose down -v
Sau đó, xây dựng các hình ảnh sản xuất và quay các vùng chứa:
$ docker-compose -f docker-compose.prod.yml up -d --build
Xác minh rằng hello_masonite_prod
cơ sở dữ liệu đã được tạo cùng với users
bảng. Thử nghiệm http: // localhost: 8000 / .
Một lần nữa, nếu vùng chứa không khởi động được, hãy kiểm tra lỗi trong nhật ký qua
docker-compose -f docker-compose.prod.yml logs -f
.
Bạn có nhận thấy rằng chúng tôi vẫn đang chạy các lệnh di chuyển: refresh (xóa cơ sở dữ liệu) và di chuyển các lệnh mỗi khi vùng chứa được chạy không? Điều này là tốt trong quá trình phát triển, nhưng hãy tạo một tệp entrypoint mới cho quá trình sản xuất.
entrypoint.prod.sh:
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Ngoài ra, thay vì tạo một tệp entrypoint mới, bạn có thể thay đổi tệp hiện có như sau:
#!/bin/sh if [ "$DB_CONNECTION" = "postgres" ] then echo "Waiting for postgres..." while ! nc -z $DB_HOST $DB_PORT; do sleep 0.1 done echo "PostgreSQL started" fi if [ "$APP_ENV" = "local" ] then echo "Refreshing the database..." craft migrate:refresh # you may want to remove this echo "Applying migrations..." craft migrate echo "Tables created" fi exec "$@"
Để sử dụng tệp này, hãy tạo một Dockerfile mới có tên Dockerfile.prod để sử dụng với các bản dựng sản xuất:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.10.5-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==4.0.1
COPY . .
RUN flake8 --ignore=E501,F401,E303,E402 .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.10.5-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# set environment variables
ENV TZ=UTC
# install dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
RUN chmod +x /home/app/web/entrypoint.prod.sh
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Ở đây, chúng tôi đã sử dụng một bản dựng nhiều giai đoạn Docker để giảm kích thước hình ảnh cuối cùng. Về cơ bản, builder
là một hình ảnh tạm thời được sử dụng để xây dựng các bánh xe Python. Các bánh xe sau đó được sao chép sang hình ảnh sản xuất cuối cùng và builder
hình ảnh bị loại bỏ.
Bạn có thể thực hiện thêm một bước nữa trong phương pháp xây dựng nhiều giai đoạn và sử dụng một Dockerfile duy nhất thay vì tạo hai Dockerfiles. Hãy nghĩ về ưu và nhược điểm của việc sử dụng phương pháp này trên hai tệp khác nhau.
Bạn có nhận thấy rằng chúng tôi đã tạo một người dùng không phải root không? Theo mặc định, Docker chạy các quy trình vùng chứa dưới dạng root bên trong vùng chứa. Đây là một thực tiễn xấu vì những kẻ tấn công có thể giành được quyền truy cập root vào máy chủ Docker nếu chúng thoát ra khỏi vùng chứa. Nếu bạn root trong vùng chứa, bạn sẽ root trên máy chủ.
Cập nhật web
dịch vụ trong tệp Dockerfile.prod.yml để xây dựng với Dockerfile.prod :
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
Hãy dùng thử:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Tiếp theo, hãy thêm Nginx vào hỗn hợp để hoạt động như một proxy ngược cho Gunicorn để xử lý các yêu cầu của khách hàng cũng như cung cấp các tệp tĩnh.
Thêm dịch vụ vào docker-compile.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Sau đó, trong thư mục gốc của dự án cục bộ, hãy tạo các tệp và thư mục sau:
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile :
FROM nginx:1.23.1-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Xem lại Tìm hiểu Cấu trúc tệp cấu hình Nginx và các khung cảnh cấu hình để biết thêm thông tin về tệp cấu hình Nginx.
Sau đó, cập nhật web
dịch vụ, trong docker-compos.prod.yml , thay thế ports
bằng expose
:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
Bây giờ, cổng 8000 chỉ được tiếp xúc nội bộ với các dịch vụ Docker khác. Cổng sẽ không được xuất bản cho máy chủ nữa.
Để biết thêm về các cổng và hiển thị, hãy xem lại câu hỏi Stack Overflow này.
Kiểm tra nó một lần nữa.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Đảm bảo ứng dụng được thiết lập và chạy tại http: // localhost: 1337 .
Cấu trúc dự án của bạn bây giờ sẽ giống như sau:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
└── web
├── .env.testing
├── Dockerfile
├── Dockerfile.prod
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── entrypoint.prod.sh
├── entrypoint.sh
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Mang các vùng chứa xuống sau khi hoàn tất:
$ docker-compose -f docker-compose.prod.yml down -v
Vì Gunicorn là một máy chủ ứng dụng, nó sẽ không phục vụ các tệp tĩnh. Vì vậy, làm thế nào để xử lý cả tệp tĩnh và tệp phương tiện trong cấu hình cụ thể này?
Đầu tiên, hãy cập nhật STATICFILES
cấu hình trong web / config / filesystem.py :
STATICFILES = {
# folder # template alias
"storage/static": "static/",
"storage/compiled": "static/",
"storage/uploads": "uploads/",
"storage/public": "/",
}
Về cơ bản, tất cả các tệp tĩnh được lưu trữ trong thư mục "lưu trữ / tĩnh" (tệp CSS và JS thông thường) và "lưu trữ / biên dịch" (tệp SASS và LESS) sẽ được cung cấp từ /static/
URL.
Để kích hoạt tính năng biên dịch nội dung, giả sử bạn đã cài đặt NPM , hãy cài đặt các phần phụ thuộc:
$ cd web
$ npm install
Sau đó, để biên dịch nội dung, hãy chạy:
$ npm run dev
Để biết thêm về sự phức tạp của tài sản, hãy xem lại Biên dịch tài sản từ tài liệu Masonite.
Tiếp theo, để kiểm tra nội dung tĩnh thông thường, hãy thêm tệp văn bản có tên hello.txt vào "web / storage / static":
hi!
Để kiểm tra, trước tiên hãy xây dựng lại các hình ảnh và xoay các vùng chứa mới theo bình thường. Sau khi hoàn tất, hãy đảm bảo các nội dung tĩnh sau tải chính xác:
Đối với phiên bản sản xuất, hãy thêm một khối lượng vào web
và nginx
các dịch vụ trong docker-compile.prod.yml để mỗi vùng chứa sẽ chia sẻ thư mục "lưu trữ":
version: '3.8'
services:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
volumes:
- storage_volume:/home/app/web/storage
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
nginx:
build: ./nginx
volumes:
- storage_volume:/home/app/web/storage
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data_prod:
storage_volume:
Tiếp theo, cập nhật cấu hình Nginx để định tuyến các yêu cầu tệp tĩnh đến thư mục thích hợp:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Giờ đây, các yêu cầu đối với tệp tĩnh sẽ được phục vụ ngay lập tức:
Yêu cầu URL | Thư mục |
---|---|
/ static / * | "lưu trữ / tĩnh", "lưu trữ / biên dịch" |
/favicon.ico, /robots.txt | "lưu trữ / công khai" |
Xoay xuống các vùng chứa phát triển:
$ docker-compose down -v
Bài kiểm tra:
$ docker-compose -f docker-compose.prod.yml up -d --build
Một lần nữa, hãy đảm bảo các nội dung tĩnh sau được tải đúng cách:
Bạn cũng có thể xác minh trong nhật ký - thông qua docker-compose -f docker-compose.prod.yml logs -f
- rằng các yêu cầu đối với tệp tĩnh được phân phối thành công qua Nginx:
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:43 +0000] "GET /robots.txt HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:52 +0000] "GET /static/hello.txt HTTP/1.1" 200 4 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:59 +0000] "GET /static/css/app.css HTTP/1.1" 200 649 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
Mang theo các thùng chứa sau khi hoàn thành:
$ docker-compose -f docker-compose.prod.yml down -v
Để kiểm tra việc xử lý các tệp phương tiện do người dùng tải lên, hãy cập nhật khối nội dung trong mẫu web / template / welcome.html :
{% block content %}
<html>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<input type="file" name="image_upload">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
</body>
</html>
{% endblock %}
Thêm một phương thức mới được gọi upload
đến WelcomeController
trong web / app / controllers / WelcomeController.py :
def upload(self, storage: Storage, view: View, request: Request):
filename = storage.disk("local").put_file("image_upload", request.input("image_upload"))
return view.render("welcome", {"image_url": f"/framework/filesystem/{filename}"})
Đừng quên nhập khẩu:
from masonite.filesystem import Storage
from masonite.request import Request
Tiếp theo, kết nối bộ điều khiển với một tuyến mới trong web / lines / web.py :
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show"),
Route.post("/", "WelcomeController@upload"),
]
Bài kiểm tra:
$ docker-compose up -d --build
Bạn có thể tải lên hình ảnh tại http: // localhost: 8000 / , sau đó xem hình ảnh tại http: // localhost: 8000 / uploads / IMAGE_FILE_NAME .
Đối với sản xuất, hãy cập nhật cấu hình Nginx để định tuyến các yêu cầu tệp phương tiện đến thư mục "tải lên":
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location /uploads/ {
alias /home/app/web/storage/framework/filesystem/image_upload/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Xây dựng lại:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
Kiểm tra nó một lần cuối cùng:
Trong hướng dẫn này, chúng tôi đã giới thiệu cho các bạn cách chứa một ứng dụng Masonite với Postgres để phát triển. Chúng tôi cũng đã tạo tệp Docker Compose sẵn sàng sản xuất để thêm Gunicorn và Nginx vào hỗn hợp để xử lý tệp tĩnh và tệp phương tiện. Bây giờ bạn có thể kiểm tra thiết lập sản xuất cục bộ.
Về triển khai thực tế cho môi trường sản xuất, bạn có thể muốn sử dụng:
db
và nginx
các dịch vụBạn có thể tìm thấy mã trong repo masonite-on-docker .
Cảm ơn vì đã đọc!
Nguồn: https://testdriven.io
1661105160
Это пошаговое руководство, в котором подробно описано, как настроить Masonite, веб-фреймворк на основе Python, для работы в Docker с Postgres. Для производственных сред мы добавим Nginx и Gunicorn. Мы также рассмотрим, как обслуживать статические и загруженные пользователем медиафайлы через Nginx.
Зависимости :
Создайте каталог проекта, установите Masonite и создайте новый проект Masonite:
$ mkdir masonite-on-docker && cd masonite-on-docker
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install masonite==4.16.2
(env)$ project start web
(env)$ cd web
(env)$ project install
(env)$ python craft serve
Перейдите по адресу http://localhost:8000/ , чтобы просмотреть экран приветствия Masonite. Убейте сервер и выйдите из виртуальной среды, как только закончите. Идите вперед и удалите виртуальную среду. Теперь у нас есть простой проект Masonite для работы.
Далее, прежде чем добавлять Docker, давайте немного подчистим структуру проекта:
Теперь структура вашего проекта должна выглядеть так:
├── .env.dev
└── web
├── .env.testing
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Установите Docker , если у вас его еще нет, затем добавьте файл Dockerfile в каталог «web»:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Итак, мы начали с образа Docker на основе Alpine для Python 3.10.5. Затем мы устанавливаем рабочий каталог вместе с тремя переменными среды:
PYTHONDONTWRITEBYTECODE
: запрещает Python записывать файлы pyc на диск (эквивалентно python -B
опции )PYTHONUNBUFFERED
: запрещает Python буферизовать stdout и stderr (эквивалентно python -u
опции )TZ=UTC
устанавливает часовой пояс в контейнере в UTC, что необходимо для ведения журналаЗатем мы установили некоторые зависимости системного уровня, необходимые для Python. Обратите внимание на зависимости openssl-dev
и . cargo
Это необходимо, так как библиотека криптографии теперь зависит от Rust . Подробнее см. в статье Построение криптографии в Linux .
Наконец, мы обновили Pip, скопировали файл requirements.txt , установили зависимости и скопировали само приложение Masonite.
Ознакомьтесь с рекомендациями Docker для разработчиков Python , чтобы узнать больше о структурировании файлов Docker, а также о некоторых рекомендациях по настройке Docker для разработки на основе Python.
Затем добавьте файл docker-compose.yml в корень проекта:
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
Просмотрите справочник по файлу Compose, чтобы узнать, как работает этот файл.
Давайте упростим .env.dev , удалив все неиспользуемые переменные:
APP_DEBUG=True
APP_ENV=local
APP_KEY=zWDMwC0aNfVk8Ao1NyVJC_LiGD9tHJtVn_uCPeaaTNY=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=sqlite
SQLITE_DB_DATABASE=masonite.sqlite3
DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=masonite
DB_PORT=3306
DB_LOG=True
QUEUE_DRIVER=async
Создайте образ:
$ docker-compose build
После сборки образа запустите контейнер:
$ docker-compose up -d
Перейдите по адресу http://localhost:8000/ , чтобы снова просмотреть экран приветствия.
Проверьте наличие ошибок в журналах, если это не работает через
docker-compose logs -f
.
Чтобы протестировать автоматическую перезагрузку, сначала откройте журналы Docker -- docker-compose logs -f
-- и затем внесите изменения в файл web/routes/web.py локально:
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show")
]
Как только вы сохраните, вы должны увидеть, как приложение перезагружается в вашем терминале, например:
* Detected change in '/usr/src/app/routes/web.py', reloading
* Restarting with watchdog (inotify)
Убедитесь , что http://localhost:8000/sample работает должным образом.
Чтобы настроить Postgres, нам нужно добавить новую службу в файл docker-compose.yml , обновить переменные среды и установить Psycopg2 .
Во- первых, добавьте новую службу с именем db
в docker-compose.yml :
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_dev:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_masonite
- POSTGRES_PASSWORD=hello_masonite
- POSTGRES_DB=hello_masonite_dev
volumes:
postgres_data_dev:
Чтобы сохранить данные после окончания срока службы контейнера, мы настроили том. Эта конфигурация будет привязана postgres_data_dev
к каталогу «/var/lib/postgresql/data/» в контейнере.
Мы также добавили ключ среды для определения имени базы данных по умолчанию и установки имени пользователя и пароля.
Дополнительные сведения см. в разделе «Переменные среды» на странице Postgres Docker Hub .
Также обновите следующие переменные среды, связанные с базой данных, в файле .env.dev :
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_dev
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
Просмотрите файл web/config/database.py для получения информации о том, как база данных настроена на основе определенных переменных среды для проекта Masonite.
Обновите Dockerfile, чтобы установить соответствующие пакеты, необходимые для Psycopg2:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Добавьте Psycopg2 в web/requirements.txt :
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
Просмотрите этот выпуск GitHub для получения дополнительной информации об установке Psycopg2 в образе Docker на основе Alpine.
Создайте новый образ и запустите два контейнера:
$ docker-compose up -d --build
Примените миграции (из папки "web/databases/migrations"):
$ docker-compose exec web python craft migrate
Тебе следует увидеть:
Migrating: 2021_01_09_033202_create_password_reset_table
Migrated: 2021_01_09_033202_create_password_reset_table (0.01s)
Migrating: 2021_01_09_043202_create_users_table
Migrated: 2021_01_09_043202_create_users_table (0.02s)
Убедитесь, что users
таблица создана:
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------------+----------------+----------+------------+------------+-----------------------------------
hello_masonite_dev | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
template1 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
(4 rows)
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------+-------+----------------
public | migrations | table | hello_masonite
public | password_resets | table | hello_masonite
public | users | table | hello_masonite
(3 rows)
hello_masonite_dev=# \q
Вы также можете проверить, что том был создан, запустив:
$ docker volume inspect masonite-on-docker_postgres_data_dev
Вы должны увидеть что-то похожее на:
[
{
"CreatedAt": "2022-07-22T20:07:50Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "masonite-on-docker",
"com.docker.compose.version": "2.6.1",
"com.docker.compose.volume": "postgres_data_dev"
},
"Mountpoint": "/var/lib/docker/volumes/masonite-on-docker_postgres_data_dev/_data",
"Name": "masonite-on-docker_postgres_data_dev",
"Options": null,
"Scope": "local"
}
]
Затем добавьте файл entrypoint.sh в каталог «web», чтобы убедиться, что Postgres запущен и работоспособен , прежде чем применять миграции и запускать сервер разработки Masonite:
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python craft migrate:refresh # you may want to remove this
python craft migrate
exec "$@"
Обратите внимание на переменные окружения.
Затем обновите Dockerfile, чтобы запустить файл entrypoint.sh в качестве команды точки входа Docker :
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Обновите права доступа к файлу локально:
$ chmod +x web/entrypoint.sh
Проверьте еще раз:
Хотите посеять несколько пользователей?
$ docker-compose exec web python craft seed:run
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# select count(*) from users;
count
-------
1
(1 row)
hello_masonite_dev=# \q
Двигаясь дальше, для производственных сред давайте добавим Gunicorn , сервер WSGI производственного уровня, в файл требований:
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
gunicorn==20.1.0
Поскольку мы по-прежнему хотим использовать встроенный сервер Masonite в разработке, создайте новый файл компоновки с именем docker-compose.prod.yml для производства:
version: '3.8'
services:
web:
build: ./web
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
volumes:
postgres_data_prod:
Если у вас несколько сред, вы можете рассмотреть использование файла конфигурации docker-compose.override.yml . При таком подходе вы добавляете свою базовую конфигурацию в файл docker-compose.yml , а затем используете файл docker-compose.override.yml для переопределения этих параметров конфигурации в зависимости от среды.
Обратите внимание на значение по умолчанию command
. Мы используем Gunicorn, а не сервер разработки Masonite. Мы также удалили том из web
службы, так как он нам не нужен в рабочей среде. Наконец, мы используем отдельные файлы переменных среды для определения переменных среды для обеих служб, которые будут переданы в контейнер во время выполнения.
.env.prod :
APP_DEBUG=False
APP_ENV=prod
APP_KEY=GM28x-FeI1sM72tgtsgikLcT-AryyVOiY8etOGr7q7o=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_prod
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
DB_LOG=True
QUEUE_DRIVER=async
.env.prod.db :
POSTGRES_USER=hello_masonite
POSTGRES_PASSWORD=hello_masonite
POSTGRES_DB=hello_masonite_prod
Добавьте два файла в корень проекта. Вы, вероятно, захотите сохранить их вне контроля версий, поэтому добавьте их в файл .gitignore .
Снесите контейнеры разработки (и связанные с ними тома с -v
флагом):
$ docker-compose down -v
Затем создайте производственные образы и разверните контейнеры:
$ docker-compose -f docker-compose.prod.yml up -d --build
Убедитесь, что hello_masonite_prod
база данных была создана вместе с users
таблицей. Проверьте http://localhost:8000/ .
Опять же, если контейнер не запускается, проверьте наличие ошибок в логах через
docker-compose -f docker-compose.prod.yml logs -f
.
Вы заметили, что мы по-прежнему выполняем команды migrate:refresh (которое очищает базу данных) и migrate при каждом запуске контейнера? Это нормально в разработке, но давайте создадим новый файл точки входа для производства.
точка входа.prod.sh :
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
В качестве альтернативы, вместо создания нового файла точки входа вы можете изменить существующий следующим образом:
#!/bin/sh if [ "$DB_CONNECTION" = "postgres" ] then echo "Waiting for postgres..." while ! nc -z $DB_HOST $DB_PORT; do sleep 0.1 done echo "PostgreSQL started" fi if [ "$APP_ENV" = "local" ] then echo "Refreshing the database..." craft migrate:refresh # you may want to remove this echo "Applying migrations..." craft migrate echo "Tables created" fi exec "$@"
Чтобы использовать этот файл, создайте новый файл Dockerfile с именем Dockerfile.prod для использования с производственными сборками:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.10.5-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==4.0.1
COPY . .
RUN flake8 --ignore=E501,F401,E303,E402 .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.10.5-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# set environment variables
ENV TZ=UTC
# install dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
RUN chmod +x /home/app/web/entrypoint.prod.sh
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Here, we used a Docker multi-stage build to reduce the final image size. Essentially, builder
is a temporary image that's used for building the Python wheels. The wheels are then copied over to the final production image and the builder
image is discarded.
You could take the multi-stage build approach a step further and use a single Dockerfile instead of creating two Dockerfiles. Think of the pros and cons of using this approach over two different files.
Вы заметили, что мы создали пользователя без полномочий root? По умолчанию Docker запускает процессы контейнера как root внутри контейнера. Это плохая практика, поскольку злоумышленники могут получить root-доступ к хосту Docker, если им удастся выйти из контейнера. Если вы root в контейнере, вы будете root на хосте.
Обновите web
службу в файле docker-compose.prod.yml для сборки с помощью Dockerfile.prod :
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
Попробуйте:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Затем давайте добавим Nginx, который будет выступать в качестве обратного прокси -сервера для Gunicorn для обработки клиентских запросов, а также для обслуживания статических файлов.
Добавьте сервис в docker-compose.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Затем в локальном корне проекта создайте следующие файлы и папки:
└── nginx
├── Dockerfile
└── nginx.conf
Докерфайл :
FROM nginx:1.23.1-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Ознакомьтесь с разделом «Понимание структуры файла конфигурации Nginx и контекстов конфигурации» , чтобы получить дополнительную информацию о файле конфигурации Nginx.
Затем обновите web
службу в docker-compose.prod.yml , заменив ports
на expose
:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
Теперь порт 8000 открыт только внутри, для других служб Docker. Порт больше не будет опубликован на хост-компьютере.
Чтобы узнать больше о портах и экспонировании, просмотрите этот вопрос о переполнении стека.
Проверьте это снова.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Убедитесь, что приложение запущено и работает по адресу http://localhost:1337 .
Теперь структура вашего проекта должна выглядеть так:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
└── web
├── .env.testing
├── Dockerfile
├── Dockerfile.prod
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── entrypoint.prod.sh
├── entrypoint.sh
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Когда закончите, опустите контейнеры:
$ docker-compose -f docker-compose.prod.yml down -v
Поскольку Gunicorn является сервером приложений, он не будет обслуживать статические файлы. Итак, как следует обрабатывать как статические, так и мультимедийные файлы в этой конкретной конфигурации?
Сначала обновите STATICFILES
конфигурацию в web/config/filesystem.py :
STATICFILES = {
# folder # template alias
"storage/static": "static/",
"storage/compiled": "static/",
"storage/uploads": "uploads/",
"storage/public": "/",
}
По сути, все статические файлы, хранящиеся в каталогах «storage/static» (обычные файлы CSS и JS) и «storage/compiled» (файлы SASS и LESS), будут обслуживаться по /static/
URL-адресу.
Чтобы включить компиляцию ресурсов, при условии, что у вас установлен NPM , установите зависимости:
$ cd web
$ npm install
Затем, чтобы скомпилировать активы, запустите:
$ npm run dev
Чтобы узнать больше об усложнении ассетов, ознакомьтесь с компиляцией ассетов из документации Masonite.
Затем, чтобы протестировать обычный статический ресурс, добавьте текстовый файл hello.txt в «web/storage/static»:
hi!
Для проверки сначала пересоберите образы и запустите новые контейнеры, как обычно. После этого убедитесь, что следующие статические ресурсы загружаются правильно:
Для производства добавьте том к службам web
and в файле docker-compose.prod.yml , чтобы каждый контейнер имел общий каталог «storage»:nginx
version: '3.8'
services:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
volumes:
- storage_volume:/home/app/web/storage
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
nginx:
build: ./nginx
volumes:
- storage_volume:/home/app/web/storage
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data_prod:
storage_volume:
Затем обновите конфигурацию Nginx, чтобы направлять запросы статических файлов в соответствующую папку:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Теперь запросы к статическим файлам будут обслуживаться соответствующим образом:
URL-адрес запроса | Папка |
---|---|
/статический/* | "хранилище/статическое", "хранилище/скомпилированное" |
/favicon.ico, /robots.txt | "хранение/общественное" |
Раскрутите контейнеры разработки:
$ docker-compose down -v
Тест:
$ docker-compose -f docker-compose.prod.yml up -d --build
Опять же, убедитесь, что следующие статические ресурсы загружены правильно:
Вы также можете проверить в журналах — через docker-compose -f docker-compose.prod.yml logs -f
— что запросы к статическим файлам успешно обрабатываются через Nginx:
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:43 +0000] "GET /robots.txt HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:52 +0000] "GET /static/hello.txt HTTP/1.1" 200 4 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:59 +0000] "GET /static/css/app.css HTTP/1.1" 200 649 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
Когда закончите, принесите контейнеры:
$ docker-compose -f docker-compose.prod.yml down -v
Чтобы проверить обработку загружаемых пользователем медиафайлов, обновите блок контента в шаблоне web/templates/welcome.html :
{% block content %}
<html>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<input type="file" name="image_upload">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
</body>
</html>
{% endblock %}
Добавьте новый метод, вызываемый в upload
web /app/controllers/WelcomeController.py :WelcomeController
def upload(self, storage: Storage, view: View, request: Request):
filename = storage.disk("local").put_file("image_upload", request.input("image_upload"))
return view.render("welcome", {"image_url": f"/framework/filesystem/{filename}"})
Не забывайте об импорте:
from masonite.filesystem import Storage
from masonite.request import Request
Затем подключите контроллер к новому маршруту в web/routes/web.py :
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show"),
Route.post("/", "WelcomeController@upload"),
]
Тест:
$ docker-compose up -d --build
Вы должны иметь возможность загрузить изображение по адресу http://localhost:8000/ , а затем просмотреть его по адресу http://localhost:8000/uploads/IMAGE_FILE_NAME .
Для производства обновите конфигурацию Nginx, чтобы направлять запросы медиафайлов в папку «uploads»:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location /uploads/ {
alias /home/app/web/storage/framework/filesystem/image_upload/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Перестроить:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
Проверьте это в последний раз:
В этом руководстве мы рассмотрели, как контейнеризировать приложение Masonite с помощью Postgres для разработки. Мы также создали готовый к работе файл Docker Compose, который добавляет в смесь Gunicorn и Nginx для обработки статических и мультимедийных файлов. Теперь вы можете протестировать производственную установку локально.
С точки зрения фактического развертывания в производственной среде вы, вероятно, захотите использовать:
db
иnginx
Вы можете найти код в репозитории masonite-on-docker .
Спасибо за чтение!
Источник: https://testdriven.io
1661097780
這是一個分步教程,詳細介紹瞭如何配置 Masonite(一個基於 Python 的 Web 框架)以使用 Postgres 在 Docker 上運行。對於生產環境,我們將添加 Nginx 和 Gunicorn。我們還將了解如何通過 Nginx 提供靜態和用戶上傳的媒體文件。
依賴項:
創建項目目錄,安裝 Masonite,新建 Masonite 項目:
$ mkdir masonite-on-docker && cd masonite-on-docker
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install masonite==4.16.2
(env)$ project start web
(env)$ cd web
(env)$ project install
(env)$ python craft serve
導航到http://localhost:8000/以查看 Masonite 歡迎屏幕。完成後終止服務器並退出虛擬環境。繼續並刪除虛擬環境。我們現在有一個簡單的 Masonite 項目可供使用。
接下來,在添加 Docker 之前,讓我們稍微清理一下項目結構:
您的項目結構現在應該如下所示:
├── .env.dev
└── web
├── .env.testing
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
安裝Docker,如果你還沒有它,那麼添加一個Dockerfile到“web”目錄:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
因此,我們從Python 3.10.5的基於Alpine的Docker 映像開始。然後我們設置一個工作目錄以及三個環境變量:
PYTHONDONTWRITEBYTECODE
: 防止 Python 將 pyc 文件寫入磁盤(相當於python -B
option)PYTHONUNBUFFERED
: 防止 Python 緩衝 stdout 和 stderr (相當於python -u
option )TZ=UTC
將容器中的時區設置為 UTC,這是日誌記錄所必需的接下來,我們安裝了 Python 所需的一些系統級依賴項。注意openssl-dev
和cargo
依賴項。這些是必需的,因為密碼庫現在依賴於 Rust。有關更多信息,請查看在 Linux 上構建密碼學。
最後,我們更新了 Pip,複製了requirements.txt文件,安裝了依賴項,並複制了 Masonite 應用程序本身。
Review Docker Best Practices for Python Developers for more on structuring Dockerfiles as well as some best practices for configuring Docker for Python-based development.
Next, add a docker-compose.yml file to the project root:
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
Review the Compose file reference for info on how this file works.
Let's simplify .env.dev by removing any unused variables:
APP_DEBUG=True
APP_ENV=local
APP_KEY=zWDMwC0aNfVk8Ao1NyVJC_LiGD9tHJtVn_uCPeaaTNY=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=sqlite
SQLITE_DB_DATABASE=masonite.sqlite3
DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=masonite
DB_PORT=3306
DB_LOG=True
QUEUE_DRIVER=async
Build the image:
$ docker-compose build
Once the image is built, run the container:
$ docker-compose up -d
Navigate to http://localhost:8000/ to again view the welcome screen.
Check for errors in the logs if this doesn't work via
docker-compose logs -f
.
要測試自動重新加載,首先打開 Docker 日誌——docker-compose logs -f
然後在本地更改web/routes/web.py :
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show")
]
保存後,您應該會在終端中看到應用重新加載,如下所示:
* Detected change in '/usr/src/app/routes/web.py', reloading
* Restarting with watchdog (inotify)
確保http://localhost:8000/sample按預期工作。
要配置 Postgres,我們需要在docker-compose.yml文件中添加一個新服務,更新環境變量,然後安裝Psycopg2。
首先,添加一個名為db
docker -compose.yml的新服務:
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_dev:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_masonite
- POSTGRES_PASSWORD=hello_masonite
- POSTGRES_DB=hello_masonite_dev
volumes:
postgres_data_dev:
為了在容器的生命週期之外持久化數據,我們配置了一個卷。此配置將綁定postgres_data_dev
到容器中的“/var/lib/postgresql/data/”目錄。
我們還添加了一個環境鍵來定義默認數據庫的名稱並設置用戶名和密碼。
查看Postgres Docker Hub 頁面的“環境變量”部分以獲取更多信息。
同時更新.env.dev文件中的以下與數據庫相關的環境變量:
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_dev
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
查看web/config/database.py文件,了解如何根據為 Masonite 項目定義的環境變量配置數據庫。
更新 Dockerfile 以安裝 Psycopg2 所需的相應軟件包:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
將 Psycopg2 添加到web/requirements.txt:
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
查看此 GitHub 問題以獲取有關在基於 Alpine 的 Docker 映像中安裝 Psycopg2 的更多信息。
構建新鏡像並啟動兩個容器:
$ docker-compose up -d --build
應用遷移(來自“web/databases/migrations”文件夾):
$ docker-compose exec web python craft migrate
你應該看到:
Migrating: 2021_01_09_033202_create_password_reset_table
Migrated: 2021_01_09_033202_create_password_reset_table (0.01s)
Migrating: 2021_01_09_043202_create_users_table
Migrated: 2021_01_09_043202_create_users_table (0.02s)
確保users
表已創建:
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------------+----------------+----------+------------+------------+-----------------------------------
hello_masonite_dev | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
template1 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
(4 rows)
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------+-------+----------------
public | migrations | table | hello_masonite
public | password_resets | table | hello_masonite
public | users | table | hello_masonite
(3 rows)
hello_masonite_dev=# \q
您可以通過運行以下命令來檢查卷是否也已創建:
$ docker volume inspect masonite-on-docker_postgres_data_dev
您應該會看到類似於以下內容的內容:
[
{
"CreatedAt": "2022-07-22T20:07:50Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "masonite-on-docker",
"com.docker.compose.version": "2.6.1",
"com.docker.compose.volume": "postgres_data_dev"
},
"Mountpoint": "/var/lib/docker/volumes/masonite-on-docker_postgres_data_dev/_data",
"Name": "masonite-on-docker_postgres_data_dev",
"Options": null,
"Scope": "local"
}
]
接下來,將entrypoint.sh文件添加到“web”目錄,以在應用遷移和運行 Masonite 開發服務器之前驗證 Postgres 是否已啟動且健康:
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python craft migrate:refresh # you may want to remove this
python craft migrate
exec "$@"
注意環境變量。
然後,更新 Dockerfile 以將entrypoint.sh文件作為 Docker入口點命令運行:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
在本地更新文件權限:
$ chmod +x web/entrypoint.sh
再次測試一下:
想要播種一些用戶?
$ docker-compose exec web python craft seed:run
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# select count(*) from users;
count
-------
1
(1 row)
hello_masonite_dev=# \q
繼續前進,對於生產環境,讓我們將生產級 WSGI 服務器Gunicorn添加到需求文件中:
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
gunicorn==20.1.0
由於我們仍然想在開發中使用 Masonite 的內置服務器,因此創建一個名為docker-compose.prod.yml的新 compose 文件用於生產:
version: '3.8'
services:
web:
build: ./web
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
volumes:
postgres_data_prod:
如果您有多個環境,您可能需要查看使用docker-compose.override.yml配置文件。使用這種方法,您可以將基本配置添加到docker-compose.yml文件中,然後使用docker-compose.override.yml文件根據環境覆蓋這些配置設置。
記下默認的command
. 我們正在運行 Gunicorn 而不是 Masonite 開發服務器。我們還從web
服務中刪除了該卷,因為我們在生產中不需要它。最後,我們使用單獨的環境變量文件為兩個服務定義環境變量,這些服務將在運行時傳遞給容器。
.env.prod:
APP_DEBUG=False
APP_ENV=prod
APP_KEY=GM28x-FeI1sM72tgtsgikLcT-AryyVOiY8etOGr7q7o=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_prod
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
DB_LOG=True
QUEUE_DRIVER=async
.env.prod.db:
POSTGRES_USER=hello_masonite
POSTGRES_PASSWORD=hello_masonite
POSTGRES_DB=hello_masonite_prod
將這兩個文件添加到項目根目錄。您可能希望將它們排除在版本控制之外,因此將它們添加到.gitignore文件中。
關閉開發容器(以及帶有標誌的相關卷):-v
$ docker-compose down -v
然後,構建生產映像並啟動容器:
$ docker-compose -f docker-compose.prod.yml up -d --build
驗證hello_masonite_prod
數據庫是否與users
表一起創建。測試http://localhost:8000/。
同樣,如果容器無法啟動,請通過 . 檢查日誌中的錯誤
docker-compose -f docker-compose.prod.yml logs -f
。
您是否注意到每次容器運行時我們仍在運行migrate:refresh (清除數據庫)和 migrate 命令?這在開發中很好,但是讓我們為生產創建一個新的入口點文件。
entrypoint.prod.sh:
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
或者,您可以像這樣更改現有的入口點文件,而不是創建新的入口點文件:
#!/bin/sh if [ "$DB_CONNECTION" = "postgres" ] then echo "Waiting for postgres..." while ! nc -z $DB_HOST $DB_PORT; do sleep 0.1 done echo "PostgreSQL started" fi if [ "$APP_ENV" = "local" ] then echo "Refreshing the database..." craft migrate:refresh # you may want to remove this echo "Applying migrations..." craft migrate echo "Tables created" fi exec "$@"
要使用此文件,請創建一個名為Dockerfile.prod的新 Dockerfile以用於生產構建:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.10.5-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==4.0.1
COPY . .
RUN flake8 --ignore=E501,F401,E303,E402 .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.10.5-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# set environment variables
ENV TZ=UTC
# install dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
RUN chmod +x /home/app/web/entrypoint.prod.sh
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
在這裡,我們使用了 Docker多階段構建來減小最終圖像的大小。本質上,builder
是用於構建 Python 輪子的臨時圖像。然後將輪子復製到最終的生產圖像中,然後builder
丟棄圖像。
您可以將多階段構建方法更進一步,使用單個Dockerfile而不是創建兩個 Dockerfile。想想在兩個不同的文件上使用這種方法的利弊。
您是否注意到我們創建了一個非 root 用戶?默認情況下,Docker 在容器內以 root 身份運行容器進程。這是一種不好的做法,因為如果攻擊者設法突破容器,他們可以獲得對 Docker 主機的 root 訪問權限。如果您是容器中的根,那麼您將是主機上的根。
更新docker-compose.prod.yml文件中的web
服務以使用Dockerfile.prod構建:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
試試看:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
接下來,讓我們將 Nginx 添加到組合中,作為 Gunicorn 的反向代理來處理客戶端請求以及提供靜態文件。
將服務添加到docker-compose.prod.yml:
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
然後,在本地項目根目錄中,創建以下文件和文件夾:
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile:
FROM nginx:1.23.1-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
查看了解 Nginx 配置文件結構和配置上下文以獲取有關 Nginx 配置文件的更多信息。
然後,在docker-compose.prod.ymlweb
中更新服務,替換為:portsexpose
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
現在,端口 8000 僅在內部公開給其他 Docker 服務。該端口將不再發佈到主機。
有關端口與公開的更多信息,請查看此Stack Overflow 問題。
再次測試一下。
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
確保應用程序在http://localhost:1337啟動並運行。
您的項目結構現在應該如下所示:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
└── web
├── .env.testing
├── Dockerfile
├── Dockerfile.prod
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── entrypoint.prod.sh
├── entrypoint.sh
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
完成後將容器放下:
$ docker-compose -f docker-compose.prod.yml down -v
由於 Gunicorn 是一個應用服務器,它不會提供靜態文件。那麼,在這個特定配置中應該如何處理靜態文件和媒體文件呢?
首先,更新web/config/filesystem.pySTATICFILES
中的配置:
STATICFILES = {
# folder # template alias
"storage/static": "static/",
"storage/compiled": "static/",
"storage/uploads": "uploads/",
"storage/public": "/",
}
本質上,存儲在“storage/static”(常規 CSS 和 JS 文件)和“storage/compiled”(SASS 和 LESS 文件)目錄中的所有靜態文件都將從/static/
URL 提供。
為了啟用資產編譯,假設您已安裝NPM,請安裝依賴項:
$ cd web
$ npm install
然後,要編譯資產,請運行:
$ npm run dev
有關資產複雜性的更多信息,請查看Masonite 文檔中的編譯資產。
接下來,要測試常規靜態資產,請將名為hello.txt的文本文件添加到“web/storage/static”:
hi!
要進行測試,首先要重新構建映像並按照慣例啟動新容器。完成後,確保正確加載以下靜態資產:
對於生產,向docker-compose.prod.ymlweb
中的和nginx
服務添加一個卷,以便每個容器共享“存儲”目錄:
version: '3.8'
services:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
volumes:
- storage_volume:/home/app/web/storage
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
nginx:
build: ./nginx
volumes:
- storage_volume:/home/app/web/storage
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data_prod:
storage_volume:
接下來,更新 Nginx 配置以將靜態文件請求路由到相應的文件夾:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
現在,將適當地提供對靜態文件的請求:
請求網址 | 文件夾 |
---|---|
/靜止的/* | “存儲/靜態”、“存儲/編譯” |
/favicon.ico, /robots.txt | “存儲/公共” |
降低開發容器的速度:
$ docker-compose down -v
測試:
$ docker-compose -f docker-compose.prod.yml up -d --build
同樣,確保正確加載以下靜態資產:
您還可以在日誌中驗證——通過docker-compose -f docker-compose.prod.yml logs -f
——對靜態文件的請求是否通過 Nginx 成功提供:
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:43 +0000] "GET /robots.txt HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:52 +0000] "GET /static/hello.txt HTTP/1.1" 200 4 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:59 +0000] "GET /static/css/app.css HTTP/1.1" 200 649 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
完成後帶上容器:
$ docker-compose -f docker-compose.prod.yml down -v
要測試對用戶上傳的媒體文件的處理,請更新web/templates/welcome.html模板中的內容塊:
{% block content %}
<html>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<input type="file" name="image_upload">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
</body>
</html>
{% endblock %}
在web/app/controllers/WelcomeController.pyupload
中添加一個新方法:WelcomeController
def upload(self, storage: Storage, view: View, request: Request):
filename = storage.disk("local").put_file("image_upload", request.input("image_upload"))
return view.render("welcome", {"image_url": f"/framework/filesystem/{filename}"})
不要忘記導入:
from masonite.filesystem import Storage
from masonite.request import Request
接下來,將控制器連接到web/routes/web.py中的新路由:
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show"),
Route.post("/", "WelcomeController@upload"),
]
測試:
$ docker-compose up -d --build
您應該能夠在http://localhost: 8000/ 上傳圖像,然後在http://localhost:8000/uploads/IMAGE_FILE_NAME中查看圖像。
對於生產,更新 Nginx 配置以將媒體文件請求路由到“上傳”文件夾:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location /uploads/ {
alias /home/app/web/storage/framework/filesystem/image_upload/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
重建:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
最後一次測試一下:
在本教程中,我們介紹瞭如何使用 Postgres 將 Masonite 應用程序容器化以進行開發。我們還創建了一個生產就緒的 Docker Compose 文件,將 Gunicorn 和 Nginx 添加到混合中以處理靜態和媒體文件。您現在可以在本地測試生產設置。
就實際部署到生產環境而言,您可能希望使用:
您可以在masonite-on-docker 存儲庫中找到代碼。
謝謝閱讀!
1661090040
Il s'agit d'un didacticiel étape par étape qui détaille comment configurer Masonite, un framework Web basé sur Python, pour qu'il s'exécute sur Docker avec Postgres. Pour les environnements de production, nous ajouterons Nginx et Gunicorn. Nous verrons également comment servir des fichiers multimédias statiques et téléchargés par l'utilisateur via Nginx.
Dépendances :
Créez un répertoire de projet, installez Masonite et créez un nouveau projet Masonite :
$ mkdir masonite-on-docker && cd masonite-on-docker
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install masonite==4.16.2
(env)$ project start web
(env)$ cd web
(env)$ project install
(env)$ python craft serve
Accédez à http://localhost:8000/ pour afficher l'écran de bienvenue de Masonite. Tuez le serveur et quittez l'environnement virtuel une fois terminé. Allez-y et supprimez également l'environnement virtuel. Nous avons maintenant un projet Masonite simple avec lequel travailler.
Ensuite, avant d'ajouter Docker, nettoyons un peu la structure du projet :
La structure de votre projet devrait maintenant ressembler à ceci :
├── .env.dev
└── web
├── .env.testing
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Installez Docker , si vous ne l'avez pas déjà, puis ajoutez un Dockerfile au répertoire "web":
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Nous avons donc commencé avec une image Docker basée sur Alpine pour Python 3.10.5 . Nous définissons ensuite un répertoire de travail avec trois variables d'environnement :
PYTHONDONTWRITEBYTECODE
: Empêche Python d'écrire des fichiers pyc sur le disque (équivalent à l' python -B
option )PYTHONUNBUFFERED
: Empêche Python de mettre en mémoire tampon stdout et stderr (équivalent à python -u
option )TZ=UTC
définit le fuseau horaire dans le conteneur sur UTC, ce qui est requis pour la journalisationEnsuite, nous avons installé certaines dépendances au niveau du système requises pour Python. Prenez note des dépendances openssl-dev
et . cargo
Celles-ci sont nécessaires car la bibliothèque de cryptographie dépend désormais de Rust . Pour en savoir plus, consultez Création de cryptographie sur Linux .
Enfin, nous avons mis à jour Pip, copié le fichier requirements.txt , installé les dépendances et copié l'application Masonite elle-même.
Consultez les bonnes pratiques Docker pour les développeurs Python pour en savoir plus sur la structuration des fichiers Docker, ainsi que sur certaines bonnes pratiques de configuration de Docker pour le développement basé sur Python.
Ensuite, ajoutez un fichier docker-compose.yml à la racine du projet :
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
Consultez la référence du fichier Compose pour plus d'informations sur le fonctionnement de ce fichier.
Simplifions .env.dev en supprimant toutes les variables inutilisées :
APP_DEBUG=True
APP_ENV=local
APP_KEY=zWDMwC0aNfVk8Ao1NyVJC_LiGD9tHJtVn_uCPeaaTNY=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=sqlite
SQLITE_DB_DATABASE=masonite.sqlite3
DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=masonite
DB_PORT=3306
DB_LOG=True
QUEUE_DRIVER=async
Construisez l'image :
$ docker-compose build
Une fois l'image créée, exécutez le conteneur :
$ docker-compose up -d
Accédez à http://localhost:8000/ pour afficher à nouveau l'écran de bienvenue.
Vérifiez les erreurs dans les journaux si cela ne fonctionne pas via
docker-compose logs -f
.
Pour tester le rechargement automatique, ouvrez d'abord les journaux Docker -- docker-compose logs -f
-- puis modifiez localement web/routes/web.py :
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show")
]
Dès que vous sauvegardez, vous devriez voir l'application se recharger dans votre terminal comme ceci :
* Detected change in '/usr/src/app/routes/web.py', reloading
* Restarting with watchdog (inotify)
Assurez -vous que http://localhost:8000/sample fonctionne comme prévu.
Pour configurer Postgres, nous devrons ajouter un nouveau service au fichier docker-compose.yml , mettre à jour les variables d'environnement et installer Psycopg2 .
Tout d'abord, ajoutez un nouveau service appelé db
à docker-compose.yml :
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_dev:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_masonite
- POSTGRES_PASSWORD=hello_masonite
- POSTGRES_DB=hello_masonite_dev
volumes:
postgres_data_dev:
Pour conserver les données au-delà de la durée de vie du conteneur, nous avons configuré un volume. Cette configuration se liera postgres_data_dev
au répertoire "/var/lib/postgresql/data/" dans le conteneur.
Nous avons également ajouté une clé d'environnement pour définir un nom pour la base de données par défaut et définir un nom d'utilisateur et un mot de passe.
Consultez la section "Variables d'environnement" de la page Postgres Docker Hub pour plus d'informations.
Mettez également à jour les variables d'environnement suivantes liées à la base de données dans le fichier .env.dev :
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_dev
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
Consultez le fichier web/config/database.py pour plus d'informations sur la configuration de la base de données en fonction des variables d'environnement définies pour le projet Masonite.
Mettez à jour le Dockerfile pour installer les packages appropriés requis pour Psycopg2 :
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Ajoutez Psycopg2 à web/requirements.txt :
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
Consultez ce problème GitHub pour plus d'informations sur l'installation de Psycopg2 dans une image Docker basée sur Alpine.
Créez la nouvelle image et faites tourner les deux conteneurs :
$ docker-compose up -d --build
Appliquez les migrations (depuis le dossier "web/databases/migrations") :
$ docker-compose exec web python craft migrate
Tu devrais voir:
Migrating: 2021_01_09_033202_create_password_reset_table
Migrated: 2021_01_09_033202_create_password_reset_table (0.01s)
Migrating: 2021_01_09_043202_create_users_table
Migrated: 2021_01_09_043202_create_users_table (0.02s)
Assurez-vous que la users
table a été créée :
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------------+----------------+----------+------------+------------+-----------------------------------
hello_masonite_dev | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
template1 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
(4 rows)
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------+-------+----------------
public | migrations | table | hello_masonite
public | password_resets | table | hello_masonite
public | users | table | hello_masonite
(3 rows)
hello_masonite_dev=# \q
Vous pouvez vérifier que le volume a également été créé en exécutant :
$ docker volume inspect masonite-on-docker_postgres_data_dev
Vous devriez voir quelque chose de similaire à :
[
{
"CreatedAt": "2022-07-22T20:07:50Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "masonite-on-docker",
"com.docker.compose.version": "2.6.1",
"com.docker.compose.volume": "postgres_data_dev"
},
"Mountpoint": "/var/lib/docker/volumes/masonite-on-docker_postgres_data_dev/_data",
"Name": "masonite-on-docker_postgres_data_dev",
"Options": null,
"Scope": "local"
}
]
Ensuite, ajoutez un fichier entrypoint.sh au répertoire "web" pour vérifier que Postgres est opérationnel et sain avant d'appliquer les migrations et d'exécuter le serveur de développement Masonite :
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python craft migrate:refresh # you may want to remove this
python craft migrate
exec "$@"
Prenez note des variables d'environnement.
Ensuite, mettez à jour le Dockerfile pour exécuter le fichier entrypoint.sh en tant que commande Docker entrypoint :
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Mettez à jour les autorisations de fichier localement :
$ chmod +x web/entrypoint.sh
Testez-le à nouveau :
Vous voulez ensemencer certains utilisateurs ?
$ docker-compose exec web python craft seed:run
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# select count(*) from users;
count
-------
1
(1 row)
hello_masonite_dev=# \q
Continuons , pour les environnements de production, ajoutons Gunicorn , un serveur WSGI de production, au fichier requirements :
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
gunicorn==20.1.0
Puisque nous voulons toujours utiliser le serveur intégré de Masonite dans le développement, créez un nouveau fichier de composition appelé docker-compose.prod.yml pour la production :
version: '3.8'
services:
web:
build: ./web
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
volumes:
postgres_data_prod:
Si vous avez plusieurs environnements, vous pouvez envisager d'utiliser un fichier de configuration docker-compose.override.yml . Avec cette approche, vous ajouteriez votre configuration de base à un fichier docker-compose.yml , puis utiliseriez un fichier docker-compose.override.yml pour remplacer ces paramètres de configuration en fonction de l'environnement.
Prenez note de la valeur par défaut command
. Nous utilisons Gunicorn plutôt que le serveur de développement Masonite. Nous avons également supprimé le volume du web
service puisque nous n'en avons pas besoin en production. Enfin, nous utilisons des fichiers de variables d'environnement distincts pour définir les variables d'environnement des deux services qui seront transmises au conteneur lors de l'exécution.
.env.prod :
APP_DEBUG=False
APP_ENV=prod
APP_KEY=GM28x-FeI1sM72tgtsgikLcT-AryyVOiY8etOGr7q7o=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_prod
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
DB_LOG=True
QUEUE_DRIVER=async
.env.prod.db :
POSTGRES_USER=hello_masonite
POSTGRES_PASSWORD=hello_masonite
POSTGRES_DB=hello_masonite_prod
Ajoutez les deux fichiers à la racine du projet. Vous voudrez probablement les garder hors du contrôle de version, alors ajoutez-les à un fichier .gitignore .
Descendez les conteneurs de développement (et les volumes associés avec le -v
drapeau) :
$ docker-compose down -v
Ensuite, créez les images de production et lancez les conteneurs :
$ docker-compose -f docker-compose.prod.yml up -d --build
Vérifiez que la base de hello_masonite_prod
données a été créée avec la users
table. Testez http://localhost:8000/ .
Encore une fois, si le conteneur ne démarre pas, recherchez les erreurs dans les journaux via
docker-compose -f docker-compose.prod.yml logs -f
.
Avez-vous remarqué que nous exécutons toujours les commandes migrate:refresh (qui efface la base de données) et migrate à chaque exécution du conteneur ? C'est bien en développement, mais créons un nouveau fichier de point d'entrée pour la production.
point d'entrée.prod.sh :
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Alternativement, au lieu de créer un nouveau fichier de point d'entrée, vous pouvez modifier celui existant comme ceci :
#!/bin/sh if [ "$DB_CONNECTION" = "postgres" ] then echo "Waiting for postgres..." while ! nc -z $DB_HOST $DB_PORT; do sleep 0.1 done echo "PostgreSQL started" fi if [ "$APP_ENV" = "local" ] then echo "Refreshing the database..." craft migrate:refresh # you may want to remove this echo "Applying migrations..." craft migrate echo "Tables created" fi exec "$@"
Pour utiliser ce fichier, créez un nouveau Dockerfile appelé Dockerfile.prod à utiliser avec les versions de production :
###########
# BUILDER #
###########
# pull official base image
FROM python:3.10.5-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==4.0.1
COPY . .
RUN flake8 --ignore=E501,F401,E303,E402 .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.10.5-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# set environment variables
ENV TZ=UTC
# install dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
RUN chmod +x /home/app/web/entrypoint.prod.sh
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Ici, nous avons utilisé une construction Docker en plusieurs étapes pour réduire la taille finale de l'image. Il s'agit essentiellement builder
d'une image temporaire utilisée pour créer les roues Python. Les roues sont ensuite copiées sur l'image de production finale et l' builder
image est supprimée.
Vous pouvez aller plus loin dans l' approche de construction en plusieurs étapes et utiliser un seul Dockerfile au lieu de créer deux Dockerfiles. Pensez aux avantages et aux inconvénients de l'utilisation de cette approche sur deux fichiers différents.
Avez-vous remarqué que nous avons créé un utilisateur non root ? Par défaut, Docker exécute les processus de conteneur en tant que racine à l'intérieur d'un conteneur. Il s'agit d'une mauvaise pratique car les attaquants peuvent obtenir un accès root à l'hôte Docker s'ils parviennent à sortir du conteneur. Si vous êtes root dans le conteneur, vous serez root sur l'hôte.
Mettez à jour le web
service dans le fichier docker-compose.prod.yml pour créer avec Dockerfile.prod :
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
Essaye le:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Ensuite, ajoutons Nginx dans le mélange pour agir comme un proxy inverse pour Gunicorn pour gérer les demandes des clients ainsi que pour servir des fichiers statiques.
Ajoutez le service à docker-compose.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Ensuite, à la racine du projet local, créez les fichiers et dossiers suivants :
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile :
FROM nginx:1.23.1-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Consultez Comprendre la structure du fichier de configuration Nginx et les contextes de configuration pour plus d'informations sur le fichier de configuration Nginx.
Ensuite, mettez à jour le web
service, dans docker-compose.prod.yml , en remplaçant ports
par expose
:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
Désormais, le port 8000 n'est exposé qu'en interne, aux autres services Docker. Le port ne sera plus publié sur la machine hôte.
Pour en savoir plus sur les ports par rapport à l'exposition, consultez cette question sur le débordement de pile.
Testez-le à nouveau.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Assurez-vous que l'application est opérationnelle à l' adresse http://localhost:1337 .
La structure de votre projet devrait maintenant ressembler à :
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
└── web
├── .env.testing
├── Dockerfile
├── Dockerfile.prod
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── entrypoint.prod.sh
├── entrypoint.sh
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Descendez les conteneurs une fois terminé :
$ docker-compose -f docker-compose.prod.yml down -v
Puisque Gunicorn est un serveur d'applications, il ne servira pas de fichiers statiques. Alors, comment les fichiers statiques et multimédias doivent-ils être gérés dans cette configuration particulière ?
Tout d'abord, mettez à jour la STATICFILES
configuration dans web/config/filesystem.py :
STATICFILES = {
# folder # template alias
"storage/static": "static/",
"storage/compiled": "static/",
"storage/uploads": "uploads/",
"storage/public": "/",
}
Essentiellement, tous les fichiers statiques stockés dans les répertoires "storage/static" (fichiers CSS et JS réguliers) et "storage/compiled" (fichiers SASS et LESS) seront servis à partir de l' /static/
URL.
Afin d'activer la compilation d'actifs, en supposant que NPM est installé, installez les dépendances :
$ cd web
$ npm install
Ensuite, pour compiler les éléments, exécutez :
$ npm run dev
Pour en savoir plus sur la complication des assets, consultez Compiling Assets dans la documentation Masonite.
Ensuite, pour tester une ressource statique standard, ajoutez un fichier texte appelé hello.txt à "web/storage/static":
hi!
Pour tester, reconstruisez d'abord les images et lancez les nouveaux conteneurs comme d'habitude. Une fois cela fait, assurez-vous que les éléments statiques suivants se chargent correctement :
Pour la production, ajoutez un volume aux services web
et nginx
dans docker-compose.prod.yml afin que chaque conteneur partage le répertoire "storage":
version: '3.8'
services:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
volumes:
- storage_volume:/home/app/web/storage
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
nginx:
build: ./nginx
volumes:
- storage_volume:/home/app/web/storage
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data_prod:
storage_volume:
Ensuite, mettez à jour la configuration Nginx pour acheminer les demandes de fichiers statiques vers le dossier approprié :
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Désormais, les demandes adressées aux fichiers statiques seront servies de manière appropriée :
URL de la demande | Dossier |
---|---|
/statique/* | "stockage/statique", "stockage/compilé" |
/favicon.ico, /robots.txt | "stockage/public" |
Faites tourner les conteneurs de développement :
$ docker-compose down -v
Test:
$ docker-compose -f docker-compose.prod.yml up -d --build
Encore une fois, assurez-vous que les éléments statiques suivants sont chargés correctement :
Vous pouvez également vérifier dans les journaux -- via docker-compose -f docker-compose.prod.yml logs -f
-- que les demandes adressées aux fichiers statiques sont correctement traitées via Nginx :
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:43 +0000] "GET /robots.txt HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:52 +0000] "GET /static/hello.txt HTTP/1.1" 200 4 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:59 +0000] "GET /static/css/app.css HTTP/1.1" 200 649 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
Apportez les contenants une fois terminé :
$ docker-compose -f docker-compose.prod.yml down -v
Pour tester la gestion des fichiers multimédias téléchargés par l'utilisateur, mettez à jour le bloc de contenu dans le modèle web/templates/welcome.html :
{% block content %}
<html>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<input type="file" name="image_upload">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
</body>
</html>
{% endblock %}
Ajoutez une nouvelle méthode appelée upload
à WelcomeController
dans web/app/controllers/WelcomeController.py :
def upload(self, storage: Storage, view: View, request: Request):
filename = storage.disk("local").put_file("image_upload", request.input("image_upload"))
return view.render("welcome", {"image_url": f"/framework/filesystem/{filename}"})
N'oubliez pas les importations :
from masonite.filesystem import Storage
from masonite.request import Request
Ensuite, connectez le contrôleur à une nouvelle route dans web/routes/web.py :
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show"),
Route.post("/", "WelcomeController@upload"),
]
Test:
$ docker-compose up -d --build
Vous devriez pouvoir télécharger une image sur http://localhost:8000/ , puis afficher l'image sur http://localhost:8000/uploads/IMAGE_FILE_NAME .
Pour la production, mettez à jour la configuration Nginx pour acheminer les demandes de fichiers multimédias vers le dossier "uploads" :
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location /uploads/ {
alias /home/app/web/storage/framework/filesystem/image_upload/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Reconstruire:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
Testez-le une dernière fois :
Dans ce didacticiel, nous avons expliqué comment conteneuriser une application Masonite avec Postgres pour le développement. Nous avons également créé un fichier Docker Compose prêt pour la production qui ajoute Gunicorn et Nginx dans le mélange pour gérer les fichiers statiques et multimédias. Vous pouvez maintenant tester une configuration de production localement.
En termes de déploiement réel dans un environnement de production, vous souhaiterez probablement utiliser :
db
etnginx
Vous pouvez trouver le code dans le référentiel masonite-on-docker .
Merci d'avoir lu!
Source : https://testdrive.io
1661082600
Este es un tutorial paso a paso que detalla cómo configurar Masonite, un marco web basado en Python, para que se ejecute en Docker con Postgres. Para entornos de producción, agregaremos Nginx y Gunicorn. También veremos cómo servir archivos multimedia estáticos y subidos por el usuario a través de Nginx.
Dependencias :
Cree un directorio de proyectos, instale Masonite y cree un nuevo proyecto de Masonite:
$ mkdir masonite-on-docker && cd masonite-on-docker
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install masonite==4.16.2
(env)$ project start web
(env)$ cd web
(env)$ project install
(env)$ python craft serve
Navegue a http://localhost:8000/ para ver la pantalla de bienvenida de Masonite. Elimine el servidor y salga del entorno virtual una vez hecho. Continúe y elimine también el entorno virtual. Ahora tenemos un proyecto de Masonite simple con el que trabajar.
A continuación, antes de agregar Docker, limpie un poco la estructura del proyecto:
La estructura de su proyecto ahora debería verse así:
├── .env.dev
└── web
├── .env.testing
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Instale Docker , si aún no lo tiene, luego agregue un Dockerfile al directorio "web":
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Entonces, comenzamos con una imagen de Docker basada en Alpine para Python 3.10.5. Luego establecemos un directorio de trabajo junto con tres variables de entorno:
PYTHONDONTWRITEBYTECODE
: Evita que Python escriba archivos pyc en el disco (equivalente a la python -B
opción )PYTHONUNBUFFERED
: evita que Python almacene en búfer stdout y stderr (equivalente a la python -u
opción )TZ=UTC
establece la zona horaria en el contenedor en UTC, que es necesaria para iniciar sesiónA continuación, instalamos algunas dependencias de nivel de sistema necesarias para Python. Tome nota de las dependencias openssl-dev
y . cargo
Estos son necesarios ya que la biblioteca de criptografía ahora depende de Rust . Para obtener más información, consulte Creación de criptografía en Linux .
Finalmente, actualizamos Pip, copiamos sobre el archivo requirements.txt , instalamos las dependencias y copiamos sobre la propia aplicación de Masonite.
Revise las prácticas recomendadas de Docker para desarrolladores de Python para obtener más información sobre la estructuración de Dockerfiles, así como algunas prácticas recomendadas para configurar Docker para el desarrollo basado en Python.
A continuación, agregue un archivo docker-compose.yml a la raíz del proyecto:
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
Revise la referencia del archivo Compose para obtener información sobre cómo funciona este archivo.
Simplifiquemos .env.dev eliminando las variables no utilizadas:
APP_DEBUG=True
APP_ENV=local
APP_KEY=zWDMwC0aNfVk8Ao1NyVJC_LiGD9tHJtVn_uCPeaaTNY=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=sqlite
SQLITE_DB_DATABASE=masonite.sqlite3
DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=masonite
DB_PORT=3306
DB_LOG=True
QUEUE_DRIVER=async
Construye la imagen:
$ docker-compose build
Una vez que se crea la imagen, ejecute el contenedor:
$ docker-compose up -d
Navegue a http://localhost:8000/ para volver a ver la pantalla de bienvenida.
Compruebe si hay errores en los registros si esto no funciona a través de
docker-compose logs -f
.
Para probar la recarga automática, primero abra los registros de Docker docker-compose logs -f
y luego realice un cambio en web/routes/web.py localmente:
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show")
]
Tan pronto como guarde, debería ver la recarga de la aplicación en su terminal así:
* Detected change in '/usr/src/app/routes/web.py', reloading
* Restarting with watchdog (inotify)
Asegúrese de que http://localhost:8000/sample funcione como se esperaba.
Para configurar Postgres, necesitaremos agregar un nuevo servicio al archivo docker-compose.yml , actualizar las variables de entorno e instalar Psycopg2 .
Primero, agregue un nuevo servicio llamado db
a docker-compose.yml :
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_dev:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_masonite
- POSTGRES_PASSWORD=hello_masonite
- POSTGRES_DB=hello_masonite_dev
volumes:
postgres_data_dev:
Para persistir los datos más allá de la vida útil del contenedor, configuramos un volumen. Esta configuración se enlazará postgres_data_dev
con el directorio "/var/lib/postgresql/data/" en el contenedor.
También agregamos una clave de entorno para definir un nombre para la base de datos predeterminada y establecer un nombre de usuario y una contraseña.
Consulte la sección "Variables de entorno" de la página de Postgres Docker Hub para obtener más información.
Actualice también las siguientes variables de entorno relacionadas con la base de datos en el archivo .env.dev :
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_dev
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
Revise el archivo web/config/database.py para obtener información sobre cómo se configura la base de datos según las variables de entorno definidas para el proyecto de Masonite.
Actualice Dockerfile para instalar los paquetes apropiados necesarios para Psycopg2:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Agregue Psycopg2 a web/requirements.txt :
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
Revise este problema de GitHub para obtener más información sobre la instalación de Psycopg2 en una imagen de Docker basada en Alpine.
Cree la nueva imagen y gire los dos contenedores:
$ docker-compose up -d --build
Aplicar las migraciones (desde la carpeta "web/databases/migrations"):
$ docker-compose exec web python craft migrate
Debería ver:
Migrating: 2021_01_09_033202_create_password_reset_table
Migrated: 2021_01_09_033202_create_password_reset_table (0.01s)
Migrating: 2021_01_09_043202_create_users_table
Migrated: 2021_01_09_043202_create_users_table (0.02s)
Asegúrese users
de que se haya creado la tabla:
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------------+----------------+----------+------------+------------+-----------------------------------
hello_masonite_dev | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
template1 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
(4 rows)
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------+-------+----------------
public | migrations | table | hello_masonite
public | password_resets | table | hello_masonite
public | users | table | hello_masonite
(3 rows)
hello_masonite_dev=# \q
Puede verificar que el volumen también se creó ejecutando:
$ docker volume inspect masonite-on-docker_postgres_data_dev
Debería ver algo similar a:
[
{
"CreatedAt": "2022-07-22T20:07:50Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "masonite-on-docker",
"com.docker.compose.version": "2.6.1",
"com.docker.compose.volume": "postgres_data_dev"
},
"Mountpoint": "/var/lib/docker/volumes/masonite-on-docker_postgres_data_dev/_data",
"Name": "masonite-on-docker_postgres_data_dev",
"Options": null,
"Scope": "local"
}
]
A continuación, agregue un archivo entrypoint.sh al directorio "web" para verificar que Postgres esté activo y en buen estado antes de aplicar las migraciones y ejecutar el servidor de desarrollo de Masonite:
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python craft migrate:refresh # you may want to remove this
python craft migrate
exec "$@"
Tome nota de las variables de entorno.
Luego, actualice Dockerfile para ejecutar el archivo entrypoint.sh como el comando de punto de entrada de Docker :
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Actualice los permisos del archivo localmente:
$ chmod +x web/entrypoint.sh
Pruébalo de nuevo:
¿Quieres sembrar algunos usuarios?
$ docker-compose exec web python craft seed:run
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# select count(*) from users;
count
-------
1
(1 row)
hello_masonite_dev=# \q
Avanzando, para entornos de producción, agreguemos Gunicorn , un servidor WSGI de nivel de producción, al archivo de requisitos:
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
gunicorn==20.1.0
Dado que todavía queremos usar el servidor incorporado de Masonite en desarrollo, cree un nuevo archivo de composición llamado docker-compose.prod.yml para producción:
version: '3.8'
services:
web:
build: ./web
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
volumes:
postgres_data_prod:
Si tiene varios entornos, es posible que desee utilizar un archivo de configuración docker-compose.override.yml . Con este enfoque, agregaría su configuración base a un archivo docker-compose.yml y luego usaría un archivo docker-compose.override.yml para anular esos ajustes de configuración según el entorno.
Tome nota del valor predeterminado command
. Estamos ejecutando Gunicorn en lugar del servidor de desarrollo de Masonite. También eliminamos el volumen del web
servicio ya que no lo necesitamos en producción. Finalmente, estamos usando archivos de variables de entorno separados para definir variables de entorno para ambos servicios que se pasarán al contenedor en tiempo de ejecución.
.env.prod :
APP_DEBUG=False
APP_ENV=prod
APP_KEY=GM28x-FeI1sM72tgtsgikLcT-AryyVOiY8etOGr7q7o=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_prod
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
DB_LOG=True
QUEUE_DRIVER=async
.env.prod.db :
POSTGRES_USER=hello_masonite
POSTGRES_PASSWORD=hello_masonite
POSTGRES_DB=hello_masonite_prod
Agregue los dos archivos a la raíz del proyecto. Probablemente querrá mantenerlos fuera del control de versiones, así que agréguelos a un archivo .gitignore .
Baje los contenedores de desarrollo (y los volúmenes asociados con la bandera-v
):
$ docker-compose down -v
Luego, crea las imágenes de producción y haz girar los contenedores:
$ docker-compose -f docker-compose.prod.yml up -d --build
Verifique que la base de hello_masonite_prod
datos se haya creado junto con la users
tabla. Pruebe http://localhost:8000/ .
Nuevamente, si el contenedor no se inicia, verifique si hay errores en los registros a través de
docker-compose -f docker-compose.prod.yml logs -f
.
¿Notó que todavía estamos ejecutando la migración: actualizar (que borra la base de datos) y los comandos de migración cada vez que se ejecuta el contenedor? Esto está bien en desarrollo, pero vamos a crear un nuevo archivo de punto de entrada para producción.
punto de entrada.prod.sh :
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Alternativamente, en lugar de crear un nuevo archivo de punto de entrada, puede modificar el existente de la siguiente manera:
#!/bin/sh if [ "$DB_CONNECTION" = "postgres" ] then echo "Waiting for postgres..." while ! nc -z $DB_HOST $DB_PORT; do sleep 0.1 done echo "PostgreSQL started" fi if [ "$APP_ENV" = "local" ] then echo "Refreshing the database..." craft migrate:refresh # you may want to remove this echo "Applying migrations..." craft migrate echo "Tables created" fi exec "$@"
Para usar este archivo, cree un nuevo Dockerfile llamado Dockerfile.prod para usar con compilaciones de producción:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.10.5-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==4.0.1
COPY . .
RUN flake8 --ignore=E501,F401,E303,E402 .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.10.5-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# set environment variables
ENV TZ=UTC
# install dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
RUN chmod +x /home/app/web/entrypoint.prod.sh
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Aquí, usamos una compilación de varias etapas de Docker para reducir el tamaño final de la imagen. Esencialmente, builder
es una imagen temporal que se usa para construir las ruedas de Python. Luego, las ruedas se copian en la imagen de producción final y la builder
imagen se descarta.
Podría llevar el enfoque de compilación de varias etapas un paso más allá y usar un solo Dockerfile en lugar de crear dos Dockerfiles. Piense en los pros y los contras de usar este enfoque en dos archivos diferentes.
¿Notaste que creamos un usuario no root? De forma predeterminada, Docker ejecuta procesos de contenedores como root dentro de un contenedor. Esta es una mala práctica, ya que los atacantes pueden obtener acceso de root al host de Docker si logran salir del contenedor. Si es root en el contenedor, será root en el host.
Actualice el web
servicio dentro del archivo docker-compose.prod.yml para compilar con Dockerfile.prod :
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
Pruébalo:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
A continuación, agreguemos Nginx a la combinación para que actúe como un proxy inverso para que Gunicorn maneje las solicitudes de los clientes y sirva archivos estáticos.
Agregue el servicio a docker-compose.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Luego, en la raíz del proyecto local, cree los siguientes archivos y carpetas:
└── nginx
├── Dockerfile
└── nginx.conf
archivo acoplable :
FROM nginx:1.23.1-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Consulte Comprender la estructura del archivo de configuración de Nginx y los contextos de configuración para obtener más información sobre el archivo de configuración de Nginx.
Luego, actualice el web
servicio, en docker-compose.prod.yml , reemplazándolo ports
con expose
:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
Ahora, el puerto 8000 solo está expuesto internamente a otros servicios de Docker. El puerto ya no se publicará en la máquina host.
Para obtener más información sobre puertos frente a exposición, revise esta pregunta de desbordamiento de pila.
Pruébelo de nuevo.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Asegúrese de que la aplicación esté funcionando en http://localhost:1337 .
La estructura de su proyecto ahora debería verse así:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
└── web
├── .env.testing
├── Dockerfile
├── Dockerfile.prod
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── entrypoint.prod.sh
├── entrypoint.sh
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Baje los contenedores una vez hecho:
$ docker-compose -f docker-compose.prod.yml down -v
Dado que Gunicorn es un servidor de aplicaciones, no entregará archivos estáticos. Entonces, ¿cómo deben manejarse los archivos estáticos y multimedia en esta configuración en particular?
Primero, actualice la STATICFILES
configuración en web/config/filesystem.py :
STATICFILES = {
# folder # template alias
"storage/static": "static/",
"storage/compiled": "static/",
"storage/uploads": "uploads/",
"storage/public": "/",
}
Básicamente, todos los archivos estáticos almacenados en los directores "almacenamiento/estático" (archivos CSS y JS normales) y "almacenamiento/compilado" (archivos SASS y LESS) se publicarán desde la /static/
URL.
Para habilitar la compilación de activos, suponiendo que tenga instalado NPM , instale las dependencias:
$ cd web
$ npm install
Luego, para compilar los activos, ejecute:
$ npm run dev
Para obtener más información sobre la complicación de activos, consulte Compilación de activos de los documentos de Masonite.
A continuación, para probar un activo estático regular, agregue un archivo de texto llamado hello.txt a "web/storage/static":
hi!
Para probar, primero reconstruya las imágenes y gire los nuevos contenedores como de costumbre. Una vez hecho esto, asegúrese de que los siguientes recursos estáticos se carguen correctamente:
Para la producción, agregue un volumen a los servicios web
y nginx
en docker-compose.prod.yml para que cada contenedor comparta el directorio de "almacenamiento":
version: '3.8'
services:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
volumes:
- storage_volume:/home/app/web/storage
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
nginx:
build: ./nginx
volumes:
- storage_volume:/home/app/web/storage
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data_prod:
storage_volume:
A continuación, actualice la configuración de Nginx para enrutar las solicitudes de archivos estáticos a la carpeta adecuada:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Ahora, las solicitudes a los archivos estáticos se atenderán adecuadamente:
URL de solicitud | Carpeta |
---|---|
/estático/* | "almacenamiento/estático", "almacenamiento/compilado" |
/favicon.ico, /robots.txt | "almacenamiento/público" |
Haga girar los contenedores de desarrollo:
$ docker-compose down -v
Prueba:
$ docker-compose -f docker-compose.prod.yml up -d --build
Nuevamente, asegúrese de que los siguientes activos estáticos estén cargados correctamente:
También puede verificar en los registros, a través docker-compose -f docker-compose.prod.yml logs -f
de, que las solicitudes a los archivos estáticos se entregan con éxito a través de Nginx:
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:43 +0000] "GET /robots.txt HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:52 +0000] "GET /static/hello.txt HTTP/1.1" 200 4 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:59 +0000] "GET /static/css/app.css HTTP/1.1" 200 649 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
Traer los recipientes una vez hecho:
$ docker-compose -f docker-compose.prod.yml down -v
Para probar el manejo de archivos multimedia subidos por el usuario, actualice el bloque de contenido en la plantilla web/templates/welcome.html :
{% block content %}
<html>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<input type="file" name="image_upload">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
</body>
</html>
{% endblock %}
Agregue un nuevo método llamado upload
en web/ WelcomeController
app /controllers/WelcomeController.py :
def upload(self, storage: Storage, view: View, request: Request):
filename = storage.disk("local").put_file("image_upload", request.input("image_upload"))
return view.render("welcome", {"image_url": f"/framework/filesystem/{filename}"})
No te olvides de las importaciones:
from masonite.filesystem import Storage
from masonite.request import Request
A continuación, conecte el controlador a una nueva ruta en web/routes/web.py :
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show"),
Route.post("/", "WelcomeController@upload"),
]
Prueba:
$ docker-compose up -d --build
Debería poder cargar una imagen en http://localhost:8000/ y luego ver la imagen en http://localhost:8000/uploads/IMAGE_FILE_NAME .
Para la producción, actualice la configuración de Nginx para enrutar las solicitudes de archivos multimedia a la carpeta "cargas":
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location /uploads/ {
alias /home/app/web/storage/framework/filesystem/image_upload/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Reconstruir:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
Pruébalo una última vez:
En este tutorial, explicamos cómo contener una aplicación de Masonite con Postgres para desarrollo. También creamos un archivo Docker Compose listo para producción que agrega Gunicorn y Nginx a la mezcla para manejar archivos estáticos y multimedia. Ahora puede probar una configuración de producción localmente.
En términos de implementación real en un entorno de producción, probablemente desee utilizar un:
db
ynginx
Puede encontrar el código en el repositorio de masonite-on-docker .
¡Gracias por leer!
Fuente: https://testdriven.io
1661074740
Este é um tutorial passo a passo que detalha como configurar o Masonite, um framework web baseado em Python, para ser executado no Docker com Postgres. Para ambientes de produção, adicionaremos Nginx e Gunicorn. Também veremos como servir arquivos de mídia estáticos e carregados pelo usuário via Nginx.
Dependências :
Crie um diretório de projeto, instale o Masonite e crie um novo projeto Masonite:
$ mkdir masonite-on-docker && cd masonite-on-docker
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install masonite==4.16.2
(env)$ project start web
(env)$ cd web
(env)$ project install
(env)$ python craft serve
Navegue até http://localhost:8000/ para ver a tela de boas-vindas da Masonite. Mate o servidor e saia do ambiente virtual assim que terminar. Vá em frente e remova o ambiente virtual também. Agora temos um projeto Masonite simples para trabalhar.
Em seguida, antes de adicionar o Docker, vamos limpar um pouco a estrutura do projeto:
A estrutura do seu projeto agora deve ficar assim:
├── .env.dev
└── web
├── .env.testing
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Instale o Docker , se você ainda não o tiver, adicione um Dockerfile ao diretório "web":
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Então, começamos com uma imagem Docker baseada em Alpine para Python 3.10.5. Em seguida, definimos um diretório de trabalho junto com três variáveis de ambiente:
PYTHONDONTWRITEBYTECODE
: Impede que o Python grave arquivos pyc no disco (equivalente à python -B
opção )PYTHONUNBUFFERED
: Impede o Python de armazenar em buffer stdout e stderr (equivalente a python -u
option )TZ=UTC
define o fuso horário no contêiner como UTC, que é necessário para o registroEm seguida, instalamos algumas dependências de nível de sistema necessárias para Python. Anote as dependências openssl-dev
e . cargo
Eles são necessários, pois a biblioteca de criptografia agora depende do Rust . Para saber mais, revise Construindo criptografia no Linux .
Por fim, atualizamos o Pip, copiamos o arquivo requirements.txt , instalamos as dependências e copiamos o próprio aplicativo Masonite.
Revise as práticas recomendadas do Docker para desenvolvedores Python para obter mais informações sobre a estruturação de Dockerfiles, bem como algumas práticas recomendadas para configurar o Docker para desenvolvimento baseado em Python.
Em seguida, adicione um arquivo docker-compose.yml à raiz do projeto:
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
Revise a referência do arquivo Compose para obter informações sobre como esse arquivo funciona.
Vamos simplificar .env.dev removendo quaisquer variáveis não utilizadas:
APP_DEBUG=True
APP_ENV=local
APP_KEY=zWDMwC0aNfVk8Ao1NyVJC_LiGD9tHJtVn_uCPeaaTNY=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=sqlite
SQLITE_DB_DATABASE=masonite.sqlite3
DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=masonite
DB_PORT=3306
DB_LOG=True
QUEUE_DRIVER=async
Construa a imagem:
$ docker-compose build
Depois que a imagem for criada, execute o contêiner:
$ docker-compose up -d
Navegue até http://localhost:8000/ para visualizar novamente a tela de boas-vindas.
Verifique se há erros nos logs se isso não funcionar via
docker-compose logs -f
.
Para testar o recarregamento automático, primeiro abra os logs do Docker -- docker-compose logs -f
-- e faça uma alteração em web/routes/web.py localmente:
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show")
]
Assim que você salvar, você deverá ver o aplicativo recarregar no seu terminal assim:
* Detected change in '/usr/src/app/routes/web.py', reloading
* Restarting with watchdog (inotify)
Certifique -se de que http://localhost:8000/sample funcione conforme o esperado.
Para configurar o Postgres, precisaremos adicionar um novo serviço ao arquivo docker-compose.yml , atualizar as variáveis de ambiente e instalar o Psycopg2 .
Primeiro, adicione um novo serviço chamado docker db
- compose.yml :
version: '3.8'
services:
web:
build: ./web
command: python craft serve -p 8000 -b 0.0.0.0
volumes:
- ./web/:/usr/src/app/
ports:
- 8000:8000
env_file:
- .env.dev
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_dev:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_masonite
- POSTGRES_PASSWORD=hello_masonite
- POSTGRES_DB=hello_masonite_dev
volumes:
postgres_data_dev:
Para persistir os dados além da vida útil do contêiner, configuramos um volume. Essa configuração será vinculada postgres_data_dev
ao diretório "/var/lib/postgresql/data/" no contêiner.
Também adicionamos uma chave de ambiente para definir um nome para o banco de dados padrão e definir um nome de usuário e senha.
Revise a seção "Variáveis de ambiente" da página Postgres Docker Hub para obter mais informações.
Atualize também as seguintes variáveis de ambiente relacionadas ao banco de dados no arquivo .env.dev :
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_dev
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
Revise o arquivo web/config/database.py para obter informações sobre como o banco de dados é configurado com base nas variáveis de ambiente definidas para o projeto Masonite.
Atualize o Dockerfile para instalar os pacotes apropriados necessários para o Psycopg2:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Adicione Psycopg2 a web/requirements.txt :
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
Revise este problema do GitHub para obter mais informações sobre como instalar o Psycopg2 em uma imagem do Docker baseada em Alpine.
Crie a nova imagem e gire os dois contêineres:
$ docker-compose up -d --build
Aplique as migrações (da pasta "web/databases/migrations"):
$ docker-compose exec web python craft migrate
Você deveria ver:
Migrating: 2021_01_09_033202_create_password_reset_table
Migrated: 2021_01_09_033202_create_password_reset_table (0.01s)
Migrating: 2021_01_09_043202_create_users_table
Migrated: 2021_01_09_043202_create_users_table (0.02s)
Verifique se a users
tabela foi criada:
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------------+----------------+----------+------------+------------+-----------------------------------
hello_masonite_dev | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
template1 | hello_masonite | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_masonite +
| | | | | hello_masonite=CTc/hello_masonite
(4 rows)
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------+-------+----------------
public | migrations | table | hello_masonite
public | password_resets | table | hello_masonite
public | users | table | hello_masonite
(3 rows)
hello_masonite_dev=# \q
Você pode verificar se o volume também foi criado executando:
$ docker volume inspect masonite-on-docker_postgres_data_dev
Você deve ver algo semelhante a:
[
{
"CreatedAt": "2022-07-22T20:07:50Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "masonite-on-docker",
"com.docker.compose.version": "2.6.1",
"com.docker.compose.volume": "postgres_data_dev"
},
"Mountpoint": "/var/lib/docker/volumes/masonite-on-docker_postgres_data_dev/_data",
"Name": "masonite-on-docker_postgres_data_dev",
"Options": null,
"Scope": "local"
}
]
Em seguida, adicione um arquivo entrypoint.sh ao diretório "web" para verificar se o Postgres está ativo e íntegro antes de aplicar as migrações e executar o servidor de desenvolvimento Masonite:
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python craft migrate:refresh # you may want to remove this
python craft migrate
exec "$@"
Tome nota das variáveis de ambiente.
Em seguida, atualize o Dockerfile para executar o arquivo entrypoint.sh como o comando entrypoint do Docker:
# pull official base image
FROM python:3.10.5-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Atualize as permissões do arquivo localmente:
$ chmod +x web/entrypoint.sh
Teste novamente:
Quer propagar alguns usuários?
$ docker-compose exec web python craft seed:run
$ docker-compose exec db psql --username=hello_masonite --dbname=hello_masonite_dev
psql (14.4)
Type "help" for help.
hello_masonite_dev=# \c hello_masonite_dev
You are now connected to database "hello_masonite_dev" as user "hello_masonite".
hello_masonite_dev=# select count(*) from users;
count
-------
1
(1 row)
hello_masonite_dev=# \q
Seguindo em frente, para ambientes de produção, vamos adicionar Gunicorn , um servidor WSGI de nível de produção, ao arquivo de requisitos:
masonite>=4.0,<5.0
masonite-orm>=2.0,<3.0
psycopg2-binary==2.9.3
gunicorn==20.1.0
Como ainda queremos usar o servidor interno do Masonite em desenvolvimento, crie um novo arquivo de composição chamado docker-compose.prod.yml para produção:
version: '3.8'
services:
web:
build: ./web
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
volumes:
postgres_data_prod:
Se você tiver vários ambientes, talvez queira usar um arquivo de configuração docker-compose.override.yml . Com essa abordagem, você adicionaria sua configuração básica a um arquivo docker-compose.yml e, em seguida, usaria um arquivo docker-compose.override.yml para substituir essas configurações com base no ambiente.
Tome nota do padrão command
. Estamos executando o Gunicorn em vez do servidor de desenvolvimento Masonite. Também removemos o volume do web
serviço, pois não precisamos dele em produção. Por fim, estamos usando arquivos de variáveis de ambiente separados para definir variáveis de ambiente para ambos os serviços que serão passados para o contêiner em tempo de execução.
.env.prod :
APP_DEBUG=False
APP_ENV=prod
APP_KEY=GM28x-FeI1sM72tgtsgikLcT-AryyVOiY8etOGr7q7o=
APP_URL=http://localhost:8000
HASHING_FUNCTION=bcrypt
MAIL_DRIVER=terminal
DB_CONNECTION=postgres
DB_HOST=db
DB_PORT=5432
DB_DATABASE=hello_masonite_prod
DB_USERNAME=hello_masonite
DB_PASSWORD=hello_masonite
DB_LOG=True
QUEUE_DRIVER=async
.env.prod.db :
POSTGRES_USER=hello_masonite
POSTGRES_PASSWORD=hello_masonite
POSTGRES_DB=hello_masonite_prod
Adicione os dois arquivos à raiz do projeto. Você provavelmente vai querer mantê-los fora do controle de versão, então adicione-os a um arquivo .gitignore .
Desative os contêineres de desenvolvimento (e os volumes associados com o -v
sinalizador):
$ docker-compose down -v
Em seguida, construa as imagens de produção e gire os contêineres:
$ docker-compose -f docker-compose.prod.yml up -d --build
Verifique se o hello_masonite_prod
banco de dados foi criado junto com a users
tabela. Teste http://localhost:8000/ .
Novamente, se o contêiner não iniciar, verifique se há erros nos logs por meio do
docker-compose -f docker-compose.prod.yml logs -f
.
Você notou que ainda estamos executando os comandos migrate:refresh (que limpa o banco de dados) e migrate toda vez que o contêiner é executado? Isso é bom em desenvolvimento, mas vamos criar um novo arquivo de ponto de entrada para produção.
entrypoint.prod.sh :
#!/bin/sh
if [ "$DB_CONNECTION" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Como alternativa, em vez de criar um novo arquivo de ponto de entrada, você pode alterar o existente da seguinte forma:
#!/bin/sh if [ "$DB_CONNECTION" = "postgres" ] then echo "Waiting for postgres..." while ! nc -z $DB_HOST $DB_PORT; do sleep 0.1 done echo "PostgreSQL started" fi if [ "$APP_ENV" = "local" ] then echo "Refreshing the database..." craft migrate:refresh # you may want to remove this echo "Applying migrations..." craft migrate echo "Tables created" fi exec "$@"
Para usar esse arquivo, crie um novo Dockerfile chamado Dockerfile.prod para uso com compilações de produção:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.10.5-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=UTC
# install system dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==4.0.1
COPY . .
RUN flake8 --ignore=E501,F401,E303,E402 .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.10.5-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# set environment variables
ENV TZ=UTC
# install dependencies
RUN apk update && apk --no-cache add \
libressl-dev libffi-dev gcc musl-dev python3-dev openssl-dev cargo \
postgresql-dev
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
RUN chmod +x /home/app/web/entrypoint.prod.sh
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Aqui, usamos uma compilação de vários estágios do Docker para reduzir o tamanho final da imagem. Essencialmente, builder
é uma imagem temporária que é usada para construir as rodas do Python. As rodas são então copiadas para a imagem de produção final e a builder
imagem é descartada.
Você pode levar a abordagem de compilação em vários estágios um passo adiante e usar um único Dockerfile em vez de criar dois Dockerfiles. Pense nos prós e contras de usar essa abordagem em dois arquivos diferentes.
Você notou que criamos um usuário não root? Por padrão, o Docker executa processos de contêiner como root dentro de um contêiner. Essa é uma prática ruim, pois os invasores podem obter acesso root ao host do Docker se conseguirem sair do contêiner. Se você for root no contêiner, será root no host.
Atualize o web
serviço no arquivo docker-compose.prod.yml para compilar com Dockerfile.prod :
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
ports:
- 8000:8000
env_file:
- .env.prod
depends_on:
- db
Experimente:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Em seguida, vamos adicionar o Nginx à mistura para atuar como um proxy reverso para o Gunicorn lidar com solicitações de clientes, bem como servir arquivos estáticos.
Adicione o serviço a docker-compose.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Em seguida, na raiz do projeto local, crie os seguintes arquivos e pastas:
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile :
FROM nginx:1.23.1-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Revise Entendendo a estrutura do arquivo de configuração do Nginx e os contextos de configuração para obter mais informações sobre o arquivo de configuração do Nginx.
Em seguida, atualize o web
serviço, em docker-compose.prod.yml , substituindo ports
por expose
:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
Agora, a porta 8000 é exposta apenas internamente, para outros serviços do Docker. A porta não será mais publicada na máquina host.
Para saber mais sobre portas versus exposição, revise esta pergunta do Stack Overflow.
Teste novamente.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python craft migrate
Verifique se o aplicativo está funcionando em http://localhost:1337 .
A estrutura do seu projeto agora deve se parecer com:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
└── web
├── .env.testing
├── Dockerfile
├── Dockerfile.prod
├── Kernel.py
├── app
│ ├── __init__.py
│ ├── controllers
│ │ ├── WelcomeController.py
│ │ └── __init__.py
│ ├── middlewares
│ │ ├── AuthenticationMiddleware.py
│ │ ├── VerifyCsrfToken.py
│ │ └── __init__.py
│ ├── models
│ │ └── User.py
│ └── providers
│ ├── AppProvider.py
│ └── __init__.py
├── config
│ ├── __init__.py
│ ├── application.py
│ ├── auth.py
│ ├── broadcast.py
│ ├── cache.py
│ ├── database.py
│ ├── exceptions.py
│ ├── filesystem.py
│ ├── mail.py
│ ├── notification.py
│ ├── providers.py
│ ├── queue.py
│ ├── security.py
│ └── session.py
├── craft
├── databases
│ ├── migrations
│ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ └── 2021_01_09_043202_create_users_table.py
│ └── seeds
│ ├── __init__.py
│ ├── database_seeder.py
│ └── user_table_seeder.py
├── entrypoint.prod.sh
├── entrypoint.sh
├── makefile
├── package.json
├── pyproject.toml
├── requirements.txt
├── resources
│ ├── css
│ │ └── app.css
│ └── js
│ ├── app.js
│ └── bootstrap.js
├── routes
│ └── web.py
├── setup.cfg
├── storage
│ ├── .gitignore
│ └── public
│ ├── favicon.ico
│ ├── logo.png
│ └── robots.txt
├── templates
│ ├── __init__.py
│ ├── base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── maintenance.html
│ └── welcome.html
├── tests
│ ├── TestCase.py
│ ├── __init__.py
│ └── unit
│ └── test_basic_testcase.py
├── webpack.mix.js
└── wsgi.py
Abaixe os recipientes uma vez feito:
$ docker-compose -f docker-compose.prod.yml down -v
Como o Gunicorn é um servidor de aplicativos, ele não servirá arquivos estáticos. Então, como os arquivos estáticos e de mídia devem ser tratados nessa configuração específica?
Primeiro, atualize a STATICFILES
configuração em web/config/filesystem.py :
STATICFILES = {
# folder # template alias
"storage/static": "static/",
"storage/compiled": "static/",
"storage/uploads": "uploads/",
"storage/public": "/",
}
Essencialmente, todos os arquivos estáticos armazenados nos diretórios "storage/static" (arquivos CSS e JS regulares) e "storage/compiled" (arquivos SASS e LESS) serão servidos a partir da /static/
URL.
Para habilitar a compilação de ativos, supondo que você tenha o NPM instalado, instale as dependências:
$ cd web
$ npm install
Em seguida, para compilar os ativos, execute:
$ npm run dev
Para saber mais sobre a complicação de ativos, revise Compiling Assets dos documentos da Masonite.
Em seguida, para testar um recurso estático regular, adicione um arquivo de texto chamado hello.txt a "web/storage/static":
hi!
Para testar, primeiro reconstrua as imagens e gire os novos contêineres normalmente. Uma vez feito, certifique-se de que os seguintes ativos estáticos sejam carregados corretamente:
Para produção, adicione um volume ao web
and nginx
services em docker-compose.prod.yml para que cada contêiner compartilhe o diretório "storage":
version: '3.8'
services:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 wsgi:application
volumes:
- storage_volume:/home/app/web/storage
expose:
- 8000
env_file:
- .env.prod
depends_on:
- db
db:
image: postgres:14.4-alpine
volumes:
- postgres_data_prod:/var/lib/postgresql/data/
env_file:
- .env.prod.db
nginx:
build: ./nginx
volumes:
- storage_volume:/home/app/web/storage
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data_prod:
storage_volume:
Em seguida, atualize a configuração do Nginx para rotear solicitações de arquivos estáticos para a pasta apropriada:
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Agora, as solicitações para os arquivos estáticos serão atendidas apropriadamente:
URL de solicitação | Pasta |
---|---|
/estático/* | "armazenamento/estático", "armazenamento/compilado" |
/favicon.ico, /robots.txt | "armazenamento/público" |
Reduza os contêineres de desenvolvimento:
$ docker-compose down -v
Teste:
$ docker-compose -f docker-compose.prod.yml up -d --build
Novamente, certifique-se de que os seguintes ativos estáticos sejam carregados corretamente:
Você também pode verificar nos logs -- via docker-compose -f docker-compose.prod.yml logs -f
-- se as solicitações para os arquivos estáticos são atendidas com sucesso via Nginx:
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:43 +0000] "GET /robots.txt HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:52 +0000] "GET /static/hello.txt HTTP/1.1" 200 4 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
nginx_1 | 172.28.0.1 - - [2022-07-20:01:39:59 +0000] "GET /static/css/app.css HTTP/1.1" 200 649 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" "-"
Traga os recipientes uma vez feito:
$ docker-compose -f docker-compose.prod.yml down -v
Para testar o manuseio de arquivos de mídia carregados pelo usuário, atualize o bloco de conteúdo no modelo web/templates/welcome.html :
{% block content %}
<html>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<input type="file" name="image_upload">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
</body>
</html>
{% endblock %}
Adicione um novo método chamado upload
em web/app/controllers/WelcomeController
WelcomeController.py :
def upload(self, storage: Storage, view: View, request: Request):
filename = storage.disk("local").put_file("image_upload", request.input("image_upload"))
return view.render("welcome", {"image_url": f"/framework/filesystem/{filename}"})
Não esqueça das importações:
from masonite.filesystem import Storage
from masonite.request import Request
Em seguida, conecte o controlador a uma nova rota em web/routes/web.py :
from masonite.routes import Route
ROUTES = [
Route.get("/", "WelcomeController@show"),
Route.get("/sample", "WelcomeController@show"),
Route.post("/", "WelcomeController@upload"),
]
Teste:
$ docker-compose up -d --build
Você deve conseguir fazer upload de uma imagem em http://localhost:8000/ e, em seguida, visualizar a imagem em http://localhost:8000/uploads/IMAGE_FILE_NAME .
Para produção, atualize a configuração do Nginx para rotear solicitações de arquivos de mídia para a pasta "uploads":
upstream hello_masonite {
server web:8000;
}
server {
listen 80;
location /static/ {
alias /home/app/web/storage/static/;
}
location ~ ^/(favicon.ico|robots.txt)/ {
alias /home/app/web/storage/public/;
}
location /uploads/ {
alias /home/app/web/storage/framework/filesystem/image_upload/;
}
location / {
proxy_pass http://hello_masonite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Reconstruir:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
Teste-o uma última vez:
Neste tutorial, explicamos como colocar em contêiner um aplicativo Masonite com o Postgres para desenvolvimento. Também criamos um arquivo Docker Compose pronto para produção que adiciona Gunicorn e Nginx à mistura para lidar com arquivos estáticos e de mídia. Agora você pode testar uma configuração de produção localmente.
Em termos de implantação real em um ambiente de produção, você provavelmente desejará usar um:
db
enginx
Você pode encontrar o código no repositório masonite-on-docker .
Obrigado por ler!
Fonte: https://testdrive.io
1660719420
This is a step-by-step tutorial that details how to configure Django to run on Docker with Postgres. For production environments, we'll add on Nginx and Gunicorn. We'll also take a look at how to serve Django static and media files via Nginx.
Source: https://testdriven.io
1660712160
Đây là hướng dẫn từng bước chi tiết cách định cấu hình Django để chạy trên Docker với Postgres. Đối với môi trường sản xuất, chúng tôi sẽ thêm vào Nginx và Gunicorn. Chúng ta cũng sẽ xem xét cách cung cấp các tệp phương tiện và tĩnh Django thông qua Nginx.
Sự phụ thuộc :
Tạo một thư mục dự án mới cùng với một dự án Django mới:
$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.9 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver
Điều hướng đến http: // localhost: 8000 / để xem màn hình chào mừng Django. Diệt máy chủ sau khi hoàn tất. Sau đó, thoát khỏi và xóa môi trường ảo. Bây giờ chúng tôi có một dự án Django đơn giản để làm việc.
Tạo một tệp tin request.txt trong thư mục "app" và thêm Django làm phần phụ thuộc:
Django==3.2.6
Vì chúng tôi sẽ chuyển sang Postgres, hãy tiếp tục và xóa tệp db.sqlite3 khỏi thư mục "ứng dụng".
Thư mục dự án của bạn sẽ giống như sau:
└── app
├── hello_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt
Cài đặt Docker , nếu bạn chưa có, sau đó thêm Dockerfile vào thư mục "ứng dụng":
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Vì vậy, chúng tôi đã bắt đầu với một hình ảnh Docker dựa trên Alpine cho Python 3.9.6. Sau đó, chúng tôi thiết lập một thư mục làm việc cùng với hai biến môi trường:
PYTHONDONTWRITEBYTECODE
: Ngăn Python ghi tệp pyc vào đĩa (tương đương với python -B
tùy chọn )PYTHONUNBUFFERED
: Ngăn Python lưu vào bộ đệm stdout và stderr (tương đương với python -u
tùy chọn )Cuối cùng, chúng tôi đã cập nhật Pip, sao chép qua tệp tin request.txt , cài đặt các phần phụ thuộc và sao chép qua chính dự án Django.
Xem lại Docker dành cho nhà phát triển Python để biết thêm về cấu trúc Dockerfiles cũng như một số phương pháp hay nhất để định cấu hình Docker cho phát triển dựa trên Python.
Tiếp theo, thêm tệp docker-compost.yml vào thư mục gốc của dự án:
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
Xem lại tài liệu tham khảo Soạn tệp để biết thông tin về cách tệp này hoạt động.
Cập nhật SECRET_KEY
và DEBUG
và ALLOWED_HOSTS
các biến trong settings.py :
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
Đảm bảo thêm nhập vào đầu:
import os
Sau đó, tạo tệp .env.dev trong thư mục gốc của dự án để lưu trữ các biến môi trường để phát triển:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
Xây dựng hình ảnh:
$ docker-compose build
Sau khi hình ảnh được tạo, hãy chạy vùng chứa:
$ docker-compose up -d
Điều hướng đến http: // localhost: 8000 / để xem lại màn hình chào mừng.
Kiểm tra lỗi trong nhật ký nếu điều này không hoạt động thông qua
docker-compose logs -f
.
Để định cấu hình Postgres, chúng tôi sẽ cần thêm một dịch vụ mới vào tệp docker -compos.yml , cập nhật cài đặt Django và cài đặt Psycopg2 .
Đầu tiên, hãy thêm một dịch vụ mới có tên là db
docker -omp.yml :
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_django
- POSTGRES_PASSWORD=hello_django
- POSTGRES_DB=hello_django_dev
volumes:
postgres_data:
Để duy trì dữ liệu ngoài vòng đời của vùng chứa, chúng tôi đã định cấu hình một ổ đĩa. Cấu hình này sẽ liên kết postgres_data
với thư mục "/ var / lib / postgresql / data /" trong vùng chứa.
Chúng tôi cũng đã thêm một khóa môi trường để xác định tên cho cơ sở dữ liệu mặc định và đặt tên người dùng và mật khẩu.
Xem lại phần "Biến môi trường" của trang Postgres Docker Hub để biết thêm thông tin.
Chúng tôi cũng sẽ cần một số biến môi trường mới cho web
dịch vụ, vì vậy hãy cập nhật .env.dev như vậy:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
Cập nhật chính tả DATABASES
trong settings.py :
DATABASES = {
"default": {
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
"USER": os.environ.get("SQL_USER", "user"),
"PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
"HOST": os.environ.get("SQL_HOST", "localhost"),
"PORT": os.environ.get("SQL_PORT", "5432"),
}
}
Ở đây, cơ sở dữ liệu được cấu hình dựa trên các biến môi trường mà chúng ta vừa xác định. Lưu ý các giá trị mặc định.
Cập nhật Dockerfile để cài đặt các gói thích hợp cần thiết cho Psycopg2:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Thêm Psycopg2 vào tệp tests.txt :
Django==3.2.6
psycopg2-binary==2.9.1
Xem lại Sự cố GitHub này để biết thêm thông tin về cách cài đặt Psycopg2 trong Hình ảnh Docker dựa trên Alpine.
Xây dựng hình ảnh mới và xoay hai vùng chứa:
$ docker-compose up -d --build
Chạy quá trình di chuyển:
$ docker-compose exec web python manage.py migrate --noinput
Nhận lỗi sau?
django.db.utils.OperationalError: FATAL: cơ sở dữ liệu "hello_django_dev" không tồn tại
Chạy
docker-compose down -v
để loại bỏ các ổ đĩa cùng với các vùng chứa. Sau đó, xây dựng lại hình ảnh, chạy các vùng chứa và áp dụng các di chuyển.
Đảm bảo các bảng Django mặc định đã được tạo:
$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev
psql (13.0)
Type "help" for help.
hello_django_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
------------------+--------------+----------+------------+------------+-------------------------------
hello_django_dev | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
template1 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
(4 rows)
hello_django_dev=# \c hello_django_dev
You are now connected to database "hello_django_dev" as user "hello_django".
hello_django_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+--------------
public | auth_group | table | hello_django
public | auth_group_permissions | table | hello_django
public | auth_permission | table | hello_django
public | auth_user | table | hello_django
public | auth_user_groups | table | hello_django
public | auth_user_user_permissions | table | hello_django
public | django_admin_log | table | hello_django
public | django_content_type | table | hello_django
public | django_migrations | table | hello_django
public | django_session | table | hello_django
(10 rows)
hello_django_dev=# \q
Bạn có thể kiểm tra xem ổ đĩa đã được tạo hay chưa bằng cách chạy:
$ docker volume inspect django-on-docker_postgres_data
Bạn sẽ thấy một cái gì đó tương tự như:
[
{
"CreatedAt": "2021-08-23T15:49:08Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-on-docker",
"com.docker.compose.version": "1.29.2",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/django-on-docker_postgres_data/_data",
"Name": "django-on-docker_postgres_data",
"Options": null,
"Scope": "local"
}
]
Tiếp theo, thêm tệp entrypoint.sh vào thư mục "ứng dụng" để xác minh rằng Postgres hoạt động tốt trước khi áp dụng di chuyển và chạy máy chủ phát triển Django:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python manage.py flush --no-input
python manage.py migrate
exec "$@"
Cập nhật các quyền đối với tệp cục bộ:
$ chmod +x app/entrypoint.sh
Sau đó, cập nhật Dockerfile để sao chép qua tệp entrypoint.sh và chạy nó dưới dạng lệnh Docker entrypoint :
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy entrypoint.sh
COPY ./entrypoint.sh .
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Thêm DATABASE
biến môi trường vào .env.dev :
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
Kiểm tra lại lần nữa:
Đầu tiên, mặc dù đã thêm Postgres, chúng tôi vẫn có thể tạo một hình ảnh Docker độc lập cho Django miễn là DATABASE
biến môi trường không được đặt thành postgres
. Để kiểm tra, hãy tạo một hình ảnh mới và sau đó chạy một vùng chứa mới:
$ docker build -f ./app/Dockerfile -t hello_django:latest ./app
$ docker run -d \
-p 8006:8000 \
-e "SECRET_KEY=please_change_me" -e "DEBUG=1" -e "DJANGO_ALLOWED_HOSTS=*" \
hello_django python /usr/src/app/manage.py runserver 0.0.0.0:8000
Bạn sẽ có thể xem trang chào mừng tại http: // localhost: 8006
Thứ hai, bạn có thể muốn nhận xét về lệnh flush và di chuyển cơ sở dữ liệu trong tập lệnh entrypoint.sh để chúng không chạy trên mọi lần khởi động hoặc khởi động lại vùng chứa:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
# python manage.py flush --no-input
# python manage.py migrate
exec "$@"
Thay vào đó, bạn có thể chạy chúng theo cách thủ công, sau khi các vùng chứa quay lên, như sau:
$ docker-compose exec web python manage.py flush --no-input
$ docker-compose exec web python manage.py migrate
Tiếp theo, đối với môi trường sản xuất, hãy thêm Gunicorn , một máy chủ WSGI cấp sản xuất, vào tệp yêu cầu:
Django==3.2.6
gunicorn==20.1.0
psycopg2-binary==2.9.1
Bạn tò mò về WSGI và Gunicorn? Xem lại chương WSGI từ khóa học Xây dựng khung web Python của riêng bạn .
Vì chúng tôi vẫn muốn sử dụng máy chủ tích hợp của Django trong quá trình phát triển, hãy tạo một tệp soạn mới có tên là docker-compos.prod.yml để sản xuất:
version: '3.8'
services:
web:
build: ./app
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
volumes:
postgres_data:
If you have multiple environments, you may want to look at using a docker-compose.override.yml configuration file. With this approach, you'd add your base config to a docker-compose.yml file and then use a docker-compose.override.yml file to override those config settings based on the environment.
Take note of the default command
. We're running Gunicorn rather than the Django development server. We also removed the volume from the web
service since we don't need it in production. Finally, we're using separate environment variable files to define environment variables for both services that will be passed to the container at runtime.
.env.prod:
DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
.env.prod.db:
POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod
Thêm hai tệp vào thư mục gốc của dự án. Có thể bạn sẽ muốn giữ chúng ngoài tầm kiểm soát phiên bản, vì vậy hãy thêm chúng vào tệp .gitignore .
Giảm các vùng chứa phát triển (và các tập liên quan với -v
cờ):
$ docker-compose down -v
Sau đó, xây dựng các hình ảnh sản xuất và quay các vùng chứa:
$ docker-compose -f docker-compose.prod.yml up -d --build
Xác minh rằng hello_django_prod
cơ sở dữ liệu đã được tạo cùng với các bảng Django mặc định. Kiểm tra trang quản trị tại http: // localhost: 8000 / admin . Các tệp tĩnh không được tải nữa. Điều này được mong đợi vì chế độ Gỡ lỗi đang tắt. Chúng tôi sẽ sớm khắc phục sự cố này.
Một lần nữa, nếu vùng chứa không khởi động được, hãy kiểm tra lỗi trong nhật ký qua
docker-compose -f docker-compose.prod.yml logs -f
.
Bạn có nhận thấy rằng chúng tôi vẫn đang chạy cơ sở dữ liệu flush (xóa cơ sở dữ liệu) và di chuyển các lệnh mỗi khi vùng chứa được chạy? Điều này là tốt trong quá trình phát triển, nhưng hãy tạo một tệp entrypoint mới cho quá trình sản xuất.
entrypoint.prod.sh:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Cập nhật các quyền đối với tệp cục bộ:
$ chmod +x app/entrypoint.prod.sh
Để sử dụng tệp này, hãy tạo một Dockerfile mới có tên Dockerfile.prod để sử dụng với các bản dựng sản xuất:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.9.6-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.2
COPY . .
RUN flake8 --ignore=E501,F401 .
# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.9.6-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh
RUN chmod +x $APP_HOME/entrypoint.prod.sh
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Ở đây, chúng tôi đã sử dụng một bản dựng nhiều giai đoạn Docker để giảm kích thước hình ảnh cuối cùng. Về cơ bản, builder
là một hình ảnh tạm thời được sử dụng để xây dựng các bánh xe Python. Các bánh xe sau đó được sao chép sang hình ảnh sản xuất cuối cùng và builder
hình ảnh bị loại bỏ.
Bạn có thể thực hiện thêm một bước nữa trong phương pháp xây dựng nhiều giai đoạn và sử dụng một Dockerfile duy nhất thay vì tạo hai Dockerfiles. Hãy nghĩ về ưu và nhược điểm của việc sử dụng phương pháp này trên hai tệp khác nhau.
Bạn có nhận thấy rằng chúng tôi đã tạo một người dùng không phải root không? Theo mặc định, Docker chạy các quy trình vùng chứa dưới dạng root bên trong vùng chứa. Đây là một thực tiễn xấu vì những kẻ tấn công có thể giành được quyền truy cập root vào máy chủ Docker nếu chúng thoát ra khỏi vùng chứa. Nếu bạn root trong vùng chứa, bạn sẽ root trên máy chủ.
Cập nhật web
dịch vụ trong tệp Dockerfile.prod.yml để xây dựng với Dockerfile.prod :
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
Hãy dùng thử:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Tiếp theo, hãy thêm Nginx vào hỗn hợp để hoạt động như một proxy ngược cho Gunicorn để xử lý các yêu cầu của khách hàng cũng như cung cấp các tệp tĩnh.
Thêm dịch vụ vào docker-compile.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Sau đó, trong thư mục gốc của dự án cục bộ, hãy tạo các tệp và thư mục sau:
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile :
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Xem lại Sử dụng NGINX và NGINX Plus làm Cổng ứng dụng với uWSGI và Django để biết thêm thông tin về cách định cấu hình Nginx hoạt động với Django.
Sau đó, cập nhật web
dịch vụ, trong docker-compos.prod.yml , thay thế ports
bằng expose
:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
Bây giờ, cổng 8000 chỉ được tiếp xúc nội bộ với các dịch vụ Docker khác. Cổng sẽ không được xuất bản cho máy chủ nữa.
Để biết thêm về các cổng và hiển thị, hãy xem lại câu hỏi Stack Overflow này.
Kiểm tra nó một lần nữa.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Đảm bảo ứng dụng được thiết lập và chạy tại http: // localhost: 1337 .
Cấu trúc dự án của bạn bây giờ sẽ giống như sau:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── app
│ ├── Dockerfile
│ ├── Dockerfile.prod
│ ├── entrypoint.prod.sh
│ ├── entrypoint.sh
│ ├── hello_django
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf
Mang các vùng chứa xuống sau khi hoàn tất:
$ docker-compose -f docker-compose.prod.yml down -v
Vì Gunicorn là một máy chủ ứng dụng, nó sẽ không phục vụ các tệp tĩnh. Vì vậy, làm thế nào để xử lý cả tệp tĩnh và tệp phương tiện trong cấu hình cụ thể này?
Cập nhật settings.py :
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
Bây giờ, bất kỳ yêu cầu nào http://localhost:8000/static/*
sẽ được phục vụ từ thư mục "staticfiles".
Để kiểm tra, trước tiên, hãy xây dựng lại các hình ảnh và xoay các vùng chứa mới theo bình thường. Đảm bảo các tệp tĩnh vẫn đang được phân phối chính xác tại http: // localhost: 8000 / admin .
Đối với phiên bản sản xuất, hãy thêm một khối lượng vào web
và nginx
các dịch vụ trong docker-compile.prod.yml để mỗi vùng chứa sẽ chia sẻ một thư mục có tên "staticfiles":
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
Chúng tôi cũng cần tạo thư mục "/ home / app / web / staticfiles" trong Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME
...
Tại sao điều này là cần thiết?
Docker Compose thường gắn kết các ổ đĩa có tên là root. Và vì chúng tôi đang sử dụng người dùng không phải root, chúng tôi sẽ gặp lỗi bị từ chối cấp quyền khi collectstatic
lệnh được chạy nếu thư mục chưa tồn tại
Để giải quyết vấn đề này, bạn có thể:
Chúng tôi đã sử dụng trước đây.
Tiếp theo, cập nhật cấu hình Nginx để định tuyến các yêu cầu tệp tĩnh đến thư mục "staticfiles":
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
}
Xoay xuống các vùng chứa phát triển:
$ docker-compose down -v
Bài kiểm tra:
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Một lần nữa, các yêu cầu đến http://localhost:1337/static/*
sẽ được phục vụ từ thư mục "staticfiles".
Điều hướng đến http: // localhost: 1337 / admin và đảm bảo nội dung tĩnh tải chính xác.
Bạn cũng có thể xác minh trong nhật ký - thông qua docker-compose -f docker-compose.prod.yml logs -f
- rằng các yêu cầu đối với tệp tĩnh được phân phối thành công qua Nginx:
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/ HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 2214 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/base.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/responsive.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/login.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/fonts.css HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/base.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
Mang theo các thùng chứa sau khi hoàn thành:
$ docker-compose -f docker-compose.prod.yml down -v
Để kiểm tra việc xử lý các tệp phương tiện, hãy bắt đầu bằng cách tạo một ứng dụng Django mới:
$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload
Thêm ứng dụng mới vào INSTALLED_APPS
danh sách trong settings.py :
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"upload",
]
app / upload / views.py :
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage
def image_upload(request):
if request.method == "POST" and request.FILES["image_file"]:
image_file = request.FILES["image_file"]
fs = FileSystemStorage()
filename = fs.save(image_file.name, image_file)
image_url = fs.url(filename)
print(image_url)
return render(request, "upload.html", {
"image_url": image_url
})
return render(request, "upload.html")
Thêm "mẫu", thư mục vào thư mục "ứng dụng / tải lên", sau đó thêm mẫu mới có tên upload.html :
{% block content %}
<form action="{% url "upload" %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="image_file">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
{% endblock %}
app / hello_django / urls.py :
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from upload.views import image_upload
urlpatterns = [
path("", image_upload, name="upload"),
path("admin/", admin.site.urls),
]
if bool(settings.DEBUG):
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
app / hello_django / settings.py :
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
Bài kiểm tra:
$ docker-compose up -d --build
Bạn có thể tải lên hình ảnh tại http: // localhost: 8000 / , sau đó xem hình ảnh tại http: // localhost: 8000 / media / IMAGE_FILE_NAME .
Để sản xuất, hãy thêm một khối lượng khác vào web
và nginx
các dịch vụ:
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
media_volume:
Tạo thư mục "/ home / app / web / mediafiles" trong Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME/mediafiles
WORKDIR $APP_HOME
...
Cập nhật lại cấu hình Nginx:
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
location /media/ {
alias /home/app/web/mediafiles/;
}
}
Xây dựng lại:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Kiểm tra nó một lần cuối cùng:
Nếu gặp
413 Request Entity Too Large
lỗi, bạn sẽ cần tăng kích thước tối đa được phép của nội dung yêu cầu ứng dụng khách trong ngữ cảnh máy chủ hoặc vị trí trong cấu hình Nginx.Thí dụ:
location / { proxy_pass http://hello_django; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; client_max_body_size 100M; }
Trong hướng dẫn này, chúng tôi đã hướng dẫn cách chứa một ứng dụng web Django với Postgres để phát triển. Chúng tôi cũng đã tạo tệp Docker Compose sẵn sàng sản xuất để thêm Gunicorn và Nginx vào hỗn hợp để xử lý tệp tĩnh và tệp phương tiện. Bây giờ bạn có thể kiểm tra thiết lập sản xuất cục bộ.
Về triển khai thực tế cho môi trường sản xuất, bạn có thể muốn sử dụng:
db
và nginx
các dịch vụĐối với các mẹo sản xuất khác, hãy xem lại cuộc thảo luận này .
Bạn có thể tìm thấy mã trong repo django-on-docker .
Nguồn: https://testdriven.io
1660704900
Это пошаговое руководство, в котором подробно описано, как настроить Django для работы в Docker с Postgres. Для производственных сред мы добавим Nginx и Gunicorn. Мы также рассмотрим, как обслуживать статические и мультимедийные файлы Django через Nginx.
Зависимости :
Создайте новый каталог проекта вместе с новым проектом Django:
$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.9 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver
Перейдите по адресу http://localhost:8000/ , чтобы просмотреть экран приветствия Django. Убейте сервер, как только закончите. Затем выйдите из виртуальной среды и удалите ее. Теперь у нас есть простой проект Django для работы.
Создайте файл requirements.txt в каталоге «app» и добавьте Django в качестве зависимости:
Django==3.2.6
Поскольку мы будем переходить на Postgres, удалите файл db.sqlite3 из каталога «app».
Каталог вашего проекта должен выглядеть так:
└── app
├── hello_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt
Установите Docker , если у вас его еще нет, затем добавьте Dockerfile в каталог «app»:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Итак, мы начали с образа Docker на основе Alpine для Python 3.9.6. Затем мы устанавливаем рабочий каталог вместе с двумя переменными среды:
PYTHONDONTWRITEBYTECODE
: запрещает Python записывать файлы pyc на диск (эквивалентно python -B
опции )PYTHONUNBUFFERED
: запрещает Python буферизовать stdout и stderr (эквивалентно python -u
опции )Наконец, мы обновили Pip, скопировали файл requirements.txt , установили зависимости и скопировали сам проект Django.
Просмотрите Docker для разработчиков Python , чтобы узнать больше о структурировании Dockerfiles, а также о некоторых рекомендациях по настройке Docker для разработки на основе Python.
Затем добавьте файл docker-compose.yml в корень проекта:
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
Просмотрите справочник по файлу Compose, чтобы узнать, как работает этот файл.
Обновите переменные SECRET_KEY
, DEBUG
и в settings.py :ALLOWED_HOSTS
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
Не забудьте добавить импорт вверху:
import os
Затем создайте файл .env.dev в корне проекта для хранения переменных среды для разработки:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
Создайте образ:
$ docker-compose build
После сборки образа запустите контейнер:
$ docker-compose up -d
Перейдите по адресу http://localhost:8000/ , чтобы снова просмотреть экран приветствия.
Проверьте наличие ошибок в журналах, если это не работает через
docker-compose logs -f
.
Чтобы настроить Postgres, нам нужно добавить новый сервис в файл docker-compose.yml , обновить настройки Django и установить Psycopg2 .
Во- первых, добавьте новую службу с именем db
в docker-compose.yml :
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_django
- POSTGRES_PASSWORD=hello_django
- POSTGRES_DB=hello_django_dev
volumes:
postgres_data:
Чтобы сохранить данные после окончания срока службы контейнера, мы настроили том. Эта конфигурация будет привязана postgres_data
к каталогу «/var/lib/postgresql/data/» в контейнере.
Мы также добавили ключ среды для определения имени базы данных по умолчанию и установки имени пользователя и пароля.
Дополнительные сведения см. в разделе «Переменные среды» на странице Postgres Docker Hub .
Нам также понадобятся некоторые новые переменные среды для web
службы, поэтому обновите .env.dev следующим образом:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
Обновите DATABASES
dict в settings.py :
DATABASES = {
"default": {
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
"USER": os.environ.get("SQL_USER", "user"),
"PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
"HOST": os.environ.get("SQL_HOST", "localhost"),
"PORT": os.environ.get("SQL_PORT", "5432"),
}
}
Здесь база данных настроена на основе переменных среды, которые мы только что определили. Обратите внимание на значения по умолчанию.
Обновите Dockerfile, чтобы установить соответствующие пакеты, необходимые для Psycopg2:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Добавьте Psycopg2 в файл requirements.txt :
Django==3.2.6
psycopg2-binary==2.9.1
Просмотрите этот выпуск GitHub для получения дополнительной информации об установке Psycopg2 в образе Docker на основе Alpine.
Создайте новый образ и запустите два контейнера:
$ docker-compose up -d --build
Запустите миграции:
$ docker-compose exec web python manage.py migrate --noinput
Получить следующую ошибку?
django.db.utils.OperationalError: FATAL: база данных «hello_django_dev» не существует
Запустите
docker-compose down -v
, чтобы удалить тома вместе с контейнерами. Затем пересоберите образы, запустите контейнеры и примените миграции.
Убедитесь, что таблицы Django по умолчанию созданы:
$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev
psql (13.0)
Type "help" for help.
hello_django_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
------------------+--------------+----------+------------+------------+-------------------------------
hello_django_dev | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
template1 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
(4 rows)
hello_django_dev=# \c hello_django_dev
You are now connected to database "hello_django_dev" as user "hello_django".
hello_django_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+--------------
public | auth_group | table | hello_django
public | auth_group_permissions | table | hello_django
public | auth_permission | table | hello_django
public | auth_user | table | hello_django
public | auth_user_groups | table | hello_django
public | auth_user_user_permissions | table | hello_django
public | django_admin_log | table | hello_django
public | django_content_type | table | hello_django
public | django_migrations | table | hello_django
public | django_session | table | hello_django
(10 rows)
hello_django_dev=# \q
Вы также можете проверить, что том был создан, запустив:
$ docker volume inspect django-on-docker_postgres_data
Вы должны увидеть что-то похожее на:
[
{
"CreatedAt": "2021-08-23T15:49:08Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-on-docker",
"com.docker.compose.version": "1.29.2",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/django-on-docker_postgres_data/_data",
"Name": "django-on-docker_postgres_data",
"Options": null,
"Scope": "local"
}
]
Затем добавьте файл entrypoint.sh в каталог «app», чтобы убедиться, что Postgres исправен, прежде чем применять миграции и запускать сервер разработки Django:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python manage.py flush --no-input
python manage.py migrate
exec "$@"
Обновите права доступа к файлу локально:
$ chmod +x app/entrypoint.sh
Затем обновите файл Dockerfile, скопировав файл entrypoint.sh , и запустите его как команду точки входа Docker :
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy entrypoint.sh
COPY ./entrypoint.sh .
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Добавьте DATABASE
переменную окружения в .env.dev :
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
Проверьте еще раз:
Во-первых, несмотря на добавление Postgres, мы по-прежнему можем создать независимый образ Docker для Django, если для DATABASE
переменной среды не задано значение postgres
. Для проверки создайте новый образ и запустите новый контейнер:
$ docker build -f ./app/Dockerfile -t hello_django:latest ./app
$ docker run -d \
-p 8006:8000 \
-e "SECRET_KEY=please_change_me" -e "DEBUG=1" -e "DJANGO_ALLOWED_HOSTS=*" \
hello_django python /usr/src/app/manage.py runserver 0.0.0.0:8000
Вы должны увидеть страницу приветствия по адресу http://localhost:8006.
Во-вторых, вы можете закомментировать команды очистки и миграции базы данных в сценарии entrypoint.sh , чтобы они не запускались при каждом запуске или перезапуске контейнера:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
# python manage.py flush --no-input
# python manage.py migrate
exec "$@"
Вместо этого вы можете запускать их вручную после того, как контейнеры раскрутятся, например:
$ docker-compose exec web python manage.py flush --no-input
$ docker-compose exec web python manage.py migrate
Двигаясь дальше, для производственных сред давайте добавим Gunicorn , сервер WSGI производственного уровня, в файл требований:
Django==3.2.6
gunicorn==20.1.0
psycopg2-binary==2.9.1
Интересуетесь WSGI и Gunicorn? Просмотрите главу WSGI из курса « Создание собственной веб-инфраструктуры Python» .
Поскольку мы по-прежнему хотим использовать встроенный сервер Django в процессе разработки, создайте новый файл компоновки с именем docker-compose.prod.yml для производства:
version: '3.8'
services:
web:
build: ./app
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
volumes:
postgres_data:
Если у вас несколько сред, вы можете рассмотреть использование файла конфигурации docker-compose.override.yml . При таком подходе вы добавляете свою базовую конфигурацию в файл docker-compose.yml , а затем используете файл docker-compose.override.yml для переопределения этих параметров конфигурации в зависимости от среды.
Обратите внимание на значение по умолчанию command
. Мы используем Gunicorn, а не сервер разработки Django. Мы также удалили том из web
службы, так как он нам не нужен в рабочей среде. Наконец, мы используем отдельные файлы переменных среды для определения переменных среды для обеих служб, которые будут переданы в контейнер во время выполнения.
.env.prod :
DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
.env.prod.db :
POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod
Добавьте два файла в корень проекта. Вы, вероятно, захотите сохранить их вне контроля версий, поэтому добавьте их в файл .gitignore .
Снесите контейнеры разработки (и связанные тома с -v
флагом):
$ docker-compose down -v
Затем создайте производственные образы и разверните контейнеры:
$ docker-compose -f docker-compose.prod.yml up -d --build
Убедитесь, что hello_django_prod
база данных была создана вместе с таблицами Django по умолчанию. Проверьте страницу администратора по адресу http://localhost:8000/admin . Статические файлы больше не загружаются. Это ожидается, поскольку режим отладки выключен. Мы исправим это в ближайшее время.
Опять же, если контейнер не запускается, проверьте наличие ошибок в логах через
docker-compose -f docker-compose.prod.yml logs -f
.
Вы заметили, что мы по-прежнему выполняем очистку базы данных ( которая очищает базу данных) и команды переноса при каждом запуске контейнера? Это нормально в разработке, но давайте создадим новый файл точки входа для производства.
точка входа.prod.sh :
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Обновите права доступа к файлу локально:
$ chmod +x app/entrypoint.prod.sh
Чтобы использовать этот файл, создайте новый файл Dockerfile с именем Dockerfile.prod для использования с производственными сборками:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.9.6-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.2
COPY . .
RUN flake8 --ignore=E501,F401 .
# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.9.6-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh
RUN chmod +x $APP_HOME/entrypoint.prod.sh
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Здесь мы использовали многоэтапную сборку Docker, чтобы уменьшить окончательный размер образа. По сути, builder
это временный образ, который используется для создания колес Python. Затем колеса копируются в окончательный производственный образ, и этот builder
образ удаляется.
You could take the multi-stage build approach a step further and use a single Dockerfile instead of creating two Dockerfiles. Think of the pros and cons of using this approach over two different files.
Did you notice that we created a non-root user? By default, Docker runs container processes as root inside of a container. This is a bad practice since attackers can gain root access to the Docker host if they manage to break out of the container. If you're root in the container, you'll be root on the host.
Update the web
service within the docker-compose.prod.yml file to build with Dockerfile.prod:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
Try it out:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Затем давайте добавим Nginx, который будет выступать в качестве обратного прокси -сервера для Gunicorn для обработки клиентских запросов, а также для обслуживания статических файлов.
Добавьте сервис в docker-compose.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Затем в локальном корне проекта создайте следующие файлы и папки:
└── nginx
├── Dockerfile
└── nginx.conf
Докерфайл :
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Ознакомьтесь с разделом Использование NGINX и NGINX Plus в качестве шлюза приложений с uWSGI и Django , чтобы получить дополнительные сведения о настройке Nginx для работы с Django.
Затем обновите web
службу в docker-compose.prod.yml , заменив ports
на expose
:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
Теперь порт 8000 открыт только внутри, для других служб Docker. Порт больше не будет опубликован на хост-компьютере.
Чтобы узнать больше о портах и экспонировании, просмотрите этот вопрос о переполнении стека.
Проверьте это снова.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Убедитесь, что приложение запущено и работает по адресу http://localhost:1337 .
Теперь структура вашего проекта должна выглядеть так:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── app
│ ├── Dockerfile
│ ├── Dockerfile.prod
│ ├── entrypoint.prod.sh
│ ├── entrypoint.sh
│ ├── hello_django
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf
Когда закончите, опустите контейнеры:
$ docker-compose -f docker-compose.prod.yml down -v
Поскольку Gunicorn является сервером приложений, он не будет обслуживать статические файлы. Итак, как следует обрабатывать как статические, так и мультимедийные файлы в этой конкретной конфигурации?
Обновить settings.py :
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
Теперь любой запрос http://localhost:8000/static/*
будет обслуживаться из каталога «staticfiles».
Для проверки сначала пересоберите образы и запустите новые контейнеры, как обычно. Убедитесь, что статические файлы по-прежнему корректно обслуживаются по адресу http://localhost:8000/admin .
Для рабочей среды добавьте том в службы web
and в файле docker-compose.prod.yml , чтобы каждый контейнер имел общий каталог с именем «staticfiles»:nginx
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
Нам также нужно создать папку «/home/app/web/staticfiles» в Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME
...
Почему это необходимо?
Docker Compose обычно монтирует именованные тома как root. И поскольку мы используем пользователя без полномочий root, мы получим ошибку отказа в разрешении при collectstatic
запуске команды, если каталог еще не существует.
Чтобы обойти это, вы можете:
Мы использовали первое.
Затем обновите конфигурацию Nginx, чтобы направлять запросы статических файлов в папку «staticfiles»:
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
}
Раскрутите контейнеры разработки:
$ docker-compose down -v
Тест:
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Опять же, запросы http://localhost:1337/static/*
будут обслуживаться из каталога «staticfiles».
Перейдите по адресу http://localhost:1337/admin и убедитесь, что статические ресурсы загружаются правильно.
Вы также можете проверить в журналах — через docker-compose -f docker-compose.prod.yml logs -f
— что запросы к статическим файлам успешно обрабатываются через Nginx:
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/ HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 2214 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/base.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/responsive.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/login.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/fonts.css HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/base.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
Когда закончите, принесите контейнеры:
$ docker-compose -f docker-compose.prod.yml down -v
Чтобы проверить работу с медиафайлами, начните с создания нового приложения Django:
$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload
Добавьте новое приложение в INSTALLED_APPS
список в settings.py :
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"upload",
]
приложение/загрузить/views.py :
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage
def image_upload(request):
if request.method == "POST" and request.FILES["image_file"]:
image_file = request.FILES["image_file"]
fs = FileSystemStorage()
filename = fs.save(image_file.name, image_file)
image_url = fs.url(filename)
print(image_url)
return render(request, "upload.html", {
"image_url": image_url
})
return render(request, "upload.html")
Добавьте каталог «templates» в каталог «app/upload», а затем добавьте новый шаблон с именем upload.html :
{% block content %}
<form action="{% url "upload" %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="image_file">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
{% endblock %}
приложение/hello_django/urls.py :
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from upload.views import image_upload
urlpatterns = [
path("", image_upload, name="upload"),
path("admin/", admin.site.urls),
]
if bool(settings.DEBUG):
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
приложение/hello_django/settings.py :
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
Тест:
$ docker-compose up -d --build
Вы должны иметь возможность загрузить изображение по адресу http://localhost:8000/ , а затем просмотреть его по адресу http://localhost:8000/media/IMAGE_FILE_NAME .
Для производства добавьте еще один том в web
and nginx
services:
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
media_volume:
Создайте папку «/home/app/web/mediafiles» в Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME/mediafiles
WORKDIR $APP_HOME
...
Снова обновите конфигурацию Nginx:
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
location /media/ {
alias /home/app/web/mediafiles/;
}
}
Перестроить:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Проверьте это в последний раз:
Если вы видите
413 Request Entity Too Large
ошибку, вам нужно увеличить максимально допустимый размер тела запроса клиента в контексте сервера или местоположения в конфигурации Nginx.Пример:
location / { proxy_pass http://hello_django; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; client_max_body_size 100M; }
В этом руководстве мы рассмотрели, как контейнеризировать веб-приложение Django с помощью Postgres для разработки. Мы также создали готовый к работе файл Docker Compose, который добавляет в смесь Gunicorn и Nginx для обработки статических и мультимедийных файлов. Теперь вы можете протестировать производственную установку локально.
С точки зрения фактического развертывания в производственной среде вы, вероятно, захотите использовать:
db
иnginx
Чтобы узнать о других советах по производству, ознакомьтесь с этим обсуждением .
Вы можете найти код в репозитории django-on-docker .
Источник: https://testdriven.io
1660697580
這是一個分步教程,詳細介紹瞭如何配置 Django 以使用 Postgres 在 Docker 上運行。對於生產環境,我們將添加 Nginx 和 Gunicorn。我們還將了解如何通過 Nginx 提供 Django 靜態和媒體文件。
依賴項:
創建一個新的項目目錄以及一個新的 Django 項目:
$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.9 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver
導航到http://localhost:8000/以查看 Django 歡迎屏幕。完成後殺死服務器。然後,退出並刪除虛擬環境。我們現在有一個簡單的 Django 項目可以使用。
在“app”目錄中創建一個requirements.txt文件,並將 Django 添加為依賴項:
Django==3.2.6
由於我們將遷移到 Postgres,因此請繼續從“app”目錄中刪除db.sqlite3文件。
您的項目目錄應如下所示:
└── app
├── hello_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt
安裝Docker,如果你還沒有,那麼在“app”目錄下添加一個Dockerfile :
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
因此,我們從Python 3.9.6的基於Alpine的Docker 映像開始。然後我們設置一個工作目錄以及兩個環境變量:
PYTHONDONTWRITEBYTECODE
: 防止 Python 將 pyc 文件寫入磁盤(相當於python -B
option)PYTHONUNBUFFERED
: 防止 Python 緩衝 stdout 和 stderr (相當於python -u
option )Finally, we updated Pip, copied over the requirements.txt file, installed the dependencies, and copied over the Django project itself.
Review Docker for Python Developers for more on structuring Dockerfiles as well as some best practices for configuring Docker for Python-based development.
Next, add a docker-compose.yml file to the project root:
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
Review the Compose file reference for info on how this file works.
Update the SECRET_KEY
, DEBUG
, and ALLOWED_HOSTS
variables in settings.py:
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
Make sure to add the import to the top:
import os
Then, create a .env.dev file in the project root to store environment variables for development:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
Build the image:
$ docker-compose build
Once the image is built, run the container:
$ docker-compose up -d
Navigate to http://localhost:8000/ to again view the welcome screen.
Check for errors in the logs if this doesn't work via
docker-compose logs -f
.
To configure Postgres, we'll need to add a new service to the docker-compose.yml file, update the Django settings, and install Psycopg2.
First, add a new service called db
to docker-compose.yml:
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_django
- POSTGRES_PASSWORD=hello_django
- POSTGRES_DB=hello_django_dev
volumes:
postgres_data:
To persist the data beyond the life of the container we configured a volume. This config will bind postgres_data
to the "/var/lib/postgresql/data/" directory in the container.
We also added an environment key to define a name for the default database and set a username and password.
Review the "Environment Variables" section of the Postgres Docker Hub page for more info.
We'll need some new environment variables for the web
service as well, so update .env.dev like so:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
Update the DATABASES
dict in settings.py:
DATABASES = {
"default": {
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
"USER": os.environ.get("SQL_USER", "user"),
"PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
"HOST": os.environ.get("SQL_HOST", "localhost"),
"PORT": os.environ.get("SQL_PORT", "5432"),
}
}
Here, the database is configured based on the environment variables that we just defined. Take note of the default values.
Update the Dockerfile to install the appropriate packages required for Psycopg2:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Add Psycopg2 to requirements.txt:
Django==3.2.6
psycopg2-binary==2.9.1
Review this GitHub Issue for more info on installing Psycopg2 in an Alpine-based Docker Image.
Build the new image and spin up the two containers:
$ docker-compose up -d --build
Run the migrations:
$ docker-compose exec web python manage.py migrate --noinput
Get the following error?
django.db.utils.OperationalError: FATAL: database "hello_django_dev" does not exist
Run
docker-compose down -v
to remove the volumes along with the containers. Then, re-build the images, run the containers, and apply the migrations.
Ensure the default Django tables were created:
$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev
psql (13.0)
Type "help" for help.
hello_django_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
------------------+--------------+----------+------------+------------+-------------------------------
hello_django_dev | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
template1 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
(4 rows)
hello_django_dev=# \c hello_django_dev
You are now connected to database "hello_django_dev" as user "hello_django".
hello_django_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+--------------
public | auth_group | table | hello_django
public | auth_group_permissions | table | hello_django
public | auth_permission | table | hello_django
public | auth_user | table | hello_django
public | auth_user_groups | table | hello_django
public | auth_user_user_permissions | table | hello_django
public | django_admin_log | table | hello_django
public | django_content_type | table | hello_django
public | django_migrations | table | hello_django
public | django_session | table | hello_django
(10 rows)
hello_django_dev=# \q
You can check that the volume was created as well by running:
$ docker volume inspect django-on-docker_postgres_data
You should see something similar to:
[
{
"CreatedAt": "2021-08-23T15:49:08Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-on-docker",
"com.docker.compose.version": "1.29.2",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/django-on-docker_postgres_data/_data",
"Name": "django-on-docker_postgres_data",
"Options": null,
"Scope": "local"
}
]
Next, add an entrypoint.sh file to the "app" directory to verify that Postgres is healthy before applying the migrations and running the Django development server:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python manage.py flush --no-input
python manage.py migrate
exec "$@"
Update the file permissions locally:
$ chmod +x app/entrypoint.sh
Then, update the Dockerfile to copy over the entrypoint.sh file and run it as the Docker entrypoint command:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy entrypoint.sh
COPY ./entrypoint.sh .
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
將DATABASE
環境變量添加到.env.dev:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
再次測試一下:
首先,儘管添加了 Postgres,我們仍然可以為 Django 創建一個獨立的 Docker 鏡像,只要DATABASE
環境變量不設置為postgres
. 要進行測試,請構建一個新映像,然後運行一個新容器:
$ docker build -f ./app/Dockerfile -t hello_django:latest ./app
$ docker run -d \
-p 8006:8000 \
-e "SECRET_KEY=please_change_me" -e "DEBUG=1" -e "DJANGO_ALLOWED_HOSTS=*" \
hello_django python /usr/src/app/manage.py runserver 0.0.0.0:8000
您應該能夠在http://localhost:8006查看歡迎頁面
其次,您可能希望在entrypoint.sh腳本中註釋掉數據庫刷新和遷移命令,這樣它們就不會在每次容器啟動或重新啟動時運行:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
# python manage.py flush --no-input
# python manage.py migrate
exec "$@"
相反,您可以在容器啟動後手動運行它們,如下所示:
$ docker-compose exec web python manage.py flush --no-input
$ docker-compose exec web python manage.py migrate
繼續前進,對於生產環境,讓我們將生產級 WSGI 服務器Gunicorn添加到需求文件中:
Django==3.2.6
gunicorn==20.1.0
psycopg2-binary==2.9.1
對 WSGI 和 Gunicorn 感到好奇嗎?查看構建您自己的 Python Web 框架課程中的WSGI章節。
由於我們仍然想在開發中使用 Django 的內置服務器,因此創建一個名為docker-compose.prod.yml的新 compose 文件用於生產:
version: '3.8'
services:
web:
build: ./app
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
volumes:
postgres_data:
如果您有多個環境,您可能需要查看使用docker-compose.override.yml配置文件。使用這種方法,您可以將基本配置添加到docker-compose.yml文件中,然後使用docker-compose.override.yml文件根據環境覆蓋這些配置設置。
記下默認的command
. 我們正在運行 Gunicorn 而不是 Django 開發服務器。我們還從web
服務中刪除了該卷,因為我們在生產中不需要它。最後,我們使用單獨的環境變量文件為兩個服務定義環境變量,這些服務將在運行時傳遞給容器。
.env.prod:
DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
.env.prod.db:
POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod
將這兩個文件添加到項目根目錄。您可能希望將它們排除在版本控制之外,因此將它們添加到.gitignore文件中。
關閉開發容器(以及帶有標誌的相關卷):-v
$ docker-compose down -v
然後,構建生產映像並啟動容器:
$ docker-compose -f docker-compose.prod.yml up -d --build
驗證hello_django_prod
數據庫是否與默認的 Django 表一起創建。在http://localhost:8000/admin測試管理頁面。不再加載靜態文件。這是預期的,因為調試模式已關閉。我們很快就會解決這個問題。
同樣,如果容器無法啟動,請通過 . 檢查日誌中的錯誤
docker-compose -f docker-compose.prod.yml logs -f
。
您是否注意到我們仍在運行數據庫刷新(清除數據庫)並在每次運行容器時遷移命令?這在開發中很好,但是讓我們為生產創建一個新的入口點文件。
entrypoint.prod.sh:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
在本地更新文件權限:
$ chmod +x app/entrypoint.prod.sh
要使用此文件,請創建一個名為Dockerfile.prod的新 Dockerfile以用於生產構建:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.9.6-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.2
COPY . .
RUN flake8 --ignore=E501,F401 .
# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.9.6-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh
RUN chmod +x $APP_HOME/entrypoint.prod.sh
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
在這裡,我們使用了 Docker多階段構建來減小最終圖像的大小。本質上,builder
是用於構建 Python 輪子的臨時圖像。然後將輪子復製到最終的生產圖像中,然後builder
丟棄圖像。
您可以將多階段構建方法更進一步,使用單個Dockerfile而不是創建兩個 Dockerfile。想想在兩個不同的文件上使用這種方法的利弊。
您是否注意到我們創建了一個非 root 用戶?默認情況下,Docker 在容器內以 root 身份運行容器進程。這是一種不好的做法,因為如果攻擊者設法突破容器,他們可以獲得對 Docker 主機的 root 訪問權限。如果您是容器中的根,那麼您將是主機上的根。
更新docker-compose.prod.yml文件中的web
服務以使用Dockerfile.prod構建:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
試試看:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
接下來,讓我們將 Nginx 添加到組合中,作為 Gunicorn 的反向代理來處理客戶端請求以及提供靜態文件。
將服務添加到docker-compose.prod.yml:
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
然後,在本地項目根目錄中,創建以下文件和文件夾:
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile:
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf:
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
查看使用 NGINX 和 NGINX Plus 作為 uWSGI 和 Django 的應用程序網關,以獲取有關配置 Nginx 以與 Django 一起使用的更多信息。
然後,在docker-compose.prod.ymlweb
中更新服務,替換為:portsexpose
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
現在,端口 8000 僅在內部公開給其他 Docker 服務。該端口將不再發佈到主機。
有關端口與公開的更多信息,請查看此Stack Overflow 問題。
再次測試一下。
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
確保應用程序在http://localhost:1337啟動並運行。
您的項目結構現在應該如下所示:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── app
│ ├── Dockerfile
│ ├── Dockerfile.prod
│ ├── entrypoint.prod.sh
│ ├── entrypoint.sh
│ ├── hello_django
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf
完成後將容器放下:
$ docker-compose -f docker-compose.prod.yml down -v
由於 Gunicorn 是一個應用服務器,它不會提供靜態文件。那麼,在這個特定配置中應該如何處理靜態文件和媒體文件呢?
更新settings.py:
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
現在,任何請求http://localhost:8000/static/*
都將從“staticfiles”目錄提供。
要進行測試,首先要重新構建映像並按照慣例啟動新容器。確保靜態文件仍在http://localhost:8000/admin上正確提供。
對於生產,向docker-compose.prod.ymlweb
中的和nginx
服務添加一個卷,以便每個容器共享一個名為“staticfiles”的目錄:
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
我們還需要在 Dockerfile.prod 中創建“/home/app/web/staticfiles”文件夾:
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME
...
為什麼這是必要的?
Docker Compose 通常將命名卷掛載為 root。由於我們使用的是非 root 用戶,collectstatic
如果目錄不存在,我們將在運行命令時收到權限被拒絕錯誤
要解決此問題,您可以:
我們使用了前者。
接下來,更新 Nginx 配置以將靜態文件請求路由到“staticfiles”文件夾:
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
}
降低開發容器的速度:
$ docker-compose down -v
測試:
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
同樣,請求http://localhost:1337/static/*
將從“staticfiles”目錄提供。
導航到http://localhost:1337/admin並確保靜態資產正確加載。
您還可以在日誌中驗證——通過docker-compose -f docker-compose.prod.yml logs -f
——對靜態文件的請求是否通過 Nginx 成功提供:
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/ HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 2214 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/base.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/responsive.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/login.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/fonts.css HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/base.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
完成後帶上容器:
$ docker-compose -f docker-compose.prod.yml down -v
要測試媒體文件的處理,首先創建一個新的 Django 應用程序:
$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload
將新應用添加到settings.pyINSTALLED_APPS
的列表中:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"upload",
]
應用/上傳/views.py:
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage
def image_upload(request):
if request.method == "POST" and request.FILES["image_file"]:
image_file = request.FILES["image_file"]
fs = FileSystemStorage()
filename = fs.save(image_file.name, image_file)
image_url = fs.url(filename)
print(image_url)
return render(request, "upload.html", {
"image_url": image_url
})
return render(request, "upload.html")
添加一個“templates”目錄到“app/upload”目錄,然後添加一個名為upload.html的新模板:
{% block content %}
<form action="{% url "upload" %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="image_file">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
{% endblock %}
應用程序/hello_django/urls.py:
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from upload.views import image_upload
urlpatterns = [
path("", image_upload, name="upload"),
path("admin/", admin.site.urls),
]
if bool(settings.DEBUG):
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
應用程序/hello_django/settings.py:
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
測試:
$ docker-compose up -d --build
您應該能夠在http://localhost:8000/上傳圖像,然後在http://localhost:8000/media/IMAGE_FILE_NAME中查看圖像。
對於生產,向web
和nginx
服務添加另一個卷:
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
media_volume:
在 Dockerfile.prod 中創建“/home/app/web/mediafiles”文件夾:
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME/mediafiles
WORKDIR $APP_HOME
...
再次更新 Nginx 配置:
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
location /media/ {
alias /home/app/web/mediafiles/;
}
}
重建:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
最後一次測試一下:
如果您看到
413 Request Entity Too Large
錯誤,則需要在 Nginx 配置中的服務器或位置上下文中增加客戶端請求正文的最大允許大小。例子:
location / { proxy_pass http://hello_django; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; client_max_body_size 100M; }
在本教程中,我們介紹瞭如何使用 Postgres 將 Django Web 應用程序容器化以進行開發。我們還創建了一個生產就緒的 Docker Compose 文件,將 Gunicorn 和 Nginx 添加到混合中以處理靜態和媒體文件。您現在可以在本地測試生產設置。
就實際部署到生產環境而言,您可能希望使用:
有關其他生產技巧,請查看此討論。
您可以在django-on-docker repo 中找到代碼。
1660690320
Il s'agit d'un tutoriel étape par étape qui détaille comment configurer Django pour qu'il s'exécute sur Docker avec Postgres. Pour les environnements de production, nous ajouterons Nginx et Gunicorn. Nous verrons également comment servir les fichiers statiques et multimédias de Django via Nginx.
Dépendances :
Créez un nouveau répertoire de projet avec un nouveau projet Django :
$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.9 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver
Accédez à http://localhost:8000/ pour afficher l'écran de bienvenue de Django. Tuez le serveur une fois terminé. Ensuite, quittez et supprimez l'environnement virtuel. Nous avons maintenant un projet Django simple avec lequel travailler.
Créez un fichier requirements.txt dans le répertoire "app" et ajoutez Django comme dépendance :
Django==3.2.6
Puisque nous allons passer à Postgres, allez-y et supprimez le fichier db.sqlite3 du répertoire "app".
Votre répertoire de projet devrait ressembler à :
└── app
├── hello_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt
Installez Docker , si vous ne l'avez pas déjà, puis ajoutez un Dockerfile au répertoire "app":
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Nous avons donc commencé avec une image Docker basée sur Alpine pour Python 3.9.6 . Nous définissons ensuite un répertoire de travail avec deux variables d'environnement :
PYTHONDONTWRITEBYTECODE
: Empêche Python d'écrire des fichiers pyc sur le disque (équivalent à l' python -B
option )PYTHONUNBUFFERED
: Empêche Python de mettre en mémoire tampon stdout et stderr (équivalent à python -u
option )Enfin, nous avons mis à jour Pip, copié le fichier requirements.txt , installé les dépendances et copié le projet Django lui-même.
Consultez Docker pour les développeurs Python pour en savoir plus sur la structuration des Dockerfiles ainsi que sur les meilleures pratiques de configuration de Docker pour le développement basé sur Python.
Ensuite, ajoutez un fichier docker-compose.yml à la racine du projet :
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
Consultez la référence du fichier Compose pour plus d'informations sur le fonctionnement de ce fichier.
Mettez à jour les variables SECRET_KEY
, DEBUG
et dans settings.py :ALLOWED_HOSTS
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
Assurez-vous d'ajouter l'importation en haut :
import os
Ensuite, créez un fichier .env.dev à la racine du projet pour stocker les variables d'environnement pour le développement :
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
Construisez l'image :
$ docker-compose build
Une fois l'image créée, exécutez le conteneur :
$ docker-compose up -d
Accédez à http://localhost:8000/ pour afficher à nouveau l'écran de bienvenue.
Vérifiez les erreurs dans les journaux si cela ne fonctionne pas via
docker-compose logs -f
.
Pour configurer Postgres, nous devrons ajouter un nouveau service au fichier docker-compose.yml , mettre à jour les paramètres de Django et installer Psycopg2 .
Tout d'abord, ajoutez un nouveau service appelé db
à docker-compose.yml :
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_django
- POSTGRES_PASSWORD=hello_django
- POSTGRES_DB=hello_django_dev
volumes:
postgres_data:
Pour conserver les données au-delà de la durée de vie du conteneur, nous avons configuré un volume. Cette configuration se liera postgres_data
au répertoire "/var/lib/postgresql/data/" dans le conteneur.
Nous avons également ajouté une clé d'environnement pour définir un nom pour la base de données par défaut et définir un nom d'utilisateur et un mot de passe.
Consultez la section "Variables d'environnement" de la page Postgres Docker Hub pour plus d'informations.
Nous aurons également besoin de nouvelles variables d'environnement pour le web
service, alors mettez à jour .env.dev comme suit :
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
Mettez à jour le DATABASES
dict dans settings.py :
DATABASES = {
"default": {
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
"USER": os.environ.get("SQL_USER", "user"),
"PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
"HOST": os.environ.get("SQL_HOST", "localhost"),
"PORT": os.environ.get("SQL_PORT", "5432"),
}
}
Ici, la base de données est configurée en fonction des variables d'environnement que nous venons de définir. Prenez note des valeurs par défaut.
Mettez à jour le Dockerfile pour installer les packages appropriés requis pour Psycopg2 :
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Ajoutez Psycopg2 à requirements.txt :
Django==3.2.6
psycopg2-binary==2.9.1
Consultez ce problème GitHub pour plus d'informations sur l'installation de Psycopg2 dans une image Docker basée sur Alpine.
Créez la nouvelle image et faites tourner les deux conteneurs :
$ docker-compose up -d --build
Exécutez les migrations :
$ docker-compose exec web python manage.py migrate --noinput
Vous obtenez l'erreur suivante ?
django.db.utils.OperationalError : FATAL : la base de données "hello_django_dev" n'existe pas
Exécutez
docker-compose down -v
pour supprimer les volumes avec les conteneurs. Ensuite, reconstruisez les images, exécutez les conteneurs et appliquez les migrations.
Assurez-vous que les tables Django par défaut ont été créées :
$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev
psql (13.0)
Type "help" for help.
hello_django_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
------------------+--------------+----------+------------+------------+-------------------------------
hello_django_dev | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
template1 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
(4 rows)
hello_django_dev=# \c hello_django_dev
You are now connected to database "hello_django_dev" as user "hello_django".
hello_django_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+--------------
public | auth_group | table | hello_django
public | auth_group_permissions | table | hello_django
public | auth_permission | table | hello_django
public | auth_user | table | hello_django
public | auth_user_groups | table | hello_django
public | auth_user_user_permissions | table | hello_django
public | django_admin_log | table | hello_django
public | django_content_type | table | hello_django
public | django_migrations | table | hello_django
public | django_session | table | hello_django
(10 rows)
hello_django_dev=# \q
Vous pouvez vérifier que le volume a également été créé en exécutant :
$ docker volume inspect django-on-docker_postgres_data
Vous devriez voir quelque chose de similaire à :
[
{
"CreatedAt": "2021-08-23T15:49:08Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-on-docker",
"com.docker.compose.version": "1.29.2",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/django-on-docker_postgres_data/_data",
"Name": "django-on-docker_postgres_data",
"Options": null,
"Scope": "local"
}
]
Ensuite, ajoutez un fichier entrypoint.sh au répertoire "app" pour vérifier que Postgres est sain avant d'appliquer les migrations et d'exécuter le serveur de développement Django :
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python manage.py flush --no-input
python manage.py migrate
exec "$@"
Mettez à jour les autorisations de fichier localement :
$ chmod +x app/entrypoint.sh
Ensuite, mettez à jour le Dockerfile pour copier le fichier entrypoint.sh et exécutez-le en tant que commande Docker entrypoint :
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy entrypoint.sh
COPY ./entrypoint.sh .
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Ajoutez la DATABASE
variable d'environnement à .env.dev :
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
Testez-le à nouveau :
Tout d'abord, malgré l'ajout de Postgres, nous pouvons toujours créer une image Docker indépendante pour Django tant que la DATABASE
variable d'environnement n'est pas définie sur postgres
. Pour tester, créez une nouvelle image, puis exécutez un nouveau conteneur :
$ docker build -f ./app/Dockerfile -t hello_django:latest ./app
$ docker run -d \
-p 8006:8000 \
-e "SECRET_KEY=please_change_me" -e "DEBUG=1" -e "DJANGO_ALLOWED_HOSTS=*" \
hello_django python /usr/src/app/manage.py runserver 0.0.0.0:8000
Vous devriez pouvoir voir la page d'accueil à l' adresse http://localhost:8006
Deuxièmement, vous pouvez mettre en commentaire les commandes de vidage et de migration de la base de données dans le script entrypoint.sh afin qu'elles ne s'exécutent pas à chaque démarrage ou redémarrage du conteneur :
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
# python manage.py flush --no-input
# python manage.py migrate
exec "$@"
Au lieu de cela, vous pouvez les exécuter manuellement, une fois les conteneurs lancés, comme ceci :
$ docker-compose exec web python manage.py flush --no-input
$ docker-compose exec web python manage.py migrate
Continuons , pour les environnements de production, ajoutons Gunicorn , un serveur WSGI de production, au fichier requirements :
Django==3.2.6
gunicorn==20.1.0
psycopg2-binary==2.9.1
Curieux de connaître WSGI et Gunicorn ? Passez en revue le chapitre WSGI du cours Construire votre propre framework Web Python .
Puisque nous voulons toujours utiliser le serveur intégré de Django dans le développement, créez un nouveau fichier de composition appelé docker-compose.prod.yml pour la production :
version: '3.8'
services:
web:
build: ./app
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
volumes:
postgres_data:
Si vous avez plusieurs environnements, vous pouvez envisager d'utiliser un fichier de configuration docker-compose.override.yml . Avec cette approche, vous ajouteriez votre configuration de base à un fichier docker-compose.yml , puis utiliseriez un fichier docker-compose.override.yml pour remplacer ces paramètres de configuration en fonction de l'environnement.
Prenez note de la valeur par défaut command
. Nous utilisons Gunicorn plutôt que le serveur de développement Django. Nous avons également supprimé le volume du web
service puisque nous n'en avons pas besoin en production. Enfin, nous utilisons des fichiers de variables d'environnement distincts pour définir les variables d'environnement des deux services qui seront transmises au conteneur lors de l'exécution.
.env.prod :
DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
.env.prod.db :
POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod
Ajoutez les deux fichiers à la racine du projet. Vous voudrez probablement les garder hors du contrôle de version, alors ajoutez-les à un fichier .gitignore .
Descendez les conteneurs de développement (et les volumes associés avec le -v
drapeau) :
$ docker-compose down -v
Ensuite, créez les images de production et lancez les conteneurs :
$ docker-compose -f docker-compose.prod.yml up -d --build
Vérifiez que la base de hello_django_prod
données a été créée avec les tables Django par défaut. Testez la page d'administration sur http://localhost:8000/admin . Les fichiers statiques ne sont plus chargés. Ceci est normal car le mode débogage est désactivé. Nous allons corriger cela sous peu.
Encore une fois, si le conteneur ne démarre pas, recherchez les erreurs dans les journaux via
docker-compose -f docker-compose.prod.yml logs -f
.
Avez-vous remarqué que nous exécutons toujours le vidage de la base de données ( qui efface la base de données) et les commandes de migration à chaque exécution du conteneur ? C'est bien en développement, mais créons un nouveau fichier de point d'entrée pour la production.
point d'entrée.prod.sh :
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Mettez à jour les autorisations de fichier localement :
$ chmod +x app/entrypoint.prod.sh
Pour utiliser ce fichier, créez un nouveau Dockerfile appelé Dockerfile.prod à utiliser avec les versions de production :
###########
# BUILDER #
###########
# pull official base image
FROM python:3.9.6-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.2
COPY . .
RUN flake8 --ignore=E501,F401 .
# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.9.6-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh
RUN chmod +x $APP_HOME/entrypoint.prod.sh
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Ici, nous avons utilisé une construction Docker en plusieurs étapes pour réduire la taille finale de l'image. Il s'agit essentiellement builder
d'une image temporaire utilisée pour créer les roues Python. Les roues sont ensuite copiées sur l'image de production finale et l' builder
image est supprimée.
Vous pouvez aller plus loin dans l' approche de construction en plusieurs étapes et utiliser un seul Dockerfile au lieu de créer deux Dockerfiles. Pensez aux avantages et aux inconvénients de l'utilisation de cette approche sur deux fichiers différents.
Avez-vous remarqué que nous avons créé un utilisateur non root ? Par défaut, Docker exécute les processus de conteneur en tant que racine à l'intérieur d'un conteneur. Il s'agit d'une mauvaise pratique car les attaquants peuvent obtenir un accès root à l'hôte Docker s'ils parviennent à sortir du conteneur. Si vous êtes root dans le conteneur, vous serez root sur l'hôte.
Mettez à jour le web
service dans le fichier docker-compose.prod.yml pour créer avec Dockerfile.prod :
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
Essaye le:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Ensuite, ajoutons Nginx dans le mélange pour agir comme un proxy inverse pour Gunicorn pour gérer les demandes des clients ainsi que pour servir des fichiers statiques.
Ajoutez le service à docker-compose.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Ensuite, à la racine du projet local, créez les fichiers et dossiers suivants :
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile :
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Consultez Utilisation de NGINX et NGINX Plus comme passerelle d'application avec uWSGI et Django pour plus d'informations sur la configuration de Nginx pour qu'il fonctionne avec Django.
Ensuite, mettez à jour le web
service, dans docker-compose.prod.yml , en remplaçant ports
par expose
:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
Désormais, le port 8000 n'est exposé qu'en interne, aux autres services Docker. Le port ne sera plus publié sur la machine hôte.
Pour en savoir plus sur les ports par rapport à l'exposition, consultez cette question sur le débordement de pile.
Testez-le à nouveau.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Assurez-vous que l'application est opérationnelle à l' adresse http://localhost:1337 .
La structure de votre projet devrait maintenant ressembler à :
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── app
│ ├── Dockerfile
│ ├── Dockerfile.prod
│ ├── entrypoint.prod.sh
│ ├── entrypoint.sh
│ ├── hello_django
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf
Descendez les conteneurs une fois terminé :
$ docker-compose -f docker-compose.prod.yml down -v
Puisque Gunicorn est un serveur d'applications, il ne servira pas de fichiers statiques. Alors, comment les fichiers statiques et multimédias doivent-ils être gérés dans cette configuration particulière ?
Mettre à jour settings.py :
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
Désormais, toute demande http://localhost:8000/static/*
sera servie à partir du répertoire "staticfiles".
Pour tester, reconstruisez d'abord les images et lancez les nouveaux conteneurs comme d'habitude. Assurez-vous que les fichiers statiques sont toujours correctement servis sur http://localhost:8000/admin .
Pour la production, ajoutez un volume aux services web
et nginx
dans docker-compose.prod.yml afin que chaque conteneur partage un répertoire nommé "staticfiles":
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
Nous devons également créer le dossier "/home/app/web/staticfiles" dans Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME
...
Pourquoi est-ce nécessaire ?
Docker Compose monte normalement les volumes nommés en tant que root. Et puisque nous utilisons un utilisateur non root, nous obtiendrons une erreur d'autorisation refusée lors de l' collectstatic
exécution de la commande si le répertoire n'existe pas déjà
Pour contourner cela, vous pouvez soit :
Nous avons utilisé le premier.
Ensuite, mettez à jour la configuration Nginx pour acheminer les requêtes de fichiers statiques vers le dossier "staticfiles" :
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
}
Faites tourner les conteneurs de développement :
$ docker-compose down -v
Test:
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Encore une fois, les demandes http://localhost:1337/static/*
seront servies à partir du répertoire "staticfiles".
Accédez à http://localhost:1337/admin et assurez-vous que les actifs statiques se chargent correctement.
Vous pouvez également vérifier dans les journaux -- via docker-compose -f docker-compose.prod.yml logs -f
-- que les demandes adressées aux fichiers statiques sont correctement traitées via Nginx :
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/ HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 2214 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/base.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/responsive.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/login.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/fonts.css HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/base.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
Apportez les contenants une fois terminé :
$ docker-compose -f docker-compose.prod.yml down -v
Pour tester la gestion des fichiers multimédias, commencez par créer une nouvelle application Django :
$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload
Ajoutez la nouvelle application à la INSTALLED_APPS
liste dans settings.py :
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"upload",
]
app/upload/views.py :
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage
def image_upload(request):
if request.method == "POST" and request.FILES["image_file"]:
image_file = request.FILES["image_file"]
fs = FileSystemStorage()
filename = fs.save(image_file.name, image_file)
image_url = fs.url(filename)
print(image_url)
return render(request, "upload.html", {
"image_url": image_url
})
return render(request, "upload.html")
Ajoutez un répertoire "templates" au répertoire "app/upload", puis ajoutez un nouveau modèle appelé upload.html :
{% block content %}
<form action="{% url "upload" %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="image_file">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
{% endblock %}
app/hello_django/urls.py :
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from upload.views import image_upload
urlpatterns = [
path("", image_upload, name="upload"),
path("admin/", admin.site.urls),
]
if bool(settings.DEBUG):
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
app/hello_django/settings.py :
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
Test:
$ docker-compose up -d --build
Vous devriez pouvoir télécharger une image sur http://localhost:8000/ , puis afficher l'image sur http://localhost:8000/media/IMAGE_FILE_NAME .
Pour la production, ajoutez un autre volume aux services web
etnginx
:
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
media_volume:
Créez le dossier "/home/app/web/mediafiles" dans Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME/mediafiles
WORKDIR $APP_HOME
...
Mettez à nouveau à jour la configuration Nginx :
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
location /media/ {
alias /home/app/web/mediafiles/;
}
}
Reconstruire:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Testez-le une dernière fois :
Si vous voyez une
413 Request Entity Too Large
erreur, vous devrez augmenter la taille maximale autorisée du corps de la requête client dans le contexte du serveur ou de l'emplacement dans la configuration Nginx.Exemple:
location / { proxy_pass http://hello_django; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; client_max_body_size 100M; }
Dans ce didacticiel, nous avons expliqué comment conteneuriser une application Web Django avec Postgres pour le développement. Nous avons également créé un fichier Docker Compose prêt pour la production qui ajoute Gunicorn et Nginx dans le mélange pour gérer les fichiers statiques et multimédias. Vous pouvez maintenant tester une configuration de production localement.
En termes de déploiement réel dans un environnement de production, vous souhaiterez probablement utiliser :
db
etnginx
Pour d'autres conseils de production, consultez cette discussion .
Vous pouvez trouver le code dans le référentiel django-on-docker .
Source : https://testdrive.io
1660683000
Este es un tutorial paso a paso que detalla cómo configurar Django para que se ejecute en Docker con Postgres. Para entornos de producción, agregaremos Nginx y Gunicorn. También veremos cómo servir archivos multimedia y estáticos de Django a través de Nginx.
Dependencias :
Cree un nuevo directorio de proyectos junto con un nuevo proyecto de Django:
$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.9 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver
Navegue a http://localhost:8000/ para ver la pantalla de bienvenida de Django. Mata al servidor una vez hecho. Luego, salga y elimine el entorno virtual. Ahora tenemos un proyecto Django simple con el que trabajar.
Cree un archivo requirements.txt en el directorio "aplicación" y agregue Django como dependencia:
Django==3.2.6
Since we'll be moving to Postgres, go ahead and remove the db.sqlite3 file from the "app" directory.
Your project directory should look like:
└── app
├── hello_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt
Install Docker, if you don't already have it, then add a Dockerfile to the "app" directory:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
So, we started with an Alpine-based Docker image for Python 3.9.6. We then set a working directory along with two environment variables:
PYTHONDONTWRITEBYTECODE
: Prevents Python from writing pyc files to disc (equivalent to python -B
option)PYTHONUNBUFFERED
: Prevents Python from buffering stdout and stderr (equivalent to python -u
option)Finalmente, actualizamos Pip, copiamos sobre el archivo requirements.txt , instalamos las dependencias y copiamos sobre el propio proyecto Django.
Revise Docker para desarrolladores de Python para obtener más información sobre la estructuración de Dockerfiles, así como algunas prácticas recomendadas para configurar Docker para el desarrollo basado en Python.
A continuación, agregue un archivo docker-compose.yml a la raíz del proyecto:
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
Revise la referencia del archivo Compose para obtener información sobre cómo funciona este archivo.
Actualice las variables SECRET_KEY
, DEBUG
y en settings.py :ALLOWED_HOSTS
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
Asegúrate de agregar la importación en la parte superior:
import os
Luego, cree un archivo .env.dev en la raíz del proyecto para almacenar las variables de entorno para el desarrollo:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
Construye la imagen:
$ docker-compose build
Una vez que se crea la imagen, ejecute el contenedor:
$ docker-compose up -d
Navegue a http://localhost:8000/ para volver a ver la pantalla de bienvenida.
Compruebe si hay errores en los registros si esto no funciona a través de
docker-compose logs -f
.
Para configurar Postgres, necesitaremos agregar un nuevo servicio al archivo docker-compose.yml , actualizar la configuración de Django e instalar Psycopg2 .
Primero, agregue un nuevo servicio llamado db
a docker-compose.yml :
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=hello_django
- POSTGRES_PASSWORD=hello_django
- POSTGRES_DB=hello_django_dev
volumes:
postgres_data:
Para persistir los datos más allá de la vida útil del contenedor, configuramos un volumen. Esta configuración se enlazará postgres_data
con el directorio "/var/lib/postgresql/data/" en el contenedor.
También agregamos una clave de entorno para definir un nombre para la base de datos predeterminada y establecer un nombre de usuario y una contraseña.
Consulte la sección "Variables de entorno" de la página de Postgres Docker Hub para obtener más información.
También necesitaremos algunas variables de entorno nuevas para el web
servicio, así que actualice .env.dev así:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
Actualice el DATABASES
dict en settings.py :
DATABASES = {
"default": {
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
"USER": os.environ.get("SQL_USER", "user"),
"PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
"HOST": os.environ.get("SQL_HOST", "localhost"),
"PORT": os.environ.get("SQL_PORT", "5432"),
}
}
Aquí, la base de datos se configura en función de las variables de entorno que acabamos de definir. Tome nota de los valores predeterminados.
Actualice Dockerfile para instalar los paquetes apropiados necesarios para Psycopg2:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
Agregue Psycopg2 a requisitos.txt :
Django==3.2.6
psycopg2-binary==2.9.1
Revise este problema de GitHub para obtener más información sobre la instalación de Psycopg2 en una imagen de Docker basada en Alpine.
Cree la nueva imagen y gire los dos contenedores:
$ docker-compose up -d --build
Ejecuta las migraciones:
$ docker-compose exec web python manage.py migrate --noinput
¿Obtienes el siguiente error?
django.db.utils.OperationalError: FATAL: database "hello_django_dev" does not exist
Run
docker-compose down -v
to remove the volumes along with the containers. Then, re-build the images, run the containers, and apply the migrations.
Ensure the default Django tables were created:
$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev
psql (13.0)
Type "help" for help.
hello_django_dev=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
------------------+--------------+----------+------------+------------+-------------------------------
hello_django_dev | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | hello_django | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
template1 | hello_django | UTF8 | en_US.utf8 | en_US.utf8 | =c/hello_django +
| | | | | hello_django=CTc/hello_django
(4 rows)
hello_django_dev=# \c hello_django_dev
You are now connected to database "hello_django_dev" as user "hello_django".
hello_django_dev=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+--------------
public | auth_group | table | hello_django
public | auth_group_permissions | table | hello_django
public | auth_permission | table | hello_django
public | auth_user | table | hello_django
public | auth_user_groups | table | hello_django
public | auth_user_user_permissions | table | hello_django
public | django_admin_log | table | hello_django
public | django_content_type | table | hello_django
public | django_migrations | table | hello_django
public | django_session | table | hello_django
(10 rows)
hello_django_dev=# \q
You can check that the volume was created as well by running:
$ docker volume inspect django-on-docker_postgres_data
You should see something similar to:
[
{
"CreatedAt": "2021-08-23T15:49:08Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-on-docker",
"com.docker.compose.version": "1.29.2",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/django-on-docker_postgres_data/_data",
"Name": "django-on-docker_postgres_data",
"Options": null,
"Scope": "local"
}
]
Next, add an entrypoint.sh file to the "app" directory to verify that Postgres is healthy before applying the migrations and running the Django development server:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
python manage.py flush --no-input
python manage.py migrate
exec "$@"
Update the file permissions locally:
$ chmod +x app/entrypoint.sh
Then, update the Dockerfile to copy over the entrypoint.sh file and run it as the Docker entrypoint command:
# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy entrypoint.sh
COPY ./entrypoint.sh .
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Agregue la DATABASE
variable de entorno a .env.dev :
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
Pruébalo de nuevo:
Primero, a pesar de agregar Postgres, aún podemos crear una imagen de Docker independiente para Django siempre que la DATABASE
variable de entorno no esté configurada en postgres
. Para probar, cree una nueva imagen y luego ejecute un nuevo contenedor:
$ docker build -f ./app/Dockerfile -t hello_django:latest ./app
$ docker run -d \
-p 8006:8000 \
-e "SECRET_KEY=please_change_me" -e "DEBUG=1" -e "DJANGO_ALLOWED_HOSTS=*" \
hello_django python /usr/src/app/manage.py runserver 0.0.0.0:8000
Debería poder ver la página de bienvenida en http://localhost:8006
En segundo lugar, es posible que desee comentar los comandos de vaciado y migración de la base de datos en el script entrypoint.sh para que no se ejecuten en cada inicio o reinicio del contenedor:
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
# python manage.py flush --no-input
# python manage.py migrate
exec "$@"
En su lugar, puede ejecutarlos manualmente, después de que los contenedores giren, así:
$ docker-compose exec web python manage.py flush --no-input
$ docker-compose exec web python manage.py migrate
Avanzando, para entornos de producción, agreguemos Gunicorn , un servidor WSGI de nivel de producción, al archivo de requisitos:
Django==3.2.6
gunicorn==20.1.0
psycopg2-binary==2.9.1
¿Tiene curiosidad acerca de WSGI y Gunicorn? Revise el capítulo WSGI del curso Creación de su propio marco web de Python .
Dado que todavía queremos usar el servidor integrado de Django en desarrollo, cree un nuevo archivo de composición llamado docker-compose.prod.yml para producción:
version: '3.8'
services:
web:
build: ./app
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
volumes:
postgres_data:
Si tiene varios entornos, es posible que desee utilizar un archivo de configuración docker-compose.override.yml . Con este enfoque, agregaría su configuración base a un archivo docker-compose.yml y luego usaría un archivo docker-compose.override.yml para anular esos ajustes de configuración según el entorno.
Tome nota del valor predeterminado command
. Estamos ejecutando Gunicorn en lugar del servidor de desarrollo Django. También eliminamos el volumen del web
servicio ya que no lo necesitamos en producción. Finalmente, estamos usando archivos de variables de entorno separados para definir variables de entorno para ambos servicios que se pasarán al contenedor en tiempo de ejecución.
.env.prod :
DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
.env.prod.db :
POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod
Agregue los dos archivos a la raíz del proyecto. Probablemente querrá mantenerlos fuera del control de versiones, así que agréguelos a un archivo .gitignore .
Baje los contenedores de desarrollo (y los volúmenes asociados con la bandera-v
):
$ docker-compose down -v
Luego, crea las imágenes de producción y haz girar los contenedores:
$ docker-compose -f docker-compose.prod.yml up -d --build
Verifique que la base de hello_django_prod
datos se haya creado junto con las tablas Django predeterminadas. Pruebe la página de administración en http://localhost:8000/admin . Los archivos estáticos ya no se cargan. Esto es de esperar ya que el modo de depuración está desactivado. Arreglaremos esto en breve.
Nuevamente, si el contenedor no se inicia, verifique si hay errores en los registros a través de
docker-compose -f docker-compose.prod.yml logs -f
.
¿Se dio cuenta de que todavía estamos ejecutando el vaciado de la base de datos (que borra la base de datos) y los comandos de migración cada vez que se ejecuta el contenedor? Esto está bien en desarrollo, pero vamos a crear un nuevo archivo de punto de entrada para producción.
punto de entrada.prod.sh :
#!/bin/sh
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
fi
exec "$@"
Actualice los permisos del archivo localmente:
$ chmod +x app/entrypoint.prod.sh
Para usar este archivo, cree un nuevo Dockerfile llamado Dockerfile.prod para usar con compilaciones de producción:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.9.6-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.2
COPY . .
RUN flake8 --ignore=E501,F401 .
# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.9.6-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh
RUN chmod +x $APP_HOME/entrypoint.prod.sh
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
Aquí, usamos una compilación de varias etapas de Docker para reducir el tamaño final de la imagen. Esencialmente, builder
es una imagen temporal que se usa para construir las ruedas de Python. Luego, las ruedas se copian en la imagen de producción final y la builder
imagen se descarta.
Podría llevar el enfoque de compilación de varias etapas un paso más allá y usar un solo Dockerfile en lugar de crear dos Dockerfiles. Piense en los pros y los contras de usar este enfoque en dos archivos diferentes.
¿Notaste que creamos un usuario no root? De forma predeterminada, Docker ejecuta procesos de contenedores como root dentro de un contenedor. Esta es una mala práctica, ya que los atacantes pueden obtener acceso de root al host de Docker si logran salir del contenedor. Si es root en el contenedor, será root en el host.
Actualice el web
servicio dentro del archivo docker-compose.prod.yml para compilar con Dockerfile.prod :
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
Pruébalo:
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
A continuación, agreguemos Nginx a la combinación para que actúe como un proxy inverso para que Gunicorn maneje las solicitudes de los clientes y sirva archivos estáticos.
Agregue el servicio a docker-compose.prod.yml :
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Luego, en la raíz del proyecto local, cree los siguientes archivos y carpetas:
└── nginx
├── Dockerfile
└── nginx.conf
archivo acoplable :
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf :
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Revise el uso de NGINX y NGINX Plus como una puerta de enlace de aplicaciones con uWSGI y Django para obtener más información sobre cómo configurar Nginx para que funcione con Django.
Luego, actualice el web
servicio, en docker-compose.prod.yml , reemplazándolo ports
con expose
:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
Ahora, el puerto 8000 solo está expuesto internamente a otros servicios de Docker. El puerto ya no se publicará en la máquina host.
Para obtener más información sobre puertos frente a exposición, revise esta pregunta de desbordamiento de pila.
Pruébelo de nuevo.
$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Asegúrese de que la aplicación esté funcionando en http://localhost:1337 .
La estructura de su proyecto ahora debería verse así:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── app
│ ├── Dockerfile
│ ├── Dockerfile.prod
│ ├── entrypoint.prod.sh
│ ├── entrypoint.sh
│ ├── hello_django
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf
Baje los contenedores una vez hecho:
$ docker-compose -f docker-compose.prod.yml down -v
Dado que Gunicorn es un servidor de aplicaciones, no entregará archivos estáticos. Entonces, ¿cómo deben manejarse los archivos estáticos y multimedia en esta configuración en particular?
Actualizar configuración.py :
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
Ahora, cualquier solicitud se http://localhost:8000/static/*
atenderá desde el directorio "staticfiles".
Para probar, primero reconstruya las imágenes y gire los nuevos contenedores como de costumbre. Asegúrese de que los archivos estáticos todavía se sirvan correctamente en http://localhost:8000/admin .
Para la producción, agregue un volumen a los servicios web
y nginx
en docker-compose.prod.yml para que cada contenedor comparta un directorio llamado "archivos estáticos":
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
También debemos crear la carpeta "/home/app/web/staticfiles" en Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME
...
¿Por qué es esto necesario?
Docker Compose normalmente monta volúmenes con nombre como root. Y dado que estamos usando un usuario que no es root, obtendremos un error de permiso denegado cuando collectstatic
se ejecute el comando si el directorio aún no existe.
Para evitar esto, puede:
Usamos el primero.
A continuación, actualice la configuración de Nginx para enrutar las solicitudes de archivos estáticos a la carpeta "staticfiles":
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
}
Haga girar los contenedores de desarrollo:
$ docker-compose down -v
Prueba:
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Nuevamente, las solicitudes se http://localhost:1337/static/*
atenderán desde el directorio "staticfiles".
Navegue a http://localhost:1337/admin y asegúrese de que los activos estáticos se carguen correctamente.
También puede verificar en los registros, a través docker-compose -f docker-compose.prod.yml logs -f
de, que las solicitudes a los archivos estáticos se entregan con éxito a través de Nginx:
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/ HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 2214 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/base.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/responsive.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/login.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/fonts.css HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/base.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
Traer los envases una vez hecho:
$ docker-compose -f docker-compose.prod.yml down -v
Para probar el manejo de archivos multimedia, comience creando una nueva aplicación Django:
$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload
Agregue la nueva aplicación a la INSTALLED_APPS
lista en settings.py :
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"upload",
]
app/upload/views.py :
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage
def image_upload(request):
if request.method == "POST" and request.FILES["image_file"]:
image_file = request.FILES["image_file"]
fs = FileSystemStorage()
filename = fs.save(image_file.name, image_file)
image_url = fs.url(filename)
print(image_url)
return render(request, "upload.html", {
"image_url": image_url
})
return render(request, "upload.html")
Agregue un directorio "templates" al directorio "app/upload" y luego agregue una nueva plantilla llamada upload.html :
{% block content %}
<form action="{% url "upload" %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="image_file">
<input type="submit" value="submit" />
</form>
{% if image_url %}
<p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
{% endif %}
{% endblock %}
app/hola_django/urls.py :
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from upload.views import image_upload
urlpatterns = [
path("", image_upload, name="upload"),
path("admin/", admin.site.urls),
]
if bool(settings.DEBUG):
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
aplicación/hola_django/configuraciones.py :
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
Prueba:
$ docker-compose up -d --build
Debería poder cargar una imagen en http://localhost:8000/ y luego ver la imagen en http://localhost:8000/media/IMAGE_FILE_NAME .
Para la producción, agregue otro volumen a los servicios web
y nginx
:
version: '3.8'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
expose:
- 8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
nginx:
build: ./nginx
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
ports:
- 1337:80
depends_on:
- web
volumes:
postgres_data:
static_volume:
media_volume:
Cree la carpeta "/home/app/web/mediafiles" en Dockerfile.prod :
...
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME/mediafiles
WORKDIR $APP_HOME
...
Actualice la configuración de Nginx nuevamente:
upstream hello_django {
server web:8000;
}
server {
listen 80;
location / {
proxy_pass http://hello_django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/staticfiles/;
}
location /media/ {
alias /home/app/web/mediafiles/;
}
}
Reconstruir:
$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Pruébalo una última vez:
Si ve un
413 Request Entity Too Large
error, deberá aumentar el tamaño máximo permitido del cuerpo de la solicitud del cliente en el contexto del servidor o de la ubicación dentro de la configuración de Nginx.Ejemplo:
location / { proxy_pass http://hello_django; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; client_max_body_size 100M; }
En este tutorial, explicamos cómo convertir en contenedor una aplicación web de Django con Postgres para el desarrollo. También creamos un archivo Docker Compose listo para producción que agrega Gunicorn y Nginx a la mezcla para manejar archivos estáticos y multimedia. Ahora puede probar una configuración de producción localmente.
En términos de implementación real en un entorno de producción, probablemente desee utilizar un:
db
ynginx
Para otros consejos de producción, revise esta discusión .
Puede encontrar el código en el repositorio de django-on-docker .
Fuente: https://testdriven.io