Desmond  Gerber

Desmond Gerber

1669001489

Dockerizing Flask with Postgres, Gunicorn, and Traefik

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.

Project Setup

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.

Docker

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:

  1. PYTHONDONTWRITEBYTECODE: Prevents Python from writing pyc files to disc (equivalent to python -B option)
  2. 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.

Postgres

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
  }
]

Gunicorn

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.

Production Dockerfile

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

Traefik

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:

  1. Reverse proxy and load balancer
  2. Automatically issues and renews SSL certificates, via Let's Encrypt, out-of-the-box
  3. Use Traefik for simple, Docker-based microservices

Nginx:

  1. Web server, reverse proxy, and load balancer
  2. Slightly faster than Traefik
  3. 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:

  1. traefik.enable=true enables Traefik to discover the service
  2. traefik.http.routers.flask.rule=Host(`flask.localhost`) when the request has Host=flask.localhost, the request is redirected to this service

Take note of the volumes within the traefik service:

  1. ./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
  2. /var/run/docker.sock:/var/run/docker.sock:ro enables Traefik to discover other containers

To 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:

traefik dashboard

Bring the containers and volumes down once done:

$ docker-compose down -v

Let's Encrypt

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:

  1. entryPoints.web sets the entry point for our insecure HTTP application to port 80
  2. entryPoints.websecure sets the entry point for our secure HTTPS application to port 443
  3. entryPoints.web.http.redirections.entryPoint redirects all insecure requests to the secure port
  4. exposedByDefault = false unexposes all services
  5. dashboard = true enables the monitoring dashboard

Finally, 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:

  1. flask-traefik.your-domain.com - for the web service
  2. dashboard-flask-traefik.your-domain.com - for the Traefik dashboard

Make 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:

  1. traefik.http.routers.flask.rule=Host(`flask-traefik.your-domain.com`) changes the host to the actual domain
  2. traefik.http.routers.flask.tls=true enables HTTPS
  3. traefik.http.routers.flask.tls.certresolver=letsencrypt sets the certificate issuer as Let's Encrypt

Next, 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:

  1. 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/
  2. traefik.http.routers.dashboard.tls=true enables HTTPS
  3. traefik.http.routers.dashboard.tls.certresolver=letsencrypt sets the certificate resolver to Let's Encrypt
  4. traefik.http.routers.dashboard.middlewares=auth enables HTTP BasicAuth middleware
  5. traefik.http.middlewares.auth.basicauth.users defines the username and hashed password for logging in

You 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:

  1. https://flask-traefik.your-domain.com
  2. https://dashboard-flask-traefik.your-domain.com/dashboard/

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!

Conclusion

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:

  1. Fully-managed database service -- like RDS or Cloud SQL -- rather than managing your own Postgres instance within a container.
  2. Non-root user for the services

You can find the code in the flask-docker-traefik repo.

Original article source at: https://testdriven.io/

#flask #postgres #gunicorn 

Dockerizing Flask with Postgres, Gunicorn, and Traefik
Eric  Bukenya

Eric Bukenya

1662632280

6 Python Servers Libraries Useful for Developers

In this Python article, let's learn about Servers: 6 Python Servers Libraries Useful for Developers

  1. Gunicorn: gunicorn 'Green Unicorn' is a WSGI HTTP Server for UNIX, fast clients and sleepy applications.
  2. gevent: Coroutine-based concurrency library for Python
  3. Uvicorn: An ASGI web server, for Python. 🦄
  4. WhiteNoise: Radically simplified static file serving for Python web apps
  5. asgiref: ASGI specification and utilities
  6. LiveReload: livereload server in python (MAINTAINERS NEEDED)
     

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.

6 Python Servers Libraries Useful for Developers

1.Gunicorn

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.

Documentation

The documentation is hosted at https://docs.gunicorn.org.

Installation

Gunicorn requires Python 3.x >= 3.5.

Install from PyPI:

$ pip install gunicorn

Usage

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

View on GitHub

2.    gevent

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).

View on GitHub

3.   uvicorn

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.

Quickstart

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:

  • the event loop uvloop will be installed and used if possible.
  • the http protocol will be handled by httptools if possible.

