大家好,又见面了,我是你们的朋友全栈君。
The author selected United Nations Foundation to receive a donation as part of the Write for DOnations program.
作者选择联合国基金会作为Write for DOnations计划的一部分接受捐赠。
The original WordPress version of this tutorial was written by Kathleen Juell.
本教程的原始WordPress版本由Kathleen Juell编写 。
Drupal is a content management system (CMS) written in PHP and distributed under the open-source GNU General Public License. People and organizations around the world use Drupal to power government sites, personal blogs, businesses, and more. What makes Drupal unique from other CMS frameworks is its growing community and a set of features that include secure processes, reliable performance, modularity, and flexibility to adapt.
Drupal是用PHP编写的内容管理系统(CMS),并根据开源GNU General Public License分发 。 世界各地的人们和组织都使用Drupal为政府站点,个人博客,企业等提供动力。 Drupal在其他CMS框架中的独特之处在于其不断发展的社区和一系列功能,这些功能包括安全流程,可靠的性能,模块化和适应灵活性。
Drupal requires installing the LAMP (Linux, Apache, MySQL, and PHP) or LEMP (Linux, Nginx, MySQL, and PHP) stack, but installing individual components is a time-consuming task. We can use tools like Docker and Docker Compose to simplify the process of installing Drupal. This tutorial will use Docker images for installing individual components within the Docker containers. By using Docker Compose, we can define and manage multiple containers for the database, application, and the networking/communication between them.
Drupal需要安装LAMP (Linux,Apache,MySQL和PHP)或LEMP (Linux,Nginx,MySQL和PHP)堆栈,但是安装单个组件是一项耗时的工作。 我们可以使用Docker和Docker Compose之类的工具来简化Drupal的安装过程。 本教程将使用Docker映像在Docker容器中安装各个组件。 通过使用Docker Compose,我们可以为数据库,应用程序以及它们之间的网络/通信定义和管理多个容器。
In this tutorial, we will install Drupal using Docker Compose so that we can take advantage of containerization and deploy our Drupal website on servers. We will be running containers for a MySQL database, Nginx webserver, and Drupal. We will also secure our installation by obtaining TLS/SSL certificates with Let’s Encrypt for the domain we want to associate with our site. Finally, we will set up a cron job to renew our certificates so that our domain remains secure.
在本教程中,我们将使用Docker Compose安装Drupal,以便我们可以利用容器化并将Drupal网站部署在服务器上。 我们将为MySQL数据库, Nginx Web服务器和Drupal运行容器。 我们还将通过使用我们要与我们的网站关联的域的Let’s Encrypt获取TLS / SSL证书来保护安装。 最后,我们将建立一个cron作业来更新我们的证书,以便我们的域保持安全。
To follow this tutorial, we will need:
要遵循本教程,我们将需要:
sudo
privileges and an active firewall. For guidance on how to set these up, please see this Initial Server Setup guide.
运行Ubuntu 18.04的服务器,以及具有sudo
特权和活动防火墙的非root用户。 有关如何进行设置的指导,请参阅此初始服务器设置指南 。
your_domain
throughout. You can get one for free at Freenom, or use the domain registrar of your choice.
注册域名。 本教程将整个使用your_domain
。 您可以从Freenom免费获得一个,或使用您选择的域名注册商。
your_domain
pointing to your server’s public IP address.
A记录,其中your_domain
指向服务器的公共IP地址。
www.your_domain
pointing to your server’s public IP address.
与www. your_domain
的A记录www. your_domain
www. your_domain
指向服务器的公共IP地址。
Both of the following DNS records set up for your server. You can follow this introduction to DigitalOcean DNS for details on how to add them to a DigitalOcean account, if that’s what you’re using: 为服务器设置了以下两个DNS记录。 您可以按照DigitalOcean DNS简介进行操作,以获取有关如何将其添加到DigitalOcean帐户的详细信息,如果您正在使用的话:
Before running any containers, we need to define the configuration for our Nginx web server. Our configuration file will include some Drupal-specific location blocks, along with a location block to direct Let’s Encrypt verification requests to the Certbot client for automated certificate renewals.
在运行任何容器之前,我们需要为Nginx Web服务器定义配置。 我们的配置文件将包括一些Drupal特定的位置块,以及一个位置块,该位置块将Let’s Encrypt验证请求定向到Certbot客户端以进行自动证书更新。
First, let’s create a project directory for our Drupal setup named drupal
:
首先,让我们为Drupal设置创建一个名为drupal
的项目目录:
Move into the newly created directory:
移至新创建的目录:
Now we can make a directory for our configuration file:
现在我们可以为我们的配置文件创建一个目录:
Open the file with nano or your favorite text editor:
使用nano或您喜欢的文本编辑器打开文件:
In this file, we will add a server block with directives for our server name and document root, and location blocks to direct the Certbot client’s request for certificates, PHP processing, and static asset requests.
在此文件中,我们将添加一个服务器块,其中包含用于我们的服务器名称和文档根的指令,以及用于指示Certbot客户端对证书,PHP处理和静态资产请求的指令的位置块。
Add the following code into the file. Be sure to replace your_domain
with your own domain name:
将以下代码添加到文件中。 确保用您自己的域名替换your_domain
:
~/drupal/nginx-conf/nginx.conf
〜/ drupal / nginx-conf / nginx.conf
server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
index index.php index.html index.htm;
root /var/www/html;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
rewrite ^/core/authorize.php/core/authorize.php(.*)$ /core/authorize.php$1;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass drupal:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
location = /robots.txt {
log_not_found off; access_log off; allow all;
}
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
expires max;
log_not_found off;
}
}
Our server block includes the following information:
我们的服务器块包含以下信息:
Directives:
指令:
listen
: This tells Nginx to listen on port 80
, which will allow us to use Certbot’s webroot plugin for our certificate requests. Note that we are not including port 443
yet—we will update our configuration to include SSL once we have successfully obtained our certificates.
listen
:这告诉Nginx监听端口80
,这将使我们能够将Certbot的webroot插件用于我们的证书请求。 请注意,我们尚未包括端口443
一旦成功获得证书,我们将更新配置以包括SSL。
server_name
: This defines our server name and the server block that should be used for requests to our server. Be sure to replace your_domain
in this line with your own domain name.
server_name
:这定义了我们的服务器名称和用于请求服务器的服务器块。 确保用您自己的域名替换此行中的your_domain
。
index
: The index directive defines the files that will be used as indexes when processing requests to our server. We’ve modified the default order of priority here, moving index.php
in front of index.html
so that Nginx prioritizes files called index.php
when possible.
index
:index指令定义在处理对我们服务器的请求时将用作索引的文件。 我们在这里修改了默认的优先级顺序,将index.php
移到了index.html
前面,以便Nginx在可能的情况下优先处理名为index.php
文件。
root
: Our root directive names the root directory for requests to our server. This directory, /var/www/html
, is created as a mount point at build time by instructions in our Drupal Dockerfile. These Dockerfile instructions also ensure that the files from the Drupal release are mounted to this volume.
root
:我们的root指令将根目录命名为服务器请求的根目录。 根据我们的Drupal Dockerfile中的说明,该目录/var/www/html
是在构建时作为安装点创建的。 这些Dockerfile指令还确保将Drupal发行版中的文件安装到该卷。
Location Blocks:
定位块:
location ~ /.well-known/acme-challenge
: This location block will handle requests to the .well-known
directory, where Certbot will place a temporary file to validate that the DNS for our domain resolves to our server. With this configuration in place, we will be able to use Certbot’s webroot plugin to obtain certificates for our domain.
location ~ /.well-known/acme-challenge
.well-known
acme .well-known
location ~ /.well-known/acme-challenge
:此位置块将处理对.well-known
目录的请求,Certbot将在其中放置一个临时文件,以验证我们域的DNS可以解析到我们的服务器。 有了此配置后,我们将能够使用Certbot的webroot插件来获取我们域的证书。
location /
: In this location block, we’ll use a try_files
directive to check for files that match individual URI requests. Instead of returning a 404 Not Found
status as a default, however, we’ll pass control to Drupal’s index.php
file with the request arguments.
location /
:在此location块中,我们将使用try_files
指令来检查与单个URI请求匹配的文件。 但是,我们不会使用默认参数返回404 Not Found
状态,而是将控制权传递给Drupal的index.php
文件。
location ~ \.php$
: This location block will handle PHP processing and proxy these requests to our drupal container. Because our Drupal Docker image will be based on the php:fpm
image, we will also include configuration options that are specific to the FastCGI protocol in this block. Nginx requires an independent PHP processor for PHP requests: in our case, these requests will be handled by the php-fpm
processor that’s included with the php:fpm
image. Additionally, this location block includes FastCGI-specific directives, variables, and options that will proxy requests to the Drupal application running in our Drupal container, set the preferred index for the parsed request URI, and parse URI requests.
location ~ \.php$
:此location块将处理PHP处理并将这些请求代理到我们的drupal容器。 因为我们的Drupal Docker映像将基于php:fpm
映像,所以我们还将在此块中包含特定于FastCGI协议的配置选项。 Nginx需要一个独立PHP处理器来处理PHP请求:在我们的示例中,这些请求将由php:fpm
映像随附的php-fpm
处理器处理。 此外,此位置块包括特定于FastCGI的指令,变量和选项,这些指令将请求代理到运行在我们的Drupal容器中的Drupal应用程序,为解析的请求URI设置首选索引,并解析URI请求。
location ~ /\.ht
: This block will handle .htaccess
files since Nginx won’t serve them. The deny_all
directive ensures that .htaccess
files will never be served to users.
location ~ /\.ht
:由于Nginx不会为它们提供服务,因此该块将处理.htaccess
文件。 deny_all
指令可确保.htaccess
文件永远不会提供给用户。
location = /favicon.ico, location = /robots.txt
: These blocks ensure that requests to /favicon.ico
and /robots.txt
will not be logged.
location = /favicon.ico, location = /robots.txt
:这些块可确保不会记录对/favicon.ico
和/robots.txt
请求。
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$
: This block turns off logging for static asset requests and ensures that these assets are highly cacheable, as they are typically expensive to serve.
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$
:此块关闭静态资产请求的日志记录,并确保这些资产具有很高的可缓存性,因为它们通常很昂贵。
For more information about FastCGI proxying, see Understanding and Implementing FastCGI Proxying in Nginx. For information about server and location blocks, see Understanding Nginx Server and Location Block Selection Algorithms.
有关FastCGI代理的更多信息,请参见了解和实现Nginx中的FastCGI代理 。 有关服务器和位置块的信息,请参阅了解Nginx服务器和位置块选择算法 。
Save and close the file when you are finished editing.
完成编辑后,保存并关闭文件。
With your Nginx configuration in place, you can move on to creating environment variables to pass to your application and database containers at runtime.
完成Nginx配置后,您可以继续创建环境变量,以在运行时传递给应用程序和数据库容器。
Our Drupal application needs a database (MySQL, PostgresSQL, etc.) for saving information related to the site. The Drupal container will need access to certain environment variables at runtime in order to access the database (MySQL) container. These variables contain the sensitive information like the credentials of the database, so we can’t expose them directly in the Docker Compose file—the main file that contains information about how our containers will run.
我们的Drupal应用程序需要一个数据库(MySQL,PostgresSQL等)来保存与站点有关的信息。 为了访问数据库(MySQL)容器,Drupal容器将需要在运行时访问某些环境变量。 这些变量包含敏感信息,例如数据库的凭据,因此我们无法在Docker Compose文件中直接公开它们-主文件包含有关容器如何运行的信息。
It is always recommended to set the sensitive values in the .env
file and restrict its circulation. This will prevent these values from copying over to our project repositories and being exposed publicly.
始终建议在.env
文件中设置敏感值并限制其流通。 这将防止这些值复制到我们的项目存储库中并公开显示。
In the main project directory, ~/drupal
, create and open a file called .env
:
在主项目目录~/drupal
,创建并打开一个名为.env
的文件:
Add the following variables to the .env
file, replacing the highlighted sections with the credentials you want to use:
将以下变量添加到.env
文件,将突出显示的部分替换为您要使用的凭据:
~/drupal/.env
〜/ drupal / .env
MYSQL_ROOT_PASSWORD=root_password
MYSQL_DATABASE=drupal
MYSQL_USER=drupal_database_user
MYSQL_PASSWORD=drupal_database_password
We have now added the password for the MySQL root administrative account, as well as our preferred username and password for our application database.
现在,我们添加了MySQL根管理帐户的密码,以及我们的应用程序数据库的首选用户名和密码。
Our .env
file contains sensitive information so it is always recommended to include it in a project’s .gitignore
and .dockerignore
files so that it won’t be added in our Git repositories and Docker images.
我们的.env
文件包含敏感信息,因此始终建议将其包含在项目的.gitignore
和.dockerignore
文件中,这样就不会将其添加到我们的Git存储库和Docker映像中。
If you plan to work with Git for version control, initialize your current working directory as a repository with git init
:
如果您打算使用Git进行版本控制, 请使用git init
将当前的工作目录初始化为存储库 :
Open .gitignore
file:
打开.gitignore
文件:
Add the following:
添加以下内容:
~/drupal/.gitignore
〜/ drupal / .gitignore
.env
Save and exit the file.
保存并退出文件。
Similarly, open the .dockerignore
file:
同样,打开.dockerignore
文件:
Then add the following:
然后添加以下内容:
~/drupal/.dockerignore
〜/ drupal / .dockerignore
.env
.git
Save and exit the file.
保存并退出文件。
Now that we have taken measures to safeguard our credentials as environment variables, let’s move to our next step of defining our services in a docker-compose.yml
file.
现在我们已经采取措施来保护我们的凭据作为环境变量,让我们进入下一步,在docker-compose.yml
文件中定义我们的服务。
Docker Compose is a tool for defining and running multi-container Docker applications. We define a YAML
file to configure our application’s services. A service in Docker Compose is a running container, and Compose allows us to link these services together with shared volumes and networks.
Docker Compose是用于定义和运行多容器Docker应用程序的工具。 我们定义一个YAML
文件来配置我们的应用程序的服务。 Docker Compose中的服务是一个正在运行的容器,Compose允许我们将这些服务与共享的卷和网络链接在一起。
We will create different containers for our Drupal application, database, and web server. Along with these, we will also create a container to run Certbot in order to obtain certificates for our web server.
我们将为我们的Drupal应用程序,数据库和Web服务器创建不同的容器。 伴随着这些,我们还将创建一个容器来运行Certbot ,以便为我们的Web服务器获取证书。
Create a docker-compose.yml
file:
创建一个docker-compose.yml
文件:
Add the following code to define the Compose file version and mysql
database service:
添加以下代码以定义Compose文件版本和mysql
数据库服务:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
version: "3"
services:
mysql:
image: mysql:8.0
container_name: mysql
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
env_file: .env
volumes:
- db-data:/var/lib/mysql
networks:
- internal
Let’s go through these one-by-one with all the configuration options of the mysql
service:
让我们通过mysql
服务的所有配置选项一一介绍这些内容:
image
: This specifies the image that will be used/pulled for creating the container. It is always recommended to use the image with the proper version tag excluding the latest
tag to avoid future conflicts. Read more on Dockerfile best practices from the Docker documentation.
image
:指定用于创建容器的图像。 始终建议使用带有正确版本标签( latest
标签除外)的映像,以免将来发生冲突。 从Docker文档中阅读有关Dockerfile最佳实践的更多信息。
container_name
: To define the name of the container.
container_name
:定义container_name
的名称。
command
: This is used to override the default command (CMD instruction) in the image. MySQL has supported different authentication plugins, but mysql_native_password
is the traditional method to authenticate. Since PHP, and hence Drupal, won’t support the newer MySQL authentication, we need to set the --default-authentication-plugin=mysql_native_password
as the default authentication mechanism.
command
:用于覆盖图像中的默认命令(CMD指令)。 MySQL支持不同的身份验证插件 ,但是mysql_native_password
是传统的身份验证方法。 由于PHP和Drupal不支持较新MySQL身份验证 ,因此我们需要将--default-authentication-plugin=mysql_native_password
为默认身份验证机制。
restart
: This is used to define the container restart policy. The unless-stopped
policy restarts a container unless it is stopped manually.
restart
:用于定义容器重启策略。 unless-stopped
策略,除非手动停止容器,否则它将重新启动容器。
env_file
: This adds the environment variables from a file. In our case, it will read the environment variables from the .env
file defined in the previous step.
env_file
:这将从文件中添加环境变量。 在我们的例子中,它将从上一步中定义的.env
文件中读取环境变量。
volumes
: This mounts host paths or named volumes, specified as sub-options to a service. We are mounting a named volume called db-data
to the /var/lib/mysql
directory on the container, where MySQL by default will write its data files.
volumes
:这将装载主机路径或命名的卷,指定为服务的子选项。 我们正在容器上的/var/lib/mysql
目录中装载一个名为db-data
的命名卷,默认情况下,MySQL将在其中写入其数据文件。
networks
: This defines the internal
network that our application service will join. We will define the networks at the end of the file.
networks
:这定义了我们的应用程序服务将加入的internal
网络。 我们将在文件末尾定义网络。
We have defined our mysql
service definition, so now let’s add the definition of the drupal
application service to the end of the file:
我们已经定义了mysql
服务定义,因此现在让我们将drupal
应用程序服务的定义添加到文件末尾:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
...
drupal:
image: drupal:8.7.8-fpm-alpine
container_name: drupal
depends_on:
- mysql
restart: unless-stopped
networks:
- internal
- external
volumes:
- drupal-data:/var/www/html
In this service definition, we are naming our container and defining a restart policy, as we did with the mysql
service. We’re also adding some options specific to this container:
在此服务定义中,我们将为容器命名并定义一个重启策略,就像对mysql
服务所做的那样。 我们还添加了一些特定于此容器的选项:
image
: Here, we are using the 8.7.8-fpm-alpine
Drupal image. This image has the php-fpm
processor that our Nginx web server requires to handle PHP processing. Moreover we are using the alpine
image, derived from the Alpine Linux project, which will reduce the size of the overall image and is recommended in the Dockerfile best practices. Drupal has more versions of images, so check them out on Dockerhub.
image
:在这里,我们使用的是8.7.8-fpm-alpine
Drupal图片。 该图像具有我们的Nginx Web服务器处理PHP处理所需的php-fpm
处理器。 此外,我们正在使用源自Alpine Linux项目的alpine
映像,它将减少整个映像的大小,并在Dockerfile最佳实践中建议使用 。 Drupal有更多版本的图像,因此请在Dockerhub上检查它们。
depends_on
: This is used to express dependency between services. Defining the mysql
service as the dependency to our drupal
container will ensure that our drupal
container will be created after the mysql
container and enable our application to start smoothly.
depends_on
:用于表示服务之间的依赖关系。 将mysql
服务定义为对我们的drupal
容器的依赖关系将确保我们的drupal
容器将在mysql
容器之后创建,并使我们的应用程序顺利启动。
networks
: Here, we have added this container to the external
network along with the internal
network. This will ensure that our mysql
service is accessible only from the drupal
container through the internal
network while keeping this container accessible to other containers through the external
network.
networks
:这里,我们已将此容器与internal
网络一起添加到external
网络。 这将确保只能通过internal
网络从drupal
容器访问我们的mysql
服务,同时保持该容器可通过external
网络访问其他容器。
volumes
: We are mounting a named volume called drupal-data
to the /var/www/html
mountpoint created by the Drupal image. Using a named volume in this way will allow us to share our application code with other containers.
volumes
:我们正在将一个名为drupal-data
的命名卷装入由Drupal映像创建的/var/www/html
挂载点。 以这种方式使用命名卷将使我们能够与其他容器共享应用程序代码。
Next, let’s add the Nginx service definition after the drupal
service definition:
接下来,让我们在drupal
服务定义之后添加Nginx服务定义:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
...
webserver:
image: nginx:1.17.4-alpine
container_name: webserver
depends_on:
- drupal
restart: unless-stopped
ports:
- 80:80
volumes:
- drupal-data:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- external
Again, we’re naming our container and making it dependent on the Drupal container in order of starting. We’re also using an alpine image—the 1.17.4-alpine
Nginx image.
再次,我们要命名容器,并使其按照启动顺序依赖于Drupal容器。 我们还使用了高山图像1.17.4-alpine
Nginx图像。
This service definition also includes the following options:
此服务定义还包括以下选项:
ports
: This exposes port 80
to enable the configuration options we defined in our nginx.conf
file in Step 1.
ports
:这暴露了端口80
以启用我们在步骤1中的nginx.conf
文件中定义的配置选项。
volumes
: Here, we are defining both the named volume and host path:
volumes
:在这里,我们定义两个命名的数量和主机路径:
drupal-data:/var/www/html
: This will mount our Drupal application code to the /var/www/html
directory, which we set as the root in our Nginx server block.
drupal-data:/var/www/html
:这会将我们的Drupal应用程序代码安装到/var/www/html
目录,该目录设置为Nginx服务器块中的根目录。
./nginx-conf:/etc/nginx/conf.d
: This will mount the Nginx configuration directory on the host to the relevant directory on the container, ensuring that any changes we make to files on the host will be reflected in the container.
./nginx-conf:/etc/nginx/conf.d
:这会将主机上的Nginx配置目录挂载到容器上的相关目录,以确保我们对主机上文件的任何更改都将反映在容器中。
certbot-etc:/etc/letsencrypt
: This will mount the relevant Let’s Encrypt certificates and keys for our domain to the appropriate directory on the container.
certbot-etc:/etc/letsencrypt
:这会将域的相关“让我们加密”证书和密钥安装到容器上的相应目录。
networks
: We have defined the external
network only to let this container communicate with the drupal
container and not with the mysql
container.
networks
:我们已定义了external
网络,仅允许该容器与drupal
容器通信,而不与mysql
容器通信。
Finally, we will add our last service definition for the certbot
service. Be sure to replace sammy@your_domain
and your_domain
with your own email and domain name:
最后,我们将为certbot
服务添加最后一个服务定义。 确保使用您自己的电子邮件和域名替换sammy@your_domain
和your_domain
:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
...
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- drupal-data:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain -d www.your_domain
This definition tells Compose to pull the certbot/certbot
image from Docker Hub. It also uses named volumes to share resources with the Nginx container, including the domain certificates and key in certbot-etc
and the application code in drupal-data
.
此定义告诉Compose从Docker Hub中提取certbot/certbot
映像。 它还使用命名卷与Nginx容器共享资源,包括certbot-etc
的域证书和密钥以及drupal-data
的应用程序代码。
We have also used depends_on
to make sure that the certbot
container will be started after the webserver
service is running.
我们还使用了depends_on
来确保在运行webserver
服务之后启动certbot
容器。
We haven’t specified any networks
here because this container won’t communicate to any services over the network. It is only adding the domain certificates and key, which we have mounted using the named volumes.
我们此处未指定任何networks
,因为此容器不会通过网络与任何服务进行通信。 它仅添加我们使用命名卷安装的域证书和密钥。
We have also included the command
option that specifies a subcommand to run with the container’s default certbot
command. The Certbot client supports plugins for obtaining and installing certificates. We are using the webroot
plugin to obtain a certificate by including certonly
and --webroot
on the command line. Read more about the plugin and additional commands from the official Certbot Documentation.
我们还包括了command
选项,该选项指定要与容器的默认certbot
命令一起运行的子命令。 Certbot客户端支持用于获取和安装证书的插件。 我们正在使用webroot
插件通过在命令行中包含certonly
和--webroot
来获取证书。 从官方Certbot文档中阅读有关插件和其他命令的更多信息 。
After the certbot
service definition, add the network and volume definitions:
在certbot
服务定义之后,添加网络和卷定义:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
...
networks:
external:
driver: bridge
internal:
driver: bridge
volumes:
drupal-data:
db-data:
certbot-etc:
The top-level networks
key lets us specify networks to be created. networks
allows communication across the services/containers on all the ports since they are on the same Docker daemon host. We have defined two networks, internal
and external
, to secure the communication of the webserver
, drupal
, and mysql
services.
顶级networks
键使我们可以指定要创建的网络。 networks
允许所有端口上的服务/容器之间进行通信,因为它们位于同一Docker守护程序主机上。 我们定义了两个网络, internal
和external
,以保护webserver
, drupal
和mysql
服务的通信安全。
The volumes
key is used to define the named volumes drupal-data
, db-data
, and certbot-etc
. When Docker creates volumes, the contents of the volume are stored in a directory on the host filesystem, /var/lib/docker/volumes/
, that’s managed by Docker. The contents of each volume then get mounted from this directory to any container that uses the volume. In this way, it’s possible to share code and data between containers.
volumes
密钥用于定义命名的卷drupal-data
, db-data
和certbot-etc
。 Docker创建卷时,卷的内容存储在主机文件系统/var/lib/docker/volumes/
的目录中,该目录由Docker管理。 然后,每个卷的内容将从该目录挂载到使用该卷的任何容器中。 这样,可以在容器之间共享代码和数据。
The finished docker-compose.yml
file will look like this:
完成docker-compose.yml
文件如下所示:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
version: "3"
services:
mysql:
image: mysql:8.0
container_name: mysql
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
env_file: .env
volumes:
- db-data:/var/lib/mysql
networks:
- internal
drupal:
image: drupal:8.7.8-fpm-alpine
container_name: drupal
depends_on:
- mysql
restart: unless-stopped
networks:
- internal
- external
volumes:
- drupal-data:/var/www/html
webserver:
image: nginx:1.17.4-alpine
container_name: webserver
depends_on:
- drupal
restart: unless-stopped
ports:
- 80:80
volumes:
- drupal-data:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- external
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- drupal-data:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain -d www.your_domain
networks:
external:
driver: bridge
internal:
driver: bridge
volumes:
drupal-data:
db-data:
certbot-etc:
We are done with defining our services. Next, let’s start the container and test our certificate requests.
定义服务已经完成。 接下来,让我们启动容器并测试我们的证书请求。
We can start our containers with the docker-compose up
command, which will create and run our containers in the order we have specified. If our domain requests are successful, we will see the correct exit status in our output and the right certificates mounted in the /etc/letsencrypt/live
folder on the web server container.
我们可以使用docker-compose up
命令启动容器,该命令将按照指定的顺序创建和运行容器。 如果我们的域请求成功,我们将在输出中看到正确的退出状态,并在Web服务器容器的/etc/letsencrypt/live
文件夹中安装正确的证书。
To run the containers in the background, use the docker-compose up
command with the -d
flag:
要在后台运行容器,请使用带有-d
标志的docker-compose up
命令:
You will see similar output confirming that your services have been created:
您将看到类似的输出,确认您的服务已创建:
Output
...
Creating mysql ... done
Creating drupal ... done
Creating webserver ... done
Creating certbot ... done
Check the status of the services using the docker-compose ps
command:
使用docker-compose ps
命令检查服务状态:
We will see the mysql
, drupal
, and webserver
services with a State
of Up
, while certbot
will be exited with a 0
status message:
我们将看到State
为Up
的mysql
, drupal
和webserver
服务,而certbot
将以0
状态消息退出:
Output
Name Command State Ports
--------------------------------------------------------------------------
certbot certbot certonly --webroot ... Exit 0
drupal docker-php-entrypoint php-fpm Up 9000/tcp
mysql docker-entrypoint.sh --def ... Up 3306/tcp, 33060/tcp
webserver nginx -g daemon off; Up 0.0.0.0:80->80/tcp
If you see anything other than Up
in the State
column for the mysql
, drupal
, or webserver
services, or an exit status other than 0
for the certbot
container, be sure to check the service logs with the docker-compose logs
command:
如果您在mysql
, drupal
或webserver
服务的State
列中看到Up
以外的内容,或者certbot
容器的退出状态不是0
,请确保使用certbot
docker-compose logs
命令检查服务日志:
We can now check that our certificates mounted on the webserver
container using the docker-compose exec
command:
现在,我们可以使用docker-compose exec
命令检查证书是否已安装在webserver
容器上:
This will give the following output:
这将给出以下输出:
Output
total 16
drwx------ 3 root root 4096 Oct 5 09:15 .
drwxr-xr-x 9 root root 4096 Oct 5 09:15 ..
-rw-r--r-- 1 root root 740 Oct 5 09:15 README
drwxr-xr-x 2 root root 4096 Oct 5 09:15 your_domain
Now that everything runs successfully, we can edit our certbot
service definition to remove the --staging
flag.
现在一切都可以成功运行,我们可以编辑certbot
服务定义以删除--staging
标志。
Open the docker-compose.yml
file, go to the certbot
service definition, and replace the --staging
flag in the command option with the --force-renewal
flag, which will tell Certbot that you want to request a new certificate with the same domains as an existing certificate. The updated certbot
definition will look like this:
打开docker-compose.yml
文件,转到certbot
服务定义,然后将命令选项中的--staging
标志替换为--force-renewal
标志,这将告知Certbot您想要使用来请求新证书。与现有证书相同的域。 更新后的certbot
定义将如下所示:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
...
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- drupal-data:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --force-renewal -d your_domain -d www.your_domain
...
We need to run docker-compose up
again to recreate the certbot
container. We will also include the --no-deps
option to tell Compose that it can skip starting the webserver
service, since it is already running:
我们需要再次运行certbot
docker-compose up
来重新创建certbot
容器。 我们还将包括--no-deps
选项,以告知Compose可以跳过启动webserver
服务的步骤,因为该服务已经在运行:
We will see output indicating that our certificate request was successful:
我们将看到输出,表明我们的证书申请成功:
Output
Recreating certbot ... done
Attaching to certbot
certbot | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot | Plugins selected: Authenticator webroot, Installer None
certbot | Renewing an existing certificate
certbot | Performing the following challenges:
certbot | http-01 challenge for your_domain
certbot | http-01 challenge for www.your_domain
certbot | Using the webroot path /var/www/html for all unmatched domains.
certbot | Waiting for verification...
certbot | Cleaning up challenges
certbot | IMPORTANT NOTES:
certbot | - Congratulations! Your certificate and chain have been saved at:
certbot | /etc/letsencrypt/live/your_domain/fullchain.pem
certbot | Your key file has been saved at:
certbot | /etc/letsencrypt/live/your_domain/privkey.pem
certbot | Your cert will expire on 2020-01-03. To obtain a new or tweaked
certbot | version of this certificate in the future, simply run certbot
certbot | again. To non-interactively renew *all* of your certificates, run
certbot | "certbot renew"
certbot | - Your account credentials have been saved in your Certbot
certbot | configuration directory at /etc/letsencrypt. You should make a
certbot | secure backup of this folder now. This configuration directory will
certbot | also contain certificates and private keys obtained by Certbot so
certbot | making regular backups of this folder is ideal.
certbot | - If you like Certbot, please consider supporting our work by:
certbot |
certbot | Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
certbot | Donating to EFF: https://eff.org/donate-le
certbot |
certbot exited with code 0
Now that we have successfully generated our certificates, we can update our Nginx Configuration to include SSL.
现在我们已经成功生成了证书,我们可以更新Nginx配置以包括SSL。
After installing SSL certificates in Nginx, we will need to redirect all the HTTP requests to HTTPS. We will also have to specify our SSL certificate and key locations and add security parameters and headers.
在Nginx中安装SSL证书后,我们将需要将所有HTTP请求重定向到HTTPS。 我们还必须指定SSL证书和密钥位置,并添加安全性参数和标头。
Since you are going to recreate the webserver
service to include these additions, you can stop it now:
由于您将重新创建webserver
服务以包括这些附加功能,因此现在可以停止它:
This will give the following output:
这将给出以下输出:
Output
Stopping webserver ... done
Next, let’s remove the Nginx configuration file we created earlier:
接下来,让我们删除之前创建的Nginx配置文件:
Open another version of the file:
打开文件的另一个版本:
Add the following code to the file to redirect HTTP to HTTPS and to add SSL credentials, protocols, and security headers. Remember to replace your_domain
with your own domain:
将以下代码添加到文件中,以将HTTP重定向到HTTPS并添加SSL凭据,协议和安全标头。 请记住用您自己的域替换your_domain
:
~/drupal/nginx-conf/nginx.conf
〜/ drupal / nginx-conf / nginx.conf
server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
rewrite ^ https://$host$request_uri? permanent;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name your_domain www.your_domain;
index index.php index.html index.htm;
root /var/www/html;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
rewrite ^/core/authorize.php/core/authorize.php(.*)$ /core/authorize.php$1;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass drupal:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
location = /robots.txt {
log_not_found off; access_log off; allow all;
}
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
expires max;
log_not_found off;
}
}
The HTTP server block specifies the webroot plugin for Certbot renewal requests to the .well-known/acme-challenge
directory. It also includes a rewrite
directive that directs HTTP requests to the root directory to HTTPS.
HTTP服务器块为Certbot的.well .well-known/acme-challenge
目录续订请求指定了webroot插件。 它还包括一个rewrite
指令,该指令将对根目录的HTTP请求定向到HTTPS。
The HTTPS server block enables ssl
and http2
. To read more about how HTTP/2 iterates on HTTP protocols and the benefits it can have for website performance, please see the introduction to How To Set Up Nginx with HTTP/2 Support on Ubuntu 18.04.
HTTPS服务器块启用ssl
和http2
。 要了解有关HTTP / 2如何在HTTP协议上进行迭代以及其对网站性能的好处的更多信息,请参阅“ 如何在Ubuntu 18.04上设置具有HTTP / 2支持的Nginx”的介绍。
These blocks enable SSL, as we have included our SSL certificate and key locations along with the recommended headers. These headers will enable us to get an A rating on the SSL Labs and Security Headers server test sites.
这些块启用了SSL,因为我们包括了SSL证书和密钥位置以及推荐的标头。 这些标头将使我们在SSL实验室和安全标头服务器测试站点上获得A评级。
Our root
and index
directives are also located in this block, as are the rest of the Drupal-specific location blocks discussed in Step 1.
我们的root
和index
指令也位于此块中,步骤1中讨论的其余Drupal特定位置块也位于此块中。
Save and close the updated Nginx configuration file.
保存并关闭更新的Nginx配置文件。
Before recreating the webserver
container, we will need to add a 443
port mapping to our webserver
service definition as we have enabled SSL certificates.
在重新创建webserver
容器之前,由于启用了SSL证书,我们需要向我们的webserver
服务定义添加443
端口映射。
Open the docker-compose.yml
file:
打开docker-compose.yml
文件:
Make the following changes in the webserver
service definition:
在webserver
服务定义中进行以下更改:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
...
webserver:
image: nginx:1.17.4-alpine
container_name: webserver
depends_on:
- drupal
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- drupal-data:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- external
...
After enabling the SSL certificates, our docker-compose.yml
will look like this:
启用SSL证书后,我们docker-compose.yml
将如下所示:
~/drupal/docker-compose.yml
〜/ drupal / docker-compose.yml
version: "3"
services:
mysql:
image: mysql:8.0
container_name: mysql
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
env_file: .env
volumes:
- db-data:/var/lib/mysql
networks:
- internal
drupal:
image: drupal:8.7.8-fpm-alpine
container_name: drupal
depends_on:
- mysql
restart: unless-stopped
networks:
- internal
- external
volumes:
- drupal-data:/var/www/html
webserver:
image: nginx:1.17.4-alpine
container_name: webserver
depends_on:
- drupal
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- drupal-data:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- external
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- drupal-data:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --force-renewal -d your_domain -d www.your_domain
networks:
external:
driver: bridge
internal:
driver: bridge
volumes:
drupal-data:
db-data:
certbot-etc:
Save and close the file. Let’s recreate the webserver
service with our updated configuration:
保存并关闭文件。 让我们使用更新后的配置重新创建webserver
服务:
This will give the following output:
这将给出以下输出:
Output
Recreating webserver ... done
Check the services with docker-compose ps
:
使用docker-compose ps
检查服务:
We will see the mysql
, drupal
, and webserver
services as Up
while certbot
will be exited with a 0
status message:
我们将看到mysql
, drupal
和webserver
服务为Up
而certbot
将以0
状态消息退出:
Output
Name Command State Ports
--------------------------------------------------------------------------
certbot certbot certonly --webroot ... Exit 0
drupal docker-php-entrypoint php-fpm Up 9000/tcp
mysql docker-entrypoint.sh --def ... Up 3306/tcp, 33060/tcp
webserver nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
Now, all our services are running and we are good to move forward with installing Drupal through the web interface.
现在,我们所有的服务都在运行,我们很高兴通过Web界面安装Drupal。
Let’s complete the installation through Drupal’s web interface.
让我们通过Drupal的Web界面完成安装。
In a web browser, navigate to the server’s domain. Remember to substitute your_domain
here with your own domain name:
在Web浏览器中,导航到服务器的域。 请记住在这里用您自己的域名替换your_domain
:
https://your_domain
Select the language to use:
选择要使用的语言:
Click Save and continue. We will land on the Installation profile page. Drupal has multiple profiles, so select the Standard profile and click on Save and continue.
点击保存并继续 。 我们将进入“ 安装配置文件”页面。 Drupal有多个配置文件,因此选择标准配置文件,然后单击保存并继续 。
After selecting the profile, we will move forward to Database configuration page. Select the Database type as MySQL, MariaDB, Percona Server, or equivalent and enter the values of Database name, username, and password from the values corresponding to MYSQL_DATABASE
, MYSQL_USER
, and MYSQL_PASSWORD
respectively defined in the .env
file in Step 2. Click on Advanced Options and set the value of Host to the name of the mysql
service container. Click on Save and continue.
选择配置文件后,我们将前进至“ 数据库配置”页面。 选择数据库类型为MySQL,MariaDB,Percona Server或等效 数据库 ,并从与步骤2中的.env
文件分别定义的MYSQL_DATABASE
, MYSQL_USER
和MYSQL_PASSWORD
对应的值中输入数据库名称 , 用户名和密码的值。单击单击高级选项,然后将Host的值设置为mysql
服务容器的名称。 单击保存并继续 。
After configuring the database, it will start installing Drupal default modules and themes:
配置数据库后,它将开始安装Drupal默认模块和主题:
Once the site is installed, we will land on the Drupal site setup page for configuring the site name, email, username, password, and regional settings. Fill in the information and click on Save and continue:
安装站点后,我们将进入Drupal站点设置页面,以配置站点名称,电子邮件,用户名,密码和区域设置。 填写信息,然后单击保存并继续 :
After clicking Save and continue, we can see the Welcome to Drupal page, which shows that our Drupal site is up and running successfully.
单击“ 保存并继续”后 ,我们可以看到“ 欢迎使用Drupal”页面,该页面显示我们的Drupal站点已成功启动并运行。
Now that our Drupal installation is complete, we need to ensure that our SSL certificates will renew automatically.
现在我们的Drupal安装已经完成,我们需要确保SSL证书将自动更新。
Let’s Encrypt certificates are valid for 90 days, so we need to set up an automated renewal process to ensure that they do not lapse. One way to do this is to create a job with the cron
scheduling utility. In this case, we will create a cron
job to periodically run a script that will renew our certificates and reload our Nginx configuration.
让我们加密证书的有效期为90天,因此我们需要建立一个自动续订过程以确保它们不会失效。 一种方法是使用cron
调度实用程序创建作业。 在这种情况下,我们将创建一个cron
作业以定期运行脚本,该脚本将更新我们的证书并重新加载我们的Nginx配置。
Let’s create the ssl_renew.sh
file to renew our certificates:
让我们创建ssl_renew.sh
文件来更新我们的证书:
Add the following code. Remember to replace the directory name with your own non-root user:
添加以下代码。 请记住用您自己的非root用户替换目录名:
~/drupal/ssl_renew.sh
〜/ drupal / ssl_renew.sh
#!/bin/bash
cd /home/sammy/drupal/
/usr/local/bin/docker-compose -f docker-compose.yml run certbot renew --dry-run && \
/usr/local/bin/docker-compose -f docker-compose.yml kill -s SIGHUP webserver
This script changes to the ~/drupal
project directory and runs the following docker-compose
commands.
该脚本将切换到~/drupal
项目目录,并运行以下docker-compose
命令。
docker-compose run
: This will start a certbot
container and override the command
provided in our certbot
service definition. Instead of using the certonly
subcommand, we’re using the renew
subcommand here, which will renew certificates that are close to expiring. We’ve included the --dry-run
option here to test our script.
certbot
docker-compose run
:这将启动certbot
容器并覆盖我们certbot
服务定义中提供的command
。 certonly
不使用certonly
子命令,而是使用renew
子命令,该命令将更新即将到期的证书。 我们在此处包括--dry-run
选项,以测试脚本。
docker-compose kill
: This will send a SIGHUP
signal to the webserver
container to reload the Nginx configuration.
docker-compose kill
:这会将SIGHUP
信号发送到webserver
容器以重新加载Nginx配置。
Close the file and make it executable by running the following command:
通过运行以下命令关闭文件并使它可执行:
Next, open the root
crontab file to run the renewal script at a specified interval:
接下来,打开crontab root
文件,以指定的时间间隔运行续订脚本:
If this is your first time editing this file, you will be asked to choose a text editor to open the file with:
如果这是您第一次编辑此文件,将要求您选择一个文本编辑器以打开文件,其中包括:
Output
no crontab for root - using an empty one
Select an editor. To change later, run 'select-editor'.
1. /bin/nano
2. /usr/bin/vim.basic
3. /usr/bin/vim.tiny
4. /bin/ed
Choose 1-4 [1]:
...
At the end of the file, add the following line, replacing sammy
with your username:
在文件末尾,添加以下行,将sammy
替换为您的用户名:
crontab
crontab
...
*/5 * * * * /home/sammy/drupal/ssl_renew.sh >> /var/log/cron.log 2>&1
This will set the job interval to every five minutes, so we can test whether or not our renewal request has worked as intended. We have also created a log file, cron.log
, to record relevant output from the job.
这会将作业间隔设置为每五分钟一遍,因此我们可以测试我们的续订请求是否按预期进行。 我们还创建了一个日志文件cron.log
,以记录作业的相关输出。
After five minutes, use the tail
command to check cron.log
to see whether or not the renewal request has succeeded:
五分钟后,使用tail
命令检查cron.log
以查看续订请求是否成功:
You will see output confirming a successful renewal:
您将看到确认续订成功的输出:
Output
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates below have not been saved.)
Congratulations, all renewals succeeded. The following certs have been renewed:
/etc/letsencrypt/live/your_domain/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press CTRL+C
to quit the tail
process.
按CTRL+C
退出tail
过程。
We can now modify the crontab file to run the script every 2nd day of the week at 2 AM. Change the final line of the crontab to the following:
现在,我们可以修改crontab文件,使其在一周的第二天每天凌晨2点运行脚本。 将crontab的最后一行更改为以下内容:
crontab
crontab
...
* 2 * * 2 /home/sammy/drupal/ssl_renew.sh >> /var/log/cron.log 2>&1
Exit and save the file.
退出并保存文件。
Now, let’s remove the --dry-run
option from the ssl_renew.sh
script. First, open it up:
现在,让我们从ssl_renew.sh
脚本中删除--dry-run
选项。 首先,打开它:
Then change the contents to the following:
然后将内容更改为以下内容:
~/drupal/ssl_renew.sh
〜/ drupal / ssl_renew.sh
#!/bin/bash
cd /home/sammy/drupal/
/usr/local/bin/docker-compose -f docker-compose.yml run certbot renew && \
/usr/local/bin/docker-compose -f docker-compose.yml kill -s SIGHUP webserver
Our cron
job will now take care of our SSL certificates expiry by renewing them when they are eligible.
现在,我们的cron
作业将通过在合格时更新SSL证书来处理它们的过期时间。
In this tutorial, we have used Docker Compose to create a Drupal installation with an Nginx web server. As part of this workflow, we obtained TLS/SSL certificates for the domain we wanted associated with our Drupal site and created a cron job to renew these certificates when necessary.
在本教程中,我们使用Docker Compose通过Nginx Web服务器创建了Drupal安装。 作为此工作流程的一部分,我们获取了我们想要与Drupal站点关联的域的TLS / SSL证书,并创建了cron作业以在必要时续订这些证书。
If you would like to learn more about Docker, check out our Docker topic page.
如果您想了解有关Docker的更多信息,请查看我们的Docker主题页面 。
翻译自: https://www.digitalocean.com/community/tutorials/how-to-install-drupal-with-docker-compose
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/162273.html原文链接:https://javaforall.cn