add deploy: docker-compose yaml
This commit is contained in:
parent
d7b4ec7f5a
commit
01df0f46d4
@ -4,4 +4,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmRegistryServer: "https://registry.npmjs.org"
|
||||
npmRegistryServer: "https://registry.npmmirror.com"
|
||||
|
||||
2
dashboard/next-env.d.ts
vendored
2
dashboard/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
14
deploy/docker-compose/certbot/conf/live/README
Normal file
14
deploy/docker-compose/certbot/conf/live/README
Normal file
@ -0,0 +1,14 @@
|
||||
This directory contains your keys and certificates.
|
||||
|
||||
`[cert name]/privkey.pem` : the private key for your certificate.
|
||||
`[cert name]/fullchain.pem`: the certificate file used in most server software.
|
||||
`[cert name]/chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
|
||||
`[cert name]/cert.pem` : will break many server configurations, and should not be used
|
||||
without reading further documentation (see link below).
|
||||
|
||||
WARNING: DO NOT MOVE OR RENAME THESE FILES!
|
||||
Certbot expects these files to remain in this location in order
|
||||
to function properly!
|
||||
|
||||
We recommend not moving these files. For more information, see the Certbot
|
||||
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
|
||||
168
deploy/docker-compose/docker-compose.yaml
Normal file
168
deploy/docker-compose/docker-compose.yaml
Normal file
@ -0,0 +1,168 @@
|
||||
services:
|
||||
|
||||
zitadel-external-tls:
|
||||
extends:
|
||||
service: zitadel-init
|
||||
command: 'start-from-setup --masterkey "MasterkeyNeedsToHave32Characters"'
|
||||
environment:
|
||||
ZITADEL_EXTERNALPORT: 443
|
||||
ZITADEL_EXTERNALSECURE: true
|
||||
ZITADEL_TLS_ENABLED: false
|
||||
networks:
|
||||
- app
|
||||
- db
|
||||
depends_on:
|
||||
db:
|
||||
condition: 'service_healthy'
|
||||
zitadel-init:
|
||||
condition: 'service_completed_successfully'
|
||||
|
||||
zitadel-enabled-tls:
|
||||
extends:
|
||||
service: zitadel-init
|
||||
command: 'start-from-setup --masterkey "MasterkeyNeedsToHave32Characters"'
|
||||
environment:
|
||||
ZITADEL_EXTERNALPORT: 443
|
||||
ZITADEL_EXTERNALSECURE: true
|
||||
ZITADEL_TLS_ENABLED: true
|
||||
ZITADEL_TLS_CERTPATH: /etc/letsencrypt/live/auth.svc.plus/fullchain.pem
|
||||
ZITADEL_TLS_KEYPATH: /etc/letsencrypt/live/auth.svc.plus/privkey.pem
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
networks:
|
||||
- app
|
||||
- db
|
||||
depends_on:
|
||||
zitadel-init:
|
||||
condition: 'service_completed_successfully'
|
||||
db:
|
||||
condition: 'service_healthy'
|
||||
|
||||
zitadel-init:
|
||||
image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}'
|
||||
command: 'init'
|
||||
depends_on:
|
||||
db:
|
||||
condition: 'service_healthy'
|
||||
environment:
|
||||
# Using an external domain other than localhost proofs, that the proxy configuration works.
|
||||
# If Zitadel can't resolve a requests original host to this domain,
|
||||
# it will return a 404 Instance not found error.
|
||||
ZITADEL_EXTERNALDOMAIN: auth.svc.plus
|
||||
# In case something doesn't work as expected,
|
||||
# it can be handy to be able to read the access logs.
|
||||
ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: true
|
||||
# For convenience, ZITADEL should not ask to change the initial admin users password.
|
||||
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false
|
||||
# database configuration
|
||||
ZITADEL_DATABASE_POSTGRES_HOST: db
|
||||
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
|
||||
# Set up a service account with IAM_LOGIN_CLIENT role and write the PAT to the file ./login-client.pat
|
||||
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /current-dir/login-client.pat
|
||||
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
|
||||
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Automatically Initialized IAM Login Client
|
||||
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: '2029-01-01T00:00:00Z'
|
||||
# The master key is used to
|
||||
networks:
|
||||
- db
|
||||
healthcheck:
|
||||
test: [ "CMD", "/app/zitadel", "ready" ]
|
||||
interval: '10s'
|
||||
timeout: '5s'
|
||||
retries: 5
|
||||
start_period: '10s'
|
||||
volumes:
|
||||
- '.:/current-dir:rw'
|
||||
|
||||
db:
|
||||
restart: 'always'
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready" ]
|
||||
interval: 5s
|
||||
timeout: 60s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
networks:
|
||||
- db
|
||||
volumes:
|
||||
- 'data:/var/lib/postgresql/data:rw'
|
||||
|
||||
login-external-tls:
|
||||
restart: 'unless-stopped'
|
||||
image: 'ghcr.io/zitadel/zitadel-login:latest'
|
||||
environment:
|
||||
- ZITADEL_API_URL=http://zitadel-external-tls:8080
|
||||
- NEXT_PUBLIC_BASE_PATH=/ui/v2/login
|
||||
- ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
|
||||
- CUSTOM_REQUEST_HEADERS=Host:auth.svc.plus
|
||||
volumes:
|
||||
- '.:/current-dir:ro'
|
||||
networks:
|
||||
- app
|
||||
depends_on:
|
||||
zitadel-external-tls:
|
||||
condition: 'service_healthy'
|
||||
|
||||
login-enabled-tls:
|
||||
restart: 'unless-stopped'
|
||||
image: 'ghcr.io/zitadel/zitadel-login:latest'
|
||||
environment:
|
||||
- ZITADEL_API_URL=https://zitadel-enabled-tls:8080
|
||||
- NEXT_PUBLIC_BASE_PATH=/ui/v2/login
|
||||
- ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
|
||||
- CUSTOM_REQUEST_HEADERS=Host:auth.svc.plus
|
||||
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
volumes:
|
||||
- '.:/current-dir:ro'
|
||||
networks:
|
||||
- app
|
||||
depends_on:
|
||||
zitadel-enabled-tls:
|
||||
condition: 'service_healthy'
|
||||
|
||||
proxy-external-tls:
|
||||
image: nginx:mainline-alpine
|
||||
container_name: proxy-external-tls
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
networks:
|
||||
- app
|
||||
depends_on:
|
||||
zitadel-external-tls:
|
||||
condition: service_healthy
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
container_name: certbot
|
||||
command: >
|
||||
certonly --webroot
|
||||
--webroot-path=/var/www/certbot
|
||||
--email manbuzhe2009@qq.com
|
||||
--agree-tos
|
||||
--no-eff-email
|
||||
-d auth.svc.plus
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
depends_on:
|
||||
proxy-external-tls:
|
||||
condition: service_started
|
||||
networks:
|
||||
- app
|
||||
|
||||
networks:
|
||||
app:
|
||||
db:
|
||||
|
||||
volumes:
|
||||
data:
|
||||
40
deploy/docker-compose/nginx/conf.d/accounts.conf
Normal file
40
deploy/docker-compose/nginx/conf.d/accounts.conf
Normal file
@ -0,0 +1,40 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name accounts.svc.plus;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name accounts.svc.plus;
|
||||
|
||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location ^~ /api/auth/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
|
||||
add_header Access-Control-Allow-Credentials "true" always;
|
||||
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
|
||||
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=None";
|
||||
}
|
||||
}
|
||||
47
deploy/docker-compose/nginx/conf.d/artifact.conf
Normal file
47
deploy/docker-compose/nginx/conf.d/artifact.conf
Normal file
@ -0,0 +1,47 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name dl.svc.plus cn-dl.svc.plus;
|
||||
|
||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
root /data/update-server;
|
||||
index index.html;
|
||||
|
||||
location ^~ /.well-known/ { allow all; }
|
||||
|
||||
# ✅ JSON 专用——放在 / 之前
|
||||
location ~* \.json$ {
|
||||
try_files $uri =404;
|
||||
add_header Cache-Control "public, max-age=60, s-maxage=60, stale-while-revalidate=300";
|
||||
default_type application/json;
|
||||
}
|
||||
|
||||
# 目录浏览
|
||||
location / {
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
autoindex_localtime on;
|
||||
add_header Accept-Ranges bytes;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# 大包直出
|
||||
location ~* \.(?:dmg|zip|tar\.gz|deb|rpm|exe|pkg|appimage|apk|ipa)$ {
|
||||
expires 7d;
|
||||
access_log off;
|
||||
add_header Cache-Control "public";
|
||||
add_header Accept-Ranges bytes;
|
||||
}
|
||||
|
||||
# 隐藏 dotfiles(不拦 /.well-known/)
|
||||
location ~ /\.(?!well-known/)[^/]+ { deny all; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name dl.svc.plus cn-dl.svc.plus;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
136
deploy/docker-compose/nginx/conf.d/homepage.conf
Normal file
136
deploy/docker-compose/nginx/conf.d/homepage.conf
Normal file
@ -0,0 +1,136 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.svc.plus cn-homepage.svc.plus;
|
||||
|
||||
# Certbot HTTP-01 challenge
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# All HTTP → HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name www.svc.plus cn-homepage.svc.plus;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/auth.svc.plus/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/auth.svc.plus/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
# ====== 静态根目录(Next.js export 产物)======
|
||||
root /dashboard/;
|
||||
index index.html;
|
||||
|
||||
# (可选)放行 ACME/健康检查等
|
||||
location ^~ /.well-known/ { allow all; }
|
||||
|
||||
# =======================
|
||||
# API 反向代理(保持原样)
|
||||
# =======================
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# /api/askai 接口限流(保持原样)
|
||||
location = /api/askai {
|
||||
access_by_lua_block {
|
||||
local redis = require "resty.redis"
|
||||
local r = redis:new()
|
||||
r:set_timeout(200)
|
||||
local ok, err = r:connect("127.0.0.1", 6379)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "Redis connect error: ", err)
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
local user = ngx.var.arg_user or ngx.var.remote_addr
|
||||
local today = os.date("%Y%m%d")
|
||||
local key = "limit:user:" .. user .. ":" .. today
|
||||
|
||||
local count, err = r:incr(key)
|
||||
if count == 1 then r:expire(key, 86400) end
|
||||
if count > 200 then
|
||||
ngx.status = 429
|
||||
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
||||
ngx.say("Too Many Requests: daily limit reached")
|
||||
return ngx.exit(429)
|
||||
end
|
||||
}
|
||||
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# =======================
|
||||
# 静态文件直出(替换原先的 Next.js 动态代理)
|
||||
# =======================
|
||||
|
||||
# Next 导出的静态资源(hash 不变 -> 长缓存)
|
||||
location ^~ /_next/static/ {
|
||||
try_files $uri =404;
|
||||
access_log off;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable, max-age=31536000";
|
||||
}
|
||||
|
||||
# 其他常见静态资源:中等缓存
|
||||
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf)$ {
|
||||
try_files $uri =404;
|
||||
access_log off;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
}
|
||||
|
||||
# 主页与已导出的所有路由:按文件/目录匹配
|
||||
# 未命中的交给 404.html(保持静态站语义)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
# 显式处理 404/500 路由目录(Next export 会生成 404/、500/ 与同名 .html)
|
||||
location = /404.html { internal; }
|
||||
error_page 404 /404.html;
|
||||
|
||||
# 如果有 /favicon.ico,则直接给文件
|
||||
location = /favicon.ico {
|
||||
try_files /favicon.ico =204;
|
||||
access_log off;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
# (可选)为某些目录开启目录索引(你有 dl-index、docs、download)
|
||||
# 若需要列表页可以这样做;不需要则删除本段
|
||||
location ^~ /dl-index/ {
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
autoindex_localtime on;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# 拒绝访问隐藏文件(如 .env)
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# (可选)开启 gzip(如启用 ngx_brotli,也可再加 br)
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 1k;
|
||||
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
|
||||
gzip_vary on;
|
||||
}
|
||||
69
deploy/docker-compose/nginx/conf.d/rag-server.conf
Normal file
69
deploy/docker-compose/nginx/conf.d/rag-server.conf
Normal file
@ -0,0 +1,69 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name rag-server.svc.plus api.svc.plus;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name rag-server.svc.plus api.svc.plus;
|
||||
|
||||
ssl_certificate /etc/ssl/svc.plus.pem;
|
||||
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location ^~ /api/ {
|
||||
proxy_pass http://127.0.0.1:8090;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
|
||||
add_header Access-Control-Allow-Credentials "true" always;
|
||||
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
|
||||
location = /api/askai {
|
||||
access_by_lua_block {
|
||||
local redis = require "resty.redis"
|
||||
local r = redis:new()
|
||||
r:set_timeout(200)
|
||||
local ok, err = r:connect("127.0.0.1", 6379)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "Redis connect error: ", err)
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
local user = ngx.var.arg_user or ngx.var.remote_addr
|
||||
local today = os.date("%Y%m%d")
|
||||
local key = "limit:user:" .. user .. ":" .. today
|
||||
|
||||
local count, err = r:incr(key)
|
||||
if count == 1 then r:expire(key, 86400) end
|
||||
if count > 200 then
|
||||
ngx.status = 429
|
||||
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
||||
ngx.say("Too Many Requests: daily limit reached")
|
||||
return ngx.exit(429)
|
||||
end
|
||||
}
|
||||
|
||||
proxy_pass http://127.0.0.1:8090;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
6
deploy/docker-compose/nginx/nginx.conf
Normal file
6
deploy/docker-compose/nginx/nginx.conf
Normal file
@ -0,0 +1,6 @@
|
||||
events {}
|
||||
|
||||
http {
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
||||
45
deploy/docker-compose/run.sh
Normal file
45
deploy/docker-compose/run.sh
Normal file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
POSTGRES_VOL="postgres_data"
|
||||
|
||||
stop_all() {
|
||||
docker compose -f docker-compose.yaml down -v || true
|
||||
}
|
||||
|
||||
wait_db() {
|
||||
until docker exec "$(docker ps -qf 'ancestor=postgres:17-alpine')" \
|
||||
pg_isready -U postgres >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
certbot)
|
||||
docker compose -f docker-compose.yaml run --rm certbot
|
||||
;;
|
||||
init)
|
||||
stop_all
|
||||
docker compose -f docker-compose.yaml run --rm zitadel-init
|
||||
docker compose -f docker-compose.yaml up -d
|
||||
;;
|
||||
|
||||
update)
|
||||
docker compose -f docker-compose.yaml pull
|
||||
docker compose -f docker-compose.yaml up -d
|
||||
;;
|
||||
|
||||
reset)
|
||||
stop_all
|
||||
|
||||
docker volume rm -f "${POSTGRES_VOL}" || true
|
||||
rm -rf ./data && mkdir -p ./data
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {init|update|reset|certbot}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user