Moreover, "optional extras" means that:

  • the websocket protocol will be handled by websockets (should you want to use wsproto you'd need to install it manually) if possible.
  • the --reload flag in development mode will use watchfiles.
  • windows users will have 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

View on GitHub

4.    WhiteNoise

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:

  • Serving compressed content (gzip and Brotli formats, handling Accept-Encoding and Vary headers correctly)
  • Setting far-future cache headers on content which won't change

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.

View on GitHub

5.     asgiref

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:

  • Sync-to-async and async-to-sync function wrappers, asgiref.sync
  • Server base classes, asgiref.server
  • A WSGI-to-ASGI adapter, in asgiref.wsgi

Function wrappers

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:

  • AsyncToSync lets a synchronous subthread stop and wait while the async function is called on the main thread's event loop, and then control is returned to the thread when the async function is finished.
  • SyncToAsync lets async code call a synchronous function, which is run in a threadpool and control returned to the async coroutine when the synchronous function completes.

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.

View on GitHub

6.     LiveReload

Reload webpages on changes, without hitting refresh in your browser.

Installation

LiveReload is for web developers who know Python. It is available on PyPI.

$ pip install livereload

View on GitHub


Related videos:

How To Create a Simple Web Server Using Python and the http.server Module

Related posts:

#python  #Servers 

6 Python Servers Libraries Useful for Developers
Isai  Upton

Isai Upton

1661119860

How to Configure Masonite To Run on Docker with Postgres

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

#docker #postgres #gunicorn #nginx 

How to Configure Masonite To Run on Docker with Postgres
Hoang Tran

Hoang Tran

1661112600

Cách Cấu Hình Masonite Để Chạy Trên Docker Với Postgres

Đâ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 :

  1. Masonite v4.16.2
  2. Docker v20.10.17
  3. Python v3.10.5

Thiết lập dự án

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:

  1. Xóa các tệp .env-example.gitignore khỏi thư mục "web"
  2. Di chuyển tệp .env tới thư mục gốc của dự án và đổi tên thành .env.dev .

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

Docker

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:

  1. PYTHONDONTWRITEBYTECODE: Ngăn Python ghi tệp pyc vào đĩa (tương đương với python -B tùy chọn )
  2. PYTHONUNBUFFERED: Ngăn Python lưu vào bộ đệm stdout và stderr (tương đương với python -u tùy chọn )
  3. 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-devvà các cargophụ 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.

Postgres

Để đị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à dbdocker -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_devvớ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 usersbả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:

  1. Xây dựng lại hình ảnh
  2. Chạy các vùng chứa
  3. Hãy thử http: // localhost: 8000 /

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

Gunicorn

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 webdị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 -vcờ):

$ 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_prodcơ sở dữ liệu đã được tạo cùng với usersbả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.

Dockerfile sản xuất

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, builderlà 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à builderhì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 webdị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

Nginx

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 webdịch vụ, trong docker-compos.prod.yml , thay thế portsbằ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?

Tệp tĩnh

Đầu tiên, hãy cập nhật STATICFILEScấ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!

Sự phát triển

Để 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:

  1. http: // localhost: 8000 / robots.txt ( nội dung tĩnh gốc )
  2. http: // localhost: 8000 / static / hello.txt (tài sản tĩnh thông thường)
  3. http: // localhost: 8000 / static / css / app.css (tài sản tĩnh đã biên dịch)

Sản xuất

Đối với phiên bản sản xuất, hãy thêm một khối lượng vào webnginxcá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 URLThư 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:

  1. http: // localhost: 1337 / robots.txt
  2. http: // localhost: 1337 / static / hello.txt
  3. http: // localhost: 1337 / static / css / app.css

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

Tệp phương tiện

Để 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 WelcomeControllertrong 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"),
]

Sự phát triển

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 .

Sản xuất

Đố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:

  1. Tải lên hình ảnh tại http: // localhost: 1337 .
  2. Sau đó, xem hình ảnh tại http: // localhost: 1337 / uploads / IMAGE_FILE_NAME .

Sự kết luận

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:

  1. Dịch vụ cơ sở dữ liệu được quản lý hoàn toàn - như RDS hoặc Cloud SQL - thay vì quản lý phiên bản Postgres của riêng bạn trong một vùng chứa.
  2. Người dùng không phải root cho dbnginxcá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

#docker #postgres #gunicorn #nginx 

Cách Cấu Hình Masonite Để Chạy Trên Docker Với Postgres

Как настроить Masonite для работы в Docker с помощью Postgres

Это пошаговое руководство, в котором подробно описано, как настроить Masonite, веб-фреймворк на основе Python, для работы в Docker с Postgres. Для производственных сред мы добавим Nginx и Gunicorn. Мы также рассмотрим, как обслуживать статические и загруженные пользователем медиафайлы через Nginx.

