背景
由于公司项目展示需要,现要将前端项目部署到服务器上。Nextjs( nextjs.org/ )是由Vercel发布的一款基于React的服务端渲染框架,其具有SEO优化、文件路由等特点。本项目使用的版本是Nextjs13,使用的服务器为阿里云ECS,同时用Docker完成对项目镜像的构建、推送、拉取以及部署。
部署方案汇总
mindmap
部署
Vercel、Railway等托管平台
云服务器
腾讯云
函数服务
Serverless应用
阿里云
ECS云服务器
1.宝塔面板可视化部署
2.pm2
3.Docker
AWS
AWS Amplify
以上为在部署过程中踩坑过的实现方案,它们各有优缺点。
Vercel、Railway等托管平台
- 对于Vercel等托管平台来说,部署起来较为方便,通常只需要连接远程仓库、选择分支后即可完成部署,同时,Vercel为个人开发者提供了永久免费部署方案、自动化CI/CD等实现。但是,由于其服务器在国外,和阿里云等国内服务器相比访问速度不佳且不够稳定。
云服务器
腾讯云
- 与托管平台相比,AWS、阿里云、腾讯云等第三方云服务器提供商能完成绝大部分的部署需求。对于腾讯云来说,可以选择函数服务或Serverless应用两种不同方式进行部署。其中,Serverless应用部署类似于Vercel等托管平台的实现方案,需要认证并连接远程仓库、选择分支,或是直接上传本地项目文件。但是它需要项目的体积不超过500m,这里可以通过优化import包、拆分chunk等方式来减小项目体积。
- 如果项目体积过大,可以选择函数服务进行部署,函数服务方案需要提前准备好项目镜像或是通过框架模版进行项目创建,该方案不限制项目的体积大小,但是无法像Serverless一样实现自动化CI/CD。由于该方案没有尝试,这里就不再展开。
AWS
- 与阿里云、腾讯云等国内平台相比,AWS的服务在国内外是不同的两个通道,而AWS国内业务目前没有开放给个人开发者,仅针对企业级用户,需要提供企业的相关负责人信息、企业营业执照等信息;而个人版目前只开放了海外服务器,这里可以尽量选择东南亚服务器来提高访问速度。
阿里云(本文实现方案!)
本文将着重讲解如何将项目部署在阿里云服务器上,这里还可以大致细分出多种部署方式:镜像部署、通过宝塔面板上传项目文件、pm2持久化部署。
-
镜像部署(本文重点!)
如果选择镜像部署,则需要通过Docker将项目构建,并推送至阿里云远程镜像仓库,最后在服务器中拉取镜像仓库中的镜像并运行,该方案为本文选择的部署方式,将在接下来的章节中展开。
-
宝塔Linux面板
宝塔面板是由第三方提供的服务器可视化运维管理工具,在这个界面中可以很方便地看到服务器的各种状态、文件、运行中项目、日志等。同时,也可以通过面板提供的软件商店安装一些依赖或环境,例如Nginx、Nodejs、MySQL等。
关于使用宝塔进行项目部署,可以看下这篇文章,这里不再展开。服务端部署 Next.js(附宝塔面板上部署方法)
-
pm2持久化部署
相较于镜像部署来说,pm2更适合传统的服务器部署方式,适用于管理和监控多个Node.js进程,提供进程管理相关功能。它可以监控和管理多个进程,并提供日志管理、负载均衡等功能,从而更好地利用系统资源和保持应用程序的稳定性。关于使用pm2进行项目部署,可以看下这篇文章,这里不再展开。Nextjs部署(pm2)
以上是部署的实现方案,接下来将详细讲解如何通过Docker镜像的方式将项目部署在阿里云的ECS服务器上。
部署前置准备
Docker以及Dockerfile
目前,Docker推出了Desktop客户端,客户端操作可以简化命令行步骤,这里首先安装了Docker Desktop( www.docker.com/products/do… )。根据自己的操作系统完成安装后,可以在界面中看到本地的容器、镜像等信息。
安装完成后,启动客户端后可以通过docker命令查看版本,能看到版本信息即说明安装成功。
docker --version
同时,我们需要在项目根目录配置 Dockerfile 以及 .dockerignore 两个文件。其中,Dockerfile为Docker提供了构建项目时的一些配置信息,包括端口号、环境变量、包管理工具等;而.dockerignore的作用类似于.gitignore,用于构建镜像时忽略掉一些文件。以下是个人项目的Dockerfile以及.dockerignore:
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# 检测项目中使用的包管理工具,并进行依赖下载
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
# 检测项目中的配置文件,并执行打包
RUN \
if [ -f yarn.lock ]; then yarn run build:prod; \
elif [ -f package-lock.json ]; then npm run build:prod; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build:prod; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
# 环境变量
ENV NODE_ENV production
ENV NEXT_PUBLIC_MAP_KEY xxxxxxxxxxxxxxxxxxxxxxx
ENV NEXT_PUBLIC_BASE_URL https://yourdomain.com/
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
完成这两个文件之后,之后便可以对项目进行镜像构建、推送等操作。在此之前,需要事先准备一台服务器。
阿里云ECS服务器
- 本项目部署使用的是阿里云提供的ECS云服务。首先需要购买服务器,这里选用的是2g2核的服务器,对于小型项目来说完全够用。在”镜像“一栏可以选择服务器的操作系统,这里默认选择Alibaba Cloud Linux,下面还可以选择预装宝塔面板。
- 购买之后,可以在控制面板左侧的“实例”处找到我们购买的服务器,这里可以选择实例右侧的远程连接登陆到服务器上。
这里需要注意的是,登陆服务器需要默认用户名root和密码,如果是第一次登陆,需要先重置密码。
登陆之后看到命令行界面,则说明登陆成功~
- 为了将镜像拉取到服务器中,我们这里使用阿里云的容器镜像服务ACR,在这里注册使用个人版来对镜像进行托管。
进入到实例中后,需要创建命名空间以及镜像仓库,方便后续推送镜像。
关于该服务的使用可以参考阿里云给出的文档,这里就不再详述。
Nginx安装
在部署后期,可以通过配置Nginx来完成项目的负载均衡、域名和证书绑定、反向代理等需求。在此次项目中,Nginx主要用于绑定域名以及ssl证书的配置。首先我们可以通过宝塔面板的软件商店来安装Nginx,之后可以在 /www/server/nginx 路径找到Nginx,也可以在终端通过 nginx -v 查看版本来校验是否成功安装。
项目镜像构建、推送
第一步:完成服务器以及Docker相关前置工作后,就可以开始项目进行镜像构建了,这里需要在项目文件目录使用docker build命令完成构建。
docker build --platform linux/amd64 -t your-image-name .
这里要注意两点:
- 命令中加入了参数 --platform linux/amd64,表示将镜像构建为amd64架构运行的镜像,按照上述步骤配置的ECS服务器需要这个架构的镜像,如果镜像架构与服务器环境不匹配则可能报错
- 命令最后有一个 . 不能省略,它表示将读取当前目录下的Dockerfile文件中的配置来构建镜像
第二步:完成构建之后,我们要为镜像指定一个新的标签tag。这里要用到docker tag指令。
docker tag [镜像id] [仓库地址]/[命名空间名称]/[仓库名称]:[tag标签名]
这里的镜像id可以通过 docker images 来查看,输入命令后可以看到第一步构建的镜像的所属本地仓库、标签、镜像id、构建时间等信息,后面的仓库地址、命名空间名称、仓库名称可以在容器镜像服务ACR的仓库基本信息中找到,最后需要自定义tag标签名。
第三步:在对镜像进行标签处理之后,就可以将镜像推送至阿里云的镜像仓库中了,但是在推送之前需要先使用下面的 docker login 命令登陆到远程镜像仓库,密码为开通服务时设置的密码。完整指令可以按照第二步操作中的位置找到你自己远程仓库的基本信息后直接复制即可。
docker login --username=[阿里云账号全名] [仓库地址]
第四步:登陆远程仓库之后完成镜像推送即可。这里使用docker push命令。推送之后,我们可以在容器镜像服务ACR中的镜像仓库中找到刚推送的镜像。
docker push [仓库地址]/[命名空间名称]/[仓库名称]:[tag标签名]
服务器镜像拉取、部署
完成镜像推送后,需要远程连接登陆至服务器端,通过 docker pull 完成镜像拉取,将仓库中的镜像拉取到服务器中。这里要注意服务器可能需要事先安装Docker。
docker pull [仓库地址]/[命名空间]/[仓库名称]:[tag标签名]
最后,在服务器端使用 docker run 命令运行项目即可完成部署,完成部署之后可以通过服务器的公网ip以及项目中设置的端口号进行访问~
docker run -d -p 3000:3000 --name [容器名称] [镜像id]
(UI有待优化...)
这里要注意的是,如果通过ip和端口号访问不到的话,很有可能是端口没有在安全组中放行。放行步骤如下:
-
在“实例“中点击实例名称,进入到下面所示的详情界面
-
在“安全组“中点击右侧的”管理规则”
-
检查“入方向“中是否已经放行项目的端口,如本项目使用的3000端口,如果没有则需要点击”手动添加”
- 手动放行端口
域名以及ssl证书配置
完成上述步骤后,我们已经可以通过ip和端口号对项目进行访问。这时,如果你已经购买域名且想要通过域名访问项目就需要配置前置工作中提到的Nginx服务器。在 /www/server/nginx/conf/nginx.conf 文件中我们可以对服务器做全局配置,包括项目的域名、ssl证书、协议、日志、事件驱动等配置项。这里给出一个示例文件:
user www www;
worker_processes auto;
error_log /www/wwwlogs/nginx_error.log crit;
pid /www/server/nginx/logs/nginx.pid;
worker_rlimit_nofile 51200;
stream {
log_format tcp_format '$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time';
access_log /www/wwwlogs/tcp-access.log tcp_format;
error_log /www/wwwlogs/tcp-error.log;
include /www/server/panel/vhost/nginx/tcp/*.conf;
}
events
{
use epoll;
worker_connections 51200;
multi_accept on;
}
http
{
include mime.types;
#include luawaf.conf;
include proxy.conf;
lua_package_path "/www/server/nginx/lib/lua/?.lua;;";
default_type application/octet-stream;
server_names_hash_bucket_size 512;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
client_max_body_size 50m;
sendfile on;
tcp_nopush on;
keepalive_timeout 60;
tcp_nodelay on;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 64k;
fastcgi_buffers 4 64k;
fastcgi_busy_buffers_size 128k;
fastcgi_temp_file_write_size 256k;
fastcgi_intercept_errors on;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/json image/jpeg image/gif image/png font/ttf font/otf image/svg+xml application/xml+rss text/x-js;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server_tokens off;
access_log off;
server
{
listen 888;
server_name phpmyadmin;
index index.html index.htm index.php;
root /www/server/phpmyadmin;
#error_page 404 /404.html;
include enable-php.conf;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d;
}
location ~ .*\.(js|css)?$
{
expires 12h;
}
location ~ /\.
{
deny all;
}
access_log /www/wwwlogs/access.log;
}
server {
#配置HTTPS的默认访问端口为443。
#如果未在此处配置HTTPS的默认访问端口,可能会造成Nginx无法启动。
#如果您使用Nginx 1.15.0及以上版本,请使用listen 443 ssl代替listen 443和ssl on。
# 默认HTTP 1.1
# listen 443 ssl;
# 开启HTTP2
listen 443 ssl http2;
#填写证书绑定的域名
server_name yingdaxinyuan.com;
#填写证书文件名称(在所示目录提前准备好文件)
ssl_certificate /www/server/nginx/cert/yingdaxinyuan.com.pem;
#填写证书私钥文件名称(在所示目录提前准备好文件)
ssl_certificate_key /www/server/nginx/cert/yingdaxinyuan.com.key;
ssl_session_timeout 5m;
#表示使用的加密套件的类型
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
#表示使用的TLS协议的类型,您需要自行评估是否配置TLSv1.1协议。
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 60;
proxy_connect_timeout 60;
proxy_redirect off;
# Allow the use of websockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
##
# HTTP配置
##
server {
listen 80;
#填写证书绑定的域名
server_name yingdaxinyuan.com;
#将所有HTTP请求通过rewrite指令重定向到HTTPS。
rewrite ^(.*)$ https://$host$1;
}
include /www/server/panel/vhost/nginx/*.conf;
}
从配置文件中可以看到,我们主要在 server 选项中添加了 server_name 来对域名进行配置,用 ssl_certificate 和 ssl_certificate_key 配置域名的ssl证书以及证书key(这里要先在配置文件中的所示目录新建文件并保存证书相应信息,具体内容由域名提供商提供),通过 listen 配置端口监听,最后在 location / 中配置被代理server的协议和地址。
这里要注意,完成对nginx.conf文件的修改后,我们首先要在服务器端使用 nginx -t 命令来校验刚修改的配置文件是否正确,如果修改没有问题命令行则会返回如下结果:
最后,通过 nginx -s reload 重载Nginx服务器即可使配置生效。在浏览器中输入刚才配置好的域名来访问项目~
双击地址栏可以发现协议为https,说明ssl证书配置生效,这里要注意同样需要在安全组的规则中放行443端口才可以进行访问。