Зависимости :

  1. Мазонит v4.16.2
  2. Докер v20.10.17
  3. Питон v3.10.5

Настройка проекта

Создайте каталог проекта, установите 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, давайте немного подчистим структуру проекта:

  1. Удалите файлы .env-example и .gitignore из каталога «web».
  2. Переместите файл .env в корень проекта и переименуйте его в .env.dev .

Теперь структура вашего проекта должна выглядеть так:

├── .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. Затем мы устанавливаем рабочий каталог вместе с тремя переменными среды:

  1. PYTHONDONTWRITEBYTECODE: запрещает Python записывать файлы pyc на диск (эквивалентно python -B опции )
  2. PYTHONUNBUFFERED: запрещает Python буферизовать stdout и stderr (эквивалентно python -u опции )
  3. 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

Проверьте еще раз:

  1. Восстановить изображения
  2. Запуск контейнеров
  3. Попробуйте http://локальный:8000/

Хотите посеять несколько пользователей?

$ 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.

Рабочий Dockerfile

Вы заметили, что мы по-прежнему выполняем команды 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

Затем давайте добавим 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!

Разработка

Для проверки сначала пересоберите образы и запустите новые контейнеры, как обычно. После этого убедитесь, что следующие статические ресурсы загружаются правильно:

  1. http://localhost:8000/robots.txt ( корневой статический актив)
  2. http://localhost:8000/static/hello.txt (обычный статический ресурс)
  3. http://localhost:8000/static/css/app.css (скомпилированный статический ресурс)

Производство

Для производства добавьте том к службам weband в файле 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

Опять же, убедитесь, что следующие статические ресурсы загружены правильно:

  1. http://локальный:1337/robots.txt
  2. http://локальный:1337/static/hello.txt
  3. http://локальный:1337/статический/css/app.css

Вы также можете проверить в журналах — через 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 %}

Добавьте новый метод, вызываемый в uploadweb /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

Проверьте это в последний раз:

  1. Загрузите изображение по адресу http://localhost:1337 .
  2. Затем просмотрите изображение по адресу http://localhost:1337/uploads/IMAGE_FILE_NAME .

Вывод

В этом руководстве мы рассмотрели, как контейнеризировать приложение Masonite с помощью Postgres для разработки. Мы также создали готовый к работе файл Docker Compose, который добавляет в смесь Gunicorn и Nginx для обработки статических и мультимедийных файлов. Теперь вы можете протестировать производственную установку локально.

С точки зрения фактического развертывания в производственной среде вы, вероятно, захотите использовать:

  1. Полностью управляемая служба базы данных, такая как RDS или Cloud SQL , вместо управления собственным экземпляром Postgres в контейнере.
  2. Пользователь без полномочий root для служб dbиnginx

Вы можете найти код в репозитории masonite-on-docker .

Спасибо за чтение!

Источник:  https://testdriven.io

#docker #postgres #gunicorn #nginx 

Как настроить Masonite для работы в Docker с помощью Postgres
小泉  晃

小泉 晃

1661097780

如何使用 Postgres 配置 Masonite 在 Docker 上運行

這是一個分步教程,詳細介紹瞭如何配置 Masonite(一個基於 Python 的 Web 框架)以使用 Postgres 在 Docker 上運行。對於生產環境,我們將添加 Nginx 和 Gunicorn。我們還將了解如何通過 Nginx 提供靜態和用戶上傳的媒體文件。

依賴項

  1. 馬索尼特 v4.16.2
  2. 碼頭工人 v20.10.17
  3. Python v3.10.5

項目設置

創建項目目錄,安裝 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 之前,讓我們稍微清理一下項目結構:

  1. 從“web”目錄中刪除.env-example.gitignore文件
  2. .env文件移動到項目根目錄並將其重命名為.env.dev

您的項目結構現在應該如下所示:

├── .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 映像開始。然後我們設置一個工作目錄以及三個環境變量:

  1. PYTHONDONTWRITEBYTECODE: 防止 Python 將 pyc 文件寫入磁盤(相當於python -B option
  2. PYTHONUNBUFFERED: 防止 Python 緩衝 stdout 和 stderr (相當於python -u option )
  3. TZ=UTC將容器中的時區設置為 UTC,這是日誌記錄所必需的

接下來,我們安裝了 Python 所需的一些系統級依賴項。注意openssl-devcargo依賴項。這些是必需的,因為密碼現在依賴於 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

要配置 Postgres,我們需要在docker-compose.yml文件中添加一個新服務,更新環境變量,然後安裝Psycopg2

首先,添加一個名為dbdocker -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

再次測試一下:

  1. 重新構建圖像
  2. 運行容器
  3. 試試http://localhost:8000/

想要播種一些用戶?

$ 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

生產 Dockerfile

您是否注意到每次容器運行時我們仍在運行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

接下來,讓我們將 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!

發展

要進行測試,首先要重新構建映像並按照慣例啟動新容器。完成後,確保正確加載以下靜態資產:

  1. http://localhost:8000/robots.txt靜態資產)
  2. http://localhost:8000/static/hello.txt(常規靜態資源)
  3. http://localhost:8000/static/css/app.css(編譯後的靜態資源)

生產

對於生產,向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

同樣,確保正確加載以下靜態資產:

  1. http://localhost:1337/robots.txt
  2. http://localhost:1337/static/hello.txt
  3. http://localhost:1337/static/css/app.css

您還可以在日誌中驗證——通過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

最後一次測試一下:

  1. 在http://localhost:1337上傳圖片。
  2. 然後,在http://localhost:1337/uploads/IMAGE_FILE_NAME查看圖像。

結論

在本教程中,我們介紹瞭如何使用 Postgres 將 Masonite 應用程序容器化以進行開發。我們還創建了一個生產就緒的 Docker Compose 文件,將 Gunicorn 和 Nginx 添加到混合中以處理靜態和媒體文件。您現在可以在本地測試生產設置。

就實際部署到生產環境而言,您可能希望使用:

  1. 完全託管的數據庫服務(如RDSCloud SQL),而不是在容器中管理您自己的 Postgres 實例。
  2. dbnginx服務的非 root 用戶

您可以在masonite-on-docker 存儲庫中找到代碼。

謝謝閱讀!

來源:  https ://testdriven.io

#docker #postgre #gunicorn #nginx 

如何使用 Postgres 配置 Masonite 在 Docker 上運行

Configurer Masonite Pour Qu'il S'exécute Sur Docker Avec Postgres

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 :

  1. Masonite v4.16.2
  2. Docker v20.10.17
  3. Python v3.10.5

Configuration du projet

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 :

  1. Supprimez les fichiers .env-example et .gitignore du répertoire "web"
  2. Déplacez le fichier .env à la racine du projet et renommez-le en .env.dev .

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

Docker

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 :

  1. PYTHONDONTWRITEBYTECODE: Empêche Python d'écrire des fichiers pyc sur le disque (équivalent à l' python -B option )
  2. PYTHONUNBUFFERED: Empêche Python de mettre en mémoire tampon stdout et stderr (équivalent à python -u option )
  3. TZ=UTCdéfinit le fuseau horaire dans le conteneur sur UTC, ce qui est requis pour la journalisation

Ensuite, nous avons installé certaines dépendances au niveau du système requises pour Python. Prenez note des dépendances openssl-devet . cargoCelles-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.

postgres

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_devau 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 userstable 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 :

  1. Reconstruire les images
  2. Exécutez les conteneurs
  3. Essayez http://localhost:8000/

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

Gunicorne

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 webservice 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 -vdrapeau) :

$ 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_proddonnées a été créée avec la userstable. 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.

Fichier Docker de production

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 builderd'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' builderimage 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 webservice 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

Nginx

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 webservice, dans docker-compose.prod.yml , en remplaçant portspar 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 ?

Fichiers statiques

Tout d'abord, mettez à jour la STATICFILESconfiguration 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!

Développement

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 :

  1. http://localhost:8000/robots.txt ( ressource statique racine )
  2. http://localhost:8000/static/hello.txt (ressource statique régulière)
  3. http://localhost:8000/static/css/app.css (actif statique compilé)

Production

Pour la production, ajoutez un volume aux services webet nginxdans 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 demandeDossier
/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 :

  1. http://localhost:1337/robots.txt
  2. http://localhost:1337/static/hello.txt
  3. http://localhost:1337/static/css/app.css

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

Fichiers multimédias

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à WelcomeControllerdans 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"),
]

Développement

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 .

Production

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 :

  1. Téléchargez une image sur http://localhost:1337 .
  2. Ensuite, affichez l'image sur http://localhost:1337/uploads/IMAGE_FILE_NAME .

Conclusion

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 :

  1. Service de base de données entièrement géré, comme RDS ou Cloud SQL , plutôt que de gérer votre propre instance Postgres dans un conteneur.
  2. Utilisateur non root pour les services dbetnginx

Vous pouvez trouver le code dans le référentiel masonite-on-docker .

Merci d'avoir lu!

Source :  https://testdrive.io

#docker #postgres #gunicorn #nginx 

Configurer Masonite Pour Qu'il S'exécute Sur Docker Avec Postgres
Derrick  Ferry

Derrick Ferry

1661082600

Cómo Configurar Masonite Para Que Se Ejecute En Docker Con Postgres

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 :

  1. Masonite v4.16.2
  2. Ventana acoplable v20.10.17
  3. Pitón v3.10.5

Configuración del proyecto

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:

  1. Elimine los archivos .env-example y .gitignore del directorio "web"
  2. Mueva el archivo .env a la raíz del proyecto y cámbiele el nombre a .env.dev .

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

Estibador

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:

  1. PYTHONDONTWRITEBYTECODE: Evita que Python escriba archivos pyc en el disco (equivalente a la python -B opción )
  2. PYTHONUNBUFFERED: evita que Python almacene en búfer stdout y stderr (equivalente a la python -u opción )
  3. TZ=UTCestablece la zona horaria en el contenedor en UTC, que es necesaria para iniciar sesión

A continuación, instalamos algunas dependencias de nivel de sistema necesarias para Python. Tome nota de las dependencias openssl-devy . cargoEstos 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 -fy 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.

postgres

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 dba 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_devcon 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 usersde 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:

  1. Reconstruir las imágenes
  2. Ejecutar los contenedores
  3. Pruebe http://localhost:8000/

¿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

gunicornio

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 webservicio 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_proddatos se haya creado junto con la userstabla. 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.

Dockerfile de producción

¿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, builderes 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 builderimagen 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 webservicio 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

Nginx

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 webservicio, en docker-compose.prod.yml , reemplazándolo portscon 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?

Archivos estáticos

Primero, actualice la STATICFILESconfiguració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!

Desarrollo

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:

  1. http://localhost:8000/robots.txt ( activo estático raíz )
  2. http://localhost:8000/static/hello.txt (recurso estático normal)
  3. http://localhost:8000/static/css/app.css (recurso estático compilado)

Producción

Para la producción, agregue un volumen a los servicios weby nginxen 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 solicitudCarpeta
/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:

  1. http://localhost:1337/robots.txt
  2. http://localhost:1337/static/hola.txt
  3. http://localhost:1337/static/css/app.css

También puede verificar en los registros, a través docker-compose -f docker-compose.prod.yml logs -fde, 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

Archivos multimedia

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 uploaden web/ WelcomeControllerapp /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"),
]

Desarrollo

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 .

Producción

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:

  1. Cargue una imagen en http://localhost:1337 .
  2. Luego, vea la imagen en http://localhost:1337/uploads/IMAGE_FILE_NAME .

Conclusión

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:

  1. Servicio de base de datos completamente administrado, como RDS o Cloud SQL , en lugar de administrar su propia instancia de Postgres dentro de un contenedor.
  2. Usuario no root para los servicios dbynginx

Puede encontrar el código en el repositorio de masonite-on-docker .

¡Gracias por leer!

Fuente:  https://testdriven.io

#docker #postgres #gunicorn #nginx 

Cómo Configurar Masonite Para Que Se Ejecute En Docker Con Postgres
Callum  Allen

Callum Allen

1661074740

Como Configurar O Masonite Para Executar No Docker Com Postgres

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 :

  1. Masonita v4.16.2
  2. Docker v20.10.17
  3. Python v3.10.5

Configuração do projeto

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:

  1. Remova os arquivos .env-example e .gitignore do diretório "web"
  2. Mova o arquivo .env para a raiz do projeto e renomeie-o para .env.dev .

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

Janela de encaixe

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:

  1. PYTHONDONTWRITEBYTECODE: Impede que o Python grave arquivos pyc no disco (equivalente à python -B opção )
  2. PYTHONUNBUFFERED: Impede o Python de armazenar em buffer stdout e stderr (equivalente a python -u option )
  3. TZ=UTCdefine o fuso horário no contêiner como UTC, que é necessário para o registro

Em seguida, instalamos algumas dependências de nível de sistema necessárias para Python. Anote as dependências openssl-deve . cargoEles 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.

Postgres

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_devao 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 userstabela 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:

  1. Reconstrua as imagens
  2. Execute os recipientes
  3. Tente http://localhost:8000/

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

Gunicorn

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 webserviç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 -vsinalizador):

$ 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_prodbanco de dados foi criado junto com a userstabela. 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.

Dockerfile de produção

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 builderimagem é 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 webserviç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

Nginx

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 webserviço, em docker-compose.prod.yml , substituindo portspor 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?

Arquivos estáticos

Primeiro, atualize a STATICFILESconfiguraçã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!

Desenvolvimento

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:

  1. http://localhost:8000/robots.txt ( ativo estático raiz )
  2. http://localhost:8000/static/hello.txt (ativo estático regular)
  3. http://localhost:8000/static/css/app.css (ativo estático compilado)

Produção

Para produção, adicione um volume ao weband nginxservices 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çãoPasta
/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:

  1. http://localhost:1337/robots.txt
  2. http://localhost:1337/static/hello.txt
  3. http://localhost:1337/static/css/app.css

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

Arquivos de mídia

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 uploadem 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"),
]

Desenvolvimento

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 .

Produção

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:

  1. Carregue uma imagem em http://localhost:1337 .
  2. Em seguida, visualize a imagem em http://localhost:1337/uploads/IMAGE_FILE_NAME .

Conclusão

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:

  1. Serviço de banco de dados totalmente gerenciado, como RDS ou Cloud SQL , em vez de gerenciar sua própria instância do Postgres em um contêiner.
  2. Usuário não root para os serviços dbenginx

Você pode encontrar o código no repositório masonite-on-docker .

Obrigado por ler!

Fonte:  https://testdrive.io

#docker #postgres #gunicorn #nginx 

Como Configurar O Masonite Para Executar No Docker Com Postgres
Emmy  Monahan

Emmy Monahan

1660719420

How to Configure Django To Run on Docker with Postgres

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

#django #docker #nginx #gunicorn #postgres 

How to Configure Django To Run on Docker with Postgres
Thai  Son

Thai Son

1660712160

Cách Định Cấu Hình Django Để Chạy Trên Docker Với Postgres

Đâ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 :

  1. Django v3.2.6
  2. Docker v20.10.8
  3. Python v3.9.6

Thiết lập dự án

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

Docker

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:

  1. PYTHONDONTWRITEBYTECODE: Ngăn Python ghi tệp pyc vào đĩa (tương đương với python -B tùy chọn )
  2. 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_KEYDEBUGALLOWED_HOSTScá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.

Postgres

Để đị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à dbdocker -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_datavớ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 webdị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ả DATABASEStrong 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 DATABASEbiế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:

  1. Xây dựng lại hình ảnh
  2. Chạy các vùng chứa
  3. Hãy thử http: // localhost: 8000 /

Ghi chú

Đầ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à DATABASEbiế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

Gunicorn

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 -vcờ):

$ 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_prodcơ 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.

Dockerfile sản xuất

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, builderlà 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à builderhì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 webdị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

Nginx

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 webdịch vụ, trong docker-compos.prod.yml , thay thế portsbằ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?

Tệp tĩnh

Cập nhật settings.py :

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

Sự phát triển

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 .

Sản xuất

Đối với phiên bản sản xuất, hãy thêm một khối lượng vào webnginxcá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 collectstaticlệ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ể:

  1. Tạo thư mục trong Dockerfile ( nguồn )
  2. Thay đổi quyền của thư mục sau khi nó được gắn kết ( nguồn )

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

Tệp phương tiện

Để 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_APPSdanh 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"

Sự phát triển

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

Để sản xuất, hãy thêm một khối lượng khác vào webnginxcá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:

  1. Tải lên hình ảnh tại http: // localhost: 1337 / .
  2. Sau đó, xem hình ảnh tại http: // localhost: 1337 / media / IMAGE_FILE_NAME .

Nếu gặp 413 Request Entity Too Largelỗ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;
}

Sự kết luận

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:

  1. Dịch vụ cơ sở dữ liệu được quản lý hoàn toàn - như RDS hoặc Cloud SQL - thay vì quản lý phiên bản Postgres của riêng bạn trong một vùng chứa.
  2. Người dùng không phải root cho dbnginxcá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

#django #docker #nginx #gunicorn #postgres 

Cách Định Cấu Hình Django Để Chạy Trên Docker Với Postgres

Как настроить Django для работы в Docker с Postgres

Это пошаговое руководство, в котором подробно описано, как настроить Django для работы в Docker с Postgres. Для производственных сред мы добавим Nginx и Gunicorn. Мы также рассмотрим, как обслуживать статические и мультимедийные файлы Django через Nginx.

Зависимости :

  1. Джанго v3.2.6
  2. Докер v20.10.8
  3. Питон v3.9.6

Настройка проекта

Создайте новый каталог проекта вместе с новым проектом 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. Затем мы устанавливаем рабочий каталог вместе с двумя переменными среды:

  1. PYTHONDONTWRITEBYTECODE: запрещает Python записывать файлы pyc на диск (эквивалентно python -B опции )
  2. 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

Обновите DATABASESdict в 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

Проверьте еще раз:

  1. Восстановить изображения
  2. Запуск контейнеров
  3. Попробуйте http://локальный:8000/

Заметки

Во-первых, несмотря на добавление 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.

Рабочий Dockerfile

Вы заметили, что мы по-прежнему выполняем очистку базы данных ( которая очищает базу данных) и команды переноса при каждом запуске контейнера? Это нормально в разработке, но давайте создадим новый файл точки входа для производства.

точка входа.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

Затем давайте добавим 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 .

Производство

Для рабочей среды добавьте том в службы weband в файле 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запуске команды, если каталог еще не существует.

Чтобы обойти это, вы можете:

  1. Создайте папку в Dockerfile ( источник )
  2. Изменить права доступа к каталогу после его монтирования ( источник )

Мы использовали первое.

Затем обновите конфигурацию 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 .

Производство

Для производства добавьте еще один том в weband nginxservices:

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

Проверьте это в последний раз:

  1. Загрузите изображение по адресу http://localhost:1337/ .
  2. Затем просмотрите изображение по адресу http://localhost:1337/media/IMAGE_FILE_NAME .

Если вы видите 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 для обработки статических и мультимедийных файлов. Теперь вы можете протестировать производственную установку локально.

С точки зрения фактического развертывания в производственной среде вы, вероятно, захотите использовать:

  1. Полностью управляемая служба базы данных, такая как RDS или Cloud SQL , вместо управления собственным экземпляром Postgres в контейнере.
  2. Пользователь без полномочий root для служб dbиnginx

Чтобы узнать о других советах по производству, ознакомьтесь с этим обсуждением .

Вы можете найти код в репозитории django-on-docker .

Источник:  https://testdriven.io

#django #docker #nginx #gunicorn #postgres 

Как настроить Django для работы в Docker с Postgres

如何使用 Postgres 配置 Django 在 Docker 上運行

這是一個分步教程,詳細介紹瞭如何配置 Django 以使用 Postgres 在 Docker 上運行。對於生產環境,我們將添加 Nginx 和 Gunicorn。我們還將了解如何通過 Nginx 提供 Django 靜態和媒體文件。

依賴項

  1. Django v3.2.6
  2. 碼頭工人 v20.10.8
  3. Python v3.9.6

項目設置

創建一個新的項目目錄以及一個新的 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 映像開始。然後我們設置一個工作目錄以及兩個環境變量:

  1. PYTHONDONTWRITEBYTECODE: 防止 Python 將 pyc 文件寫入磁盤(相當於python -B option
  2. 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.

Postgres

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

再次測試一下:

  1. 重新構建圖像
  2. 運行容器
  3. 試試http://localhost:8000/

筆記

首先,儘管添加了 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

生產 Dockerfile

您是否注意到我們仍在運行數據庫刷新(清除數據庫)並在每次運行容器時遷移命令?這在開發中很好,但是讓我們為生產創建一個新的入口點文件。

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

接下來,讓我們將 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如果目錄不存在,我們將在運行命令時收到權限被拒絕錯誤

要解決此問題,您可以:

  1. 在 Dockerfile ( source )中創建文件夾
  2. 掛載後更改目錄的權限(source

我們使用了前者。

接下來,更新 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中查看圖像。

生產

對於生產,向webnginx服務添加另一個卷:

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

最後一次測試一下:

  1. 在http://localhost:1337/上傳圖片。
  2. 然後,在http://localhost:1337/media/IMAGE_FILE_NAME查看圖像。

如果您看到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 添加到混合中以處理靜態和媒體文件。您現在可以在本地測試生產設置。

就實際部署到生產環境而言,您可能希望使用:

  1. 完全託管的數據庫服務(如RDSCloud SQL),而不是在容器中管理您自己的 Postgres 實例。
  2. dbnginx服務的非 root 用戶

有關其他生產技巧,請查看此討論

您可以在django-on-docker repo 中找到代碼。

來源:  https ://testdriven.io

#django #docker #nginx #gunicorn #postgres 

如何使用 Postgres 配置 Django 在 Docker 上運行
Eladio  Rau

Eladio Rau

1660690320

Comment Configurer Django Pour Qu'il S'exécute Sur Docker Avec Postgre

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 :

  1. Django v3.2.6
  2. Docker v20.10.8
  3. Python v3.9.6

Configuration du projet

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

Docker

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 :

  1. PYTHONDONTWRITEBYTECODE: Empêche Python d'écrire des fichiers pyc sur le disque (équivalent à l' python -B option )
  2. 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, DEBUGet 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.

postgres

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_dataau 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 webservice, 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 DATABASESdict 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 -vpour 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 DATABASEvariable 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 :

  1. Reconstruire les images
  2. Exécutez les conteneurs
  3. Essayez http://localhost:8000/

Remarques

Tout d'abord, malgré l'ajout de Postgres, nous pouvons toujours créer une image Docker indépendante pour Django tant que la DATABASEvariable 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

Gunicorne

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 webservice 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 -vdrapeau) :

$ 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_proddonné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.

Fichier Docker de production

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 builderd'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' builderimage 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 webservice 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

Nginx

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 webservice, dans docker-compose.prod.yml , en remplaçant portspar 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 ?

Fichiers statiques

Mettre à jour settings.py :

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

Développement

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 .

Production

Pour la production, ajoutez un volume aux services webet nginxdans 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' collectstaticexécution de la commande si le répertoire n'existe pas déjà

Pour contourner cela, vous pouvez soit :

  1. Créez le dossier dans le Dockerfile ( source )
  2. Modifiez les autorisations du répertoire après son montage ( source )

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

Fichiers multimédias

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_APPSliste 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"

Développement

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 .

Production

Pour la production, ajoutez un autre volume aux services webetnginx :

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 :

  1. Téléchargez une image sur http://localhost:1337/ .
  2. Ensuite, affichez l'image sur http://localhost:1337/media/IMAGE_FILE_NAME .

Si vous voyez une 413 Request Entity Too Largeerreur, 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;
}

Conclusion

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 :

  1. Service de base de données entièrement géré, comme RDS ou Cloud SQL , plutôt que de gérer votre propre instance Postgres dans un conteneur.
  2. Utilisateur non root pour les services dbetnginx

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

#django #docker #nginx #gunicorn #postgres 

Comment Configurer Django Pour Qu'il S'exécute Sur Docker Avec Postgre
Lilly  Wilson

Lilly Wilson

1660683000

Cómo Configurar Django Para Que Se Ejecute En Docker Con Postgres

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 :

  1. Django v3.2.6
  2. Docker v20.10.8
  3. Pitón v3.9.6

Configuración del proyecto

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

Docker

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:

  1. PYTHONDONTWRITEBYTECODE: Prevents Python from writing pyc files to disc (equivalent to python -B option)
  2. 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, DEBUGy 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.

postgres

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 dba 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_datacon 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 webservicio, 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 DATABASESdict 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 DATABASEvariable 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:

  1. Reconstruir las imágenes
  2. Ejecutar los contenedores
  3. Pruebe http://localhost:8000/

notas

Primero, a pesar de agregar Postgres, aún podemos crear una imagen de Docker independiente para Django siempre que la DATABASEvariable 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

gunicornio

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 webservicio 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_proddatos 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.

Dockerfile de producción

¿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, builderes 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 builderimagen 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 webservicio 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

Nginx

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 webservicio, en docker-compose.prod.yml , reemplazándolo portscon 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?

Archivos estáticos

Actualizar configuración.py :

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

Desarrollo

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 .

Producción

Para la producción, agregue un volumen a los servicios weby nginxen 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 collectstaticse ejecute el comando si el directorio aún no existe.

Para evitar esto, puede:

  1. Cree la carpeta en el Dockerfile ( fuente )
  2. Cambie los permisos del directorio después de que esté montado ( fuente )

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 -fde, 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

Archivos multimedia

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_APPSlista 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"

Desarrollo

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 .

Producción

Para la producción, agregue otro volumen a los servicios weby 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:

  1. Cargue una imagen en http://localhost:1337/ .
  2. Luego, vea la imagen en http://localhost:1337/media/IMAGE_FILE_NAME .

Si ve un 413 Request Entity Too Largeerror, 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;
}

Conclusión

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:

  1. Servicio de base de datos completamente administrado, como RDS o Cloud SQL , en lugar de administrar su propia instancia de Postgres dentro de un contenedor.
  2. Usuario no root para los servicios dbynginx

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

#django #docker #nginx #gunicorn #postgres 

Cómo Configurar Django Para Que Se Ejecute En Docker Con Postgres