build: add container and deployment configuration
This commit is contained in:
1 parent
50039353ad
commit
c41b1d350a
26 files changed
+1238
No files matched your search
@@ -0,0 +1,11 @@
|
||||
.git
|
||||
.codegraph
|
||||
.superpowers
|
||||
.worktrees
|
||||
.tmp
|
||||
.playwright-mcp
|
||||
.env
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/*_test.go
|
||||
**/testdata
|
||||
@@ -0,0 +1,38 @@
|
||||
# Alisten 相关
|
||||
ALISTEN_HOST=127.0.0.1 # 绑定的 ip
|
||||
ALISTEN_PORT=9876 # 监听的端口
|
||||
ALISTEN_ALLOWED_ORIGINS="" # 逗号分隔的 Origin 允许列表,留空时仅允许 ALISTEN_PUBLIC_URL
|
||||
ALISTEN_LOG_LEVEL=info # 日志等级, 可选值为 debug、info、warn、error
|
||||
ALISTEN_WORKER_TMP_SIZE="1g" # Worker 临时文件大小
|
||||
ALISTEN_ADMIN_TOKEN="" # 进入管理后台需要的凭证, 留空则禁用后台
|
||||
|
||||
# 公网访问相关
|
||||
ACME_EMAIL="" # Let's Encrypt 邮箱
|
||||
ALISTEN_DOMAIN="" # Alisten 域名
|
||||
STORAGE_DOMAIN="" # 对象存储服务域名
|
||||
ALISTEN_PUBLIC_URL="https://${ALISTEN_DOMAIN}" # 公网访问 URL
|
||||
|
||||
# 数据库相关
|
||||
DATABASE_HOST=postgres # 数据库主机
|
||||
DATABASE_PORT=5432 # 数据库端口
|
||||
DATABASE_USERNAME=alisten # 数据库用户名
|
||||
DATABASE_PASSWORD="" # 数据库密码
|
||||
DATABASE_NAME=alisten # 数据库名称
|
||||
DATABASE_SSLMODE=prefer # SSL 模式
|
||||
|
||||
# redis 相关
|
||||
REDIS_HOST=redis # redis 主机
|
||||
REDIS_PORT=6379 # redis 端口
|
||||
REDIS_PASSWORD="" # redis 密码
|
||||
REDIS_DB=0 # redis 数据库
|
||||
|
||||
# 对象存储相关
|
||||
STORAGE_ENDPOINT_URL=http://rustfs:9000 # AWS S3 兼容的对象存储服务端点
|
||||
STORAGE_REGION=us-east-1 # 区域
|
||||
STORAGE_BUCKET_NAME=alisten # 存储桶名称
|
||||
STORAGE_ACCESS_KEY_ID="" # 访问密钥 ID
|
||||
STORAGE_SECRET_ACCESS_KEY="" # 秘密访问密钥
|
||||
STORAGE_PUBLIC_URL="" # AWS S3 公开访问地址
|
||||
|
||||
# 可选
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="" # OTLP/HTTP 地址
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS build
|
||||
ARG TARGETOS TARGETARCH
|
||||
ARG VERSION=dev
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
COPY cmd ./cmd
|
||||
COPY db ./db
|
||||
COPY internal ./internal
|
||||
COPY protocol ./protocol
|
||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -tags grpcnotrace \
|
||||
-ldflags "-s -w -X github.com/dihemoe/alisten/internal/bootstrap.version=$VERSION" \
|
||||
-o /out/alisten ./cmd/alisten
|
||||
|
||||
FROM gcr.io/distroless/static-debian13:nonroot AS alisten
|
||||
COPY LICENSE /usr/share/doc/alisten/
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=build /out/alisten /usr/local/bin/alisten
|
||||
USER 65532:65532
|
||||
EXPOSE 8080 9090
|
||||
HEALTHCHECK --start-period=30s --start-interval=1s --interval=10s --timeout=3s --retries=3 \
|
||||
CMD ["/usr/local/bin/alisten", "healthcheck"]
|
||||
ENTRYPOINT ["/usr/local/bin/alisten"]
|
||||
CMD ["serve"]
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
migrate:
|
||||
pull_policy: never
|
||||
|
||||
serve:
|
||||
pull_policy: never
|
||||
build:
|
||||
context: .
|
||||
|
||||
worker:
|
||||
pull_policy: never
|
||||
build:
|
||||
context: .
|
||||
dockerfile: worker.Dockerfile
|
||||
|
||||
web:
|
||||
pull_policy: never
|
||||
build:
|
||||
context: .
|
||||
dockerfile: web/Dockerfile
|
||||
@@ -0,0 +1,107 @@
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.7.16
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --providers.docker.network=proxy
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||
- --entrypoints.web.http.redirections.entrypoint.permanent=true
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --entrypoints.websecure.http.tls.certresolver=letsencrypt
|
||||
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
|
||||
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
||||
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
|
||||
- --ping=true
|
||||
- --log.format=json
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- letsencrypt:/letsencrypt
|
||||
networks: [proxy]
|
||||
healthcheck:
|
||||
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
||||
start_period: 30s
|
||||
start_interval: 1s
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
migrate:
|
||||
networks:
|
||||
- db
|
||||
- backend
|
||||
|
||||
serve:
|
||||
environment:
|
||||
ALISTEN_PUBLIC_URL: https://${ALISTEN_DOMAIN}
|
||||
STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL}
|
||||
networks:
|
||||
- backend
|
||||
- db
|
||||
- proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.services.serve.loadbalancer.server.port=8080
|
||||
- traefik.http.routers.serve.rule=Host(`${ALISTEN_DOMAIN}`) && (PathPrefix(`/api/`) || Path(`/ws`) || PathPrefix(`/media/`))
|
||||
- traefik.http.routers.serve.entrypoints=websecure
|
||||
- traefik.http.routers.serve.service=serve
|
||||
- traefik.http.services.serve.loadbalancer.healthcheck.path=/readyz
|
||||
- traefik.http.services.serve.loadbalancer.healthcheck.port=9090
|
||||
- traefik.http.services.serve.loadbalancer.healthcheck.interval=1s
|
||||
- traefik.http.services.serve.loadbalancer.healthcheck.timeout=1s
|
||||
|
||||
worker:
|
||||
networks:
|
||||
- backend
|
||||
- db
|
||||
|
||||
web:
|
||||
ports: !reset []
|
||||
environment: !reset {}
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.web.rule=Host(`${ALISTEN_DOMAIN}`)
|
||||
- traefik.http.routers.web.entrypoints=websecure
|
||||
- traefik.http.services.web.loadbalancer.server.port=8080
|
||||
|
||||
postgres:
|
||||
networks:
|
||||
- db
|
||||
|
||||
redis:
|
||||
networks:
|
||||
- db
|
||||
|
||||
rustfs:
|
||||
networks:
|
||||
- proxy
|
||||
- backend
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.storage.rule=Host(`${STORAGE_DOMAIN}`) && PathPrefix(`/${STORAGE_BUCKET_NAME}/link/`) && (Method(`GET`) || Method(`HEAD`))
|
||||
- traefik.http.routers.storage.entrypoints=websecure
|
||||
- traefik.http.routers.storage.middlewares=storage-headers
|
||||
- traefik.http.middlewares.storage-headers.headers.contentsecuritypolicy=sandbox; default-src 'none'
|
||||
- traefik.http.middlewares.storage-headers.headers.contenttypenosniff=true
|
||||
- traefik.http.services.storage.loadbalancer.server.port=9000
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
name: proxy
|
||||
backend:
|
||||
name: alisten-backend
|
||||
db:
|
||||
name: alisten-db
|
||||
|
||||
volumes:
|
||||
letsencrypt:
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
x-alisten-env: &alisten-env
|
||||
ALISTEN_LOG_LEVEL: ${ALISTEN_LOG_LEVEL}
|
||||
DATABASE_HOST: ${DATABASE_HOST}
|
||||
DATABASE_PORT: ${DATABASE_PORT}
|
||||
DATABASE_NAME: ${DATABASE_NAME}
|
||||
DATABASE_USERNAME: ${DATABASE_USERNAME}
|
||||
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
|
||||
DATABASE_SSLMODE: ${DATABASE_SSLMODE}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
REDIS_PORT: ${REDIS_PORT}
|
||||
REDIS_DB: ${REDIS_DB}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
STORAGE_ENDPOINT_URL: ${STORAGE_ENDPOINT_URL}
|
||||
STORAGE_REGION: ${STORAGE_REGION}
|
||||
STORAGE_BUCKET_NAME: ${STORAGE_BUCKET_NAME}
|
||||
STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID}
|
||||
STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT}
|
||||
|
||||
services:
|
||||
migrate:
|
||||
image: ghcr.io/dihemoe/alisten/server:latest
|
||||
command: migrate
|
||||
environment: *alisten-env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
serve:
|
||||
image: ghcr.io/dihemoe/alisten/server:latest
|
||||
command: serve
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
<<: *alisten-env
|
||||
ALISTEN_PUBLIC_URL: ${ALISTEN_PUBLIC_URL}
|
||||
ALISTEN_ALLOWED_ORIGINS: ${ALISTEN_ALLOWED_ORIGINS}
|
||||
ALISTEN_ADMIN_TOKEN: ${ALISTEN_ADMIN_TOKEN}
|
||||
STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL}
|
||||
GOMAXPROCS: "2"
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
deploy:
|
||||
replicas: 2
|
||||
stop_grace_period: 30s
|
||||
|
||||
worker:
|
||||
image: ghcr.io/dihemoe/alisten/worker:latest
|
||||
command: worker
|
||||
restart: unless-stopped
|
||||
environment: *alisten-env
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
tmpfs:
|
||||
- /tmp:size=${ALISTEN_WORKER_TMP_SIZE}
|
||||
volumes:
|
||||
- worker-cache:/var/cache/alisten
|
||||
stop_grace_period: 30s
|
||||
|
||||
web:
|
||||
image: ghcr.io/dihemoe/alisten/web:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${ALISTEN_HOST}:${ALISTEN_PORT}:8080"
|
||||
tmpfs:
|
||||
- /tmp:size=64m
|
||||
environment:
|
||||
NGINX_ENVSUBST_TEMPLATE_DIR: /etc/nginx/alisten-templates
|
||||
NGINX_ENVSUBST_OUTPUT_DIR: /tmp
|
||||
STORAGE_BUCKET_NAME: ${STORAGE_BUCKET_NAME}
|
||||
|
||||
postgres:
|
||||
image: postgres:18-trixie
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "--host=127.0.0.1", "--username=${DATABASE_USERNAME}", "--dbname=${DATABASE_NAME}"]
|
||||
start_period: 30s
|
||||
start_interval: 1s
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
redis:
|
||||
image: valkey/valkey:8-alpine
|
||||
restart: unless-stopped
|
||||
command: ["valkey-server", "--save", "", "--appendonly", "no", "--requirepass", "${REDIS_PASSWORD}"]
|
||||
environment:
|
||||
REDISCLI_AUTH: ${REDIS_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD", "valkey-cli", "ping"]
|
||||
start_period: 30s
|
||||
start_interval: 1s
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.3
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RUSTFS_ACCESS_KEY: ${STORAGE_ACCESS_KEY_ID}
|
||||
RUSTFS_SECRET_KEY: ${STORAGE_SECRET_ACCESS_KEY}
|
||||
RUSTFS_CONSOLE_ENABLE: "false"
|
||||
RUSTFS_OBS_LOG_DIRECTORY: ""
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--fail", "--silent", "--output", "/dev/null", "http://127.0.0.1:9000/health/ready"]
|
||||
start_period: 30s
|
||||
start_interval: 1s
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
rustfs-data:
|
||||
worker-cache:
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: alisten
|
||||
description: Alisten 的服务端、yt-dlp worker 和前端
|
||||
type: application
|
||||
kubeVersion: ">=1.30.0-0"
|
||||
version: 0.0.0
|
||||
appVersion: latest
|
||||
@@ -0,0 +1,10 @@
|
||||
Alisten 部署在 https://{{ .Values.domain }}。
|
||||
{{- if .Values.existingSecret }}
|
||||
|
||||
后台需要 Secret {{ .Values.existingSecret }} 中有 ALISTEN_ADMIN_TOKEN。修改这个 Secret 之后重启 serve:
|
||||
kubectl --namespace {{ .Release.Namespace }} rollout restart deployment/{{ include "alisten.fullname" . }}-serve
|
||||
{{- else if not .Values.secrets.adminToken }}
|
||||
|
||||
没有设置管理 token,后台不可用。设置之后 serve 会重建:
|
||||
helm upgrade {{ .Release.Name }} helm/alisten --namespace {{ .Release.Namespace }} --reuse-values --set secrets.adminToken="$(openssl rand -hex 32)"
|
||||
{{- end }}
|
||||
@@ -0,0 +1,103 @@
|
||||
{{/* 资源名后面还要加上 -worker 之类的组件后缀,因此留出 8 个字符。 */}}
|
||||
{{- define "alisten.fullname" -}}
|
||||
{{- if contains .Chart.Name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 55 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 55 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .root.Chart.Name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
app.kubernetes.io/component: {{ .component }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.image" -}}
|
||||
{{- printf "%s:%s" .image.repository (.image.tag | default .root.Chart.AppVersion) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.secretName" -}}
|
||||
{{- .Values.existingSecret | default (include "alisten.fullname" .) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.domain" -}}
|
||||
{{- $domain := required "domain 不能为空" .Values.domain -}}
|
||||
{{- if not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$" $domain) -}}
|
||||
{{- fail "domain 必须是小写的主机名,不带协议、端口和路径" -}}
|
||||
{{- end -}}
|
||||
{{- $domain -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.secretEnv" -}}
|
||||
- name: {{ .name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "alisten.secretName" .root }}
|
||||
key: {{ .name }}
|
||||
{{- if .optional }}
|
||||
optional: true
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.databaseEnv" -}}
|
||||
- name: DATABASE_HOST
|
||||
value: {{ required "database.host 不能为空" .Values.database.host | quote }}
|
||||
- name: DATABASE_PORT
|
||||
value: {{ .Values.database.port | quote }}
|
||||
- name: DATABASE_USERNAME
|
||||
value: {{ required "database.username 不能为空" .Values.database.username | quote }}
|
||||
{{ include "alisten.secretEnv" (dict "root" . "name" "DATABASE_PASSWORD") }}
|
||||
- name: DATABASE_NAME
|
||||
value: {{ required "database.name 不能为空" .Values.database.name | quote }}
|
||||
- name: DATABASE_SSLMODE
|
||||
value: {{ .Values.database.sslmode | quote }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.redisEnv" -}}
|
||||
- name: REDIS_HOST
|
||||
value: {{ required "redis.host 不能为空" .Values.redis.host | quote }}
|
||||
- name: REDIS_PORT
|
||||
value: {{ .Values.redis.port | quote }}
|
||||
{{ include "alisten.secretEnv" (dict "root" . "name" "REDIS_PASSWORD" "optional" true) }}
|
||||
- name: REDIS_DB
|
||||
value: {{ .Values.redis.db | quote }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.storageEnv" -}}
|
||||
- name: STORAGE_ENDPOINT_URL
|
||||
value: {{ required "storage.endpointUrl 不能为空" .Values.storage.endpointUrl | quote }}
|
||||
- name: STORAGE_REGION
|
||||
value: {{ .Values.storage.region | quote }}
|
||||
- name: STORAGE_BUCKET_NAME
|
||||
value: {{ .Values.storage.bucketName | quote }}
|
||||
{{ include "alisten.secretEnv" (dict "root" . "name" "STORAGE_ACCESS_KEY_ID") }}
|
||||
{{ include "alisten.secretEnv" (dict "root" . "name" "STORAGE_SECRET_ACCESS_KEY") }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.podSecurityContext" -}}
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.containerSecurityContext" -}}
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
{{- end -}}
|
||||
|
||||
{{- define "alisten.secretChecksum" -}}
|
||||
{{- if not .Values.existingSecret -}}
|
||||
checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,23 @@
|
||||
{{- if .Values.serve.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}-serve
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: serve
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "alisten.fullname" . }}-serve
|
||||
minReplicas: {{ .Values.serve.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.serve.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.serve.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,38 @@
|
||||
{{- if .Values.httpRoute.enabled }}
|
||||
{{- if not .Values.httpRoute.parentRefs }}
|
||||
{{- fail "启用 httpRoute 时要设置 httpRoute.parentRefs" }}
|
||||
{{- end }}
|
||||
{{- $fullname := include "alisten.fullname" . }}
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ $fullname }}
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- toYaml .Values.httpRoute.parentRefs | nindent 4 }}
|
||||
hostnames:
|
||||
- {{ include "alisten.domain" . | quote }}
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /api
|
||||
- path:
|
||||
type: Exact
|
||||
value: /ws
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /media
|
||||
backendRefs:
|
||||
- name: {{ $fullname }}-serve
|
||||
port: 8080
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: {{ $fullname }}-web
|
||||
port: 8080
|
||||
{{- end }}
|
||||
@@ -0,0 +1,55 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $fullname := include "alisten.fullname" . }}
|
||||
{{- $host := include "alisten.domain" . }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullname }}
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.tlsSecretName }}
|
||||
tls:
|
||||
- hosts: [{{ $host | quote }}]
|
||||
secretName: {{ . }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ $host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullname }}-serve
|
||||
port:
|
||||
name: http
|
||||
- path: /ws
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullname }}-serve
|
||||
port:
|
||||
name: http
|
||||
- path: /media
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullname }}-serve
|
||||
port:
|
||||
name: http
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullname }}-web
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
@@ -0,0 +1,48 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
{{- $v4 := list }}
|
||||
{{- $v6 := list }}
|
||||
{{- range .Values.networkPolicy.privateCIDRs }}
|
||||
{{- if contains ":" . }}
|
||||
{{- $v6 = append $v6 . }}
|
||||
{{- else }}
|
||||
{{- $v4 = append $v4 . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}-worker
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: worker
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "alisten.selectorLabels" (dict "root" . "component" "worker") | nindent 6 }}
|
||||
policyTypes: [Egress]
|
||||
egress:
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
{{- toYaml $v4 | nindent 14 }}
|
||||
- ipBlock:
|
||||
cidr: ::/0
|
||||
except:
|
||||
{{- toYaml $v6 | nindent 14 }}
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- port: 53
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
{{- with .Values.networkPolicy.egress }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{- $replicas := ternary .Values.serve.autoscaling.minReplicas .Values.serve.replicas .Values.serve.autoscaling.enabled }}
|
||||
{{- /* 只有一个副本时,至少保留 1 个可用副本会让节点 drain 永远无法完成。 */}}
|
||||
{{- if ge (int $replicas) 2 }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}-serve
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: serve
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "alisten.selectorLabels" (dict "root" . "component" "serve") | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if not .Values.existingSecret }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
DATABASE_PASSWORD: {{ required "secrets.databasePassword 不能为空,或者设置 existingSecret" .Values.secrets.databasePassword | quote }}
|
||||
STORAGE_ACCESS_KEY_ID: {{ required "secrets.storageAccessKeyId 不能为空,或者设置 existingSecret" .Values.secrets.storageAccessKeyId | quote }}
|
||||
STORAGE_SECRET_ACCESS_KEY: {{ required "secrets.storageSecretAccessKey 不能为空,或者设置 existingSecret" .Values.secrets.storageSecretAccessKey | quote }}
|
||||
{{- with .Values.secrets.redisPassword }}
|
||||
REDIS_PASSWORD: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.secrets.adminToken }}
|
||||
ALISTEN_ADMIN_TOKEN: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,95 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}-serve
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: serve
|
||||
spec:
|
||||
{{- if not .Values.serve.autoscaling.enabled }}
|
||||
replicas: {{ .Values.serve.replicas }}
|
||||
{{- end }}
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "alisten.selectorLabels" (dict "root" . "component" "serve") | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: serve
|
||||
{{- with include "alisten.secretChecksum" . }}
|
||||
annotations:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "alisten.podSecurityContext" . | nindent 8 }}
|
||||
# serve 退出最长需要 27 秒。
|
||||
terminationGracePeriodSeconds: 30
|
||||
initContainers:
|
||||
- name: migrate
|
||||
image: {{ include "alisten.image" (dict "root" . "image" .Values.serve.image) }}
|
||||
imagePullPolicy: {{ .Values.imagePullPolicy }}
|
||||
args: [migrate]
|
||||
env:
|
||||
{{- include "alisten.databaseEnv" . | nindent 12 }}
|
||||
{{- include "alisten.storageEnv" . | nindent 12 }}
|
||||
- name: ALISTEN_LOG_LEVEL
|
||||
value: {{ .Values.logLevel | quote }}
|
||||
securityContext:
|
||||
{{- include "alisten.containerSecurityContext" . | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.serve.resources | nindent 12 }}
|
||||
containers:
|
||||
- name: serve
|
||||
image: {{ include "alisten.image" (dict "root" . "image" .Values.serve.image) }}
|
||||
imagePullPolicy: {{ .Values.imagePullPolicy }}
|
||||
args: [serve]
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
- name: ops
|
||||
containerPort: 9090
|
||||
env:
|
||||
- name: ALISTEN_PUBLIC_URL
|
||||
value: {{ printf "https://%s" (include "alisten.domain" .) | quote }}
|
||||
- name: ALISTEN_ALLOWED_ORIGINS
|
||||
value: {{ .Values.allowedOrigins | quote }}
|
||||
- name: STORAGE_PUBLIC_URL
|
||||
value: {{ required "storage.publicUrl 不能为空" .Values.storage.publicUrl | quote }}
|
||||
{{- include "alisten.databaseEnv" . | nindent 12 }}
|
||||
{{- include "alisten.redisEnv" . | nindent 12 }}
|
||||
{{- include "alisten.storageEnv" . | nindent 12 }}
|
||||
{{- include "alisten.secretEnv" (dict "root" . "name" "ALISTEN_ADMIN_TOKEN" "optional" true) | nindent 12 }}
|
||||
- name: ALISTEN_LOG_LEVEL
|
||||
value: {{ .Values.logLevel | quote }}
|
||||
{{- with .Values.otlpEndpoint }}
|
||||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: ops
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: ops
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
securityContext:
|
||||
{{- include "alisten.containerSecurityContext" . | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.serve.resources | nindent 12 }}
|
||||
@@ -0,0 +1,24 @@
|
||||
{{- if .Values.serviceMonitor.enabled }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceMonitor.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/component
|
||||
operator: In
|
||||
values: [serve, worker]
|
||||
endpoints:
|
||||
- port: ops
|
||||
path: /metrics
|
||||
interval: {{ .Values.serviceMonitor.interval }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,38 @@
|
||||
{{- range $component := list "serve" "web" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" $ }}-{{ $component }}
|
||||
labels:
|
||||
{{- include "alisten.labels" $ | nindent 4 }}
|
||||
app.kubernetes.io/component: {{ $component }}
|
||||
spec:
|
||||
selector:
|
||||
{{- include "alisten.selectorLabels" (dict "root" $ "component" $component) | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
- name: ops
|
||||
port: 9090
|
||||
targetPort: ops
|
||||
{{- end }}
|
||||
{{- if .Values.serviceMonitor.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}-worker
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: worker
|
||||
spec:
|
||||
clusterIP: None
|
||||
selector:
|
||||
{{- include "alisten.selectorLabels" (dict "root" . "component" "worker") | nindent 4 }}
|
||||
ports:
|
||||
- name: ops
|
||||
port: 9090
|
||||
targetPort: ops
|
||||
{{- end }}
|
||||
@@ -0,0 +1,64 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}-web
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: web
|
||||
spec:
|
||||
replicas: {{ .Values.web.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "alisten.selectorLabels" (dict "root" . "component" "web") | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: web
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "alisten.podSecurityContext" . | nindent 8 }}
|
||||
containers:
|
||||
- name: web
|
||||
image: {{ include "alisten.image" (dict "root" . "image" .Values.web.image) }}
|
||||
imagePullPolicy: {{ .Values.imagePullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
- name: ops
|
||||
containerPort: 9090
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: ops
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: ops
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
lifecycle:
|
||||
preStop:
|
||||
sleep:
|
||||
seconds: 5
|
||||
securityContext:
|
||||
{{- include "alisten.containerSecurityContext" . | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.web.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 64Mi
|
||||
@@ -0,0 +1,81 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "alisten.fullname" . }}-worker
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: worker
|
||||
spec:
|
||||
replicas: {{ .Values.worker.replicas }}
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "alisten.selectorLabels" (dict "root" . "component" "worker") | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "alisten.labels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: worker
|
||||
{{- with include "alisten.secretChecksum" . }}
|
||||
annotations:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "alisten.podSecurityContext" . | nindent 8 }}
|
||||
# worker 退出最长需要 27 秒。
|
||||
terminationGracePeriodSeconds: 30
|
||||
containers:
|
||||
- name: worker
|
||||
image: {{ include "alisten.image" (dict "root" . "image" .Values.worker.image) }}
|
||||
imagePullPolicy: {{ .Values.imagePullPolicy }}
|
||||
args: [worker]
|
||||
ports:
|
||||
- name: ops
|
||||
containerPort: 9090
|
||||
env:
|
||||
{{- include "alisten.databaseEnv" . | nindent 12 }}
|
||||
{{- include "alisten.redisEnv" . | nindent 12 }}
|
||||
{{- include "alisten.storageEnv" . | nindent 12 }}
|
||||
- name: ALISTEN_LOG_LEVEL
|
||||
value: {{ .Values.logLevel | quote }}
|
||||
{{- with .Values.otlpEndpoint }}
|
||||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: ops
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: ops
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
securityContext:
|
||||
{{- include "alisten.containerSecurityContext" . | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.worker.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: cache
|
||||
mountPath: /var/cache/alisten
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: {{ .Values.worker.tmpSizeLimit | quote }}
|
||||
- name: cache
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,96 @@
|
||||
domain: ""
|
||||
allowedOrigins: ""
|
||||
logLevel: info
|
||||
otlpEndpoint: ""
|
||||
database:
|
||||
host: ""
|
||||
port: 5432
|
||||
username: ""
|
||||
name: ""
|
||||
sslmode: prefer
|
||||
redis:
|
||||
host: ""
|
||||
port: 6379
|
||||
db: 0
|
||||
storage:
|
||||
endpointUrl: ""
|
||||
region: us-east-1
|
||||
bucketName: alisten
|
||||
publicUrl: ""
|
||||
existingSecret: ""
|
||||
secrets:
|
||||
databasePassword: ""
|
||||
redisPassword: ""
|
||||
storageAccessKeyId: ""
|
||||
storageSecretAccessKey: ""
|
||||
adminToken: ""
|
||||
|
||||
serve:
|
||||
image:
|
||||
repository: ghcr.io/dihemoe/alisten/server
|
||||
tag: ""
|
||||
replicas: 2
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 2
|
||||
maxReplicas: 4
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
worker:
|
||||
image:
|
||||
repository: ghcr.io/dihemoe/alisten/worker
|
||||
tag: ""
|
||||
replicas: 1
|
||||
tmpSizeLimit: 2Gi
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
|
||||
web:
|
||||
image:
|
||||
repository: ghcr.io/dihemoe/alisten/web
|
||||
tag: ""
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
|
||||
imagePullPolicy: IfNotPresent
|
||||
imagePullSecrets: []
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: ""
|
||||
annotations: {}
|
||||
tlsSecretName: ""
|
||||
|
||||
httpRoute:
|
||||
enabled: false
|
||||
parentRefs: []
|
||||
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
labels: {}
|
||||
interval: 30s
|
||||
|
||||
networkPolicy:
|
||||
enabled: false
|
||||
egress: []
|
||||
privateCIDRs:
|
||||
- 10.0.0.0/8
|
||||
- 100.64.0.0/10
|
||||
- 169.254.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- fc00::/7
|
||||
- fe80::/10
|
||||
- 64:ff9b::/96
|
||||
- 64:ff9b:1::/48
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM --platform=$BUILDPLATFORM node:24-alpine AS build
|
||||
ARG VERSION=dev
|
||||
WORKDIR /src
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY web/ .
|
||||
RUN npm run build \
|
||||
&& find dist -type f \( -name '*.html' -o -name '*.js' -o -name '*.css' -o -name '*.svg' -o -name '*.json' -o -name '*.webmanifest' \) -exec gzip -9 -k {} + \
|
||||
&& sed "s|@VERSION@|${VERSION}|" nginx/default.conf > /tmp/default.conf
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:1.30-alpine
|
||||
ENV NGINX_ENTRYPOINT_QUIET_LOGS=1
|
||||
COPY web/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=build /tmp/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /src/dist /usr/share/nginx/html
|
||||
COPY web/nginx/templates /etc/nginx/alisten-templates
|
||||
COPY LICENSE /usr/share/doc/alisten/
|
||||
EXPOSE 8080 9090
|
||||
HEALTHCHECK --start-period=30s --start-interval=1s --interval=10s --timeout=3s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://127.0.0.1:9090/healthz || exit 1
|
||||
@@ -0,0 +1,59 @@
|
||||
map $status $log_level {
|
||||
~^5 ERROR;
|
||||
default INFO;
|
||||
}
|
||||
|
||||
log_format json escape=json
|
||||
'{"time":"$time_iso8601","level":"$log_level","msg":"HTTP 请求",'
|
||||
'"service":"alisten","component":"web","version":"@VERSION@",'
|
||||
'"method":"$request_method","path":"$uri","status":$status,'
|
||||
'"bytes":$body_bytes_sent,"duration_seconds":$request_time}';
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
root /usr/share/nginx/html;
|
||||
server_tokens off;
|
||||
access_log /dev/stdout json;
|
||||
|
||||
gzip_static on;
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_types text/css text/javascript application/javascript application/json application/manifest+json image/svg+xml;
|
||||
|
||||
include /tmp/alisten.d/*.conf;
|
||||
|
||||
location ~ ^/(?:admin|settings|history)?$ {
|
||||
add_header Cache-Control "no-cache";
|
||||
try_files /index.html =404;
|
||||
}
|
||||
location ~ ^/r/[^/]+(?:/(?:queue|chat|search|playlists/[^/]+/[^/]+))?$ {
|
||||
add_header Cache-Control "no-cache";
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
location ~ ^/.+/$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
location / {
|
||||
add_header Cache-Control "no-cache";
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 9090;
|
||||
access_log off;
|
||||
|
||||
location = /healthz {
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
worker_processes 1;
|
||||
|
||||
error_log /dev/stderr warn;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
types {
|
||||
application/manifest+json webmanifest;
|
||||
}
|
||||
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# 主机名放在变量中,nginx 才会在每个请求时解析:serve 有两个副本且重启后地址会变,启动时上游也不必已经存在。
|
||||
resolver 127.0.0.11 valid=5s;
|
||||
set $serve serve:8080;
|
||||
set $rustfs rustfs:9000;
|
||||
proxy_http_version 1.1;
|
||||
absolute_redirect off;
|
||||
|
||||
# default.conf 中有匹配以 / 结尾的路径的正则 location,只有带 ^~ 的前缀 location 才能优先于它。
|
||||
location ^~ /api/ {
|
||||
# 不小于 serve 的请求体上限,请求体就不会写入临时文件,也就不会因此输出纯文本的 warn 日志。
|
||||
client_body_buffer_size 64k;
|
||||
proxy_pass http://$serve;
|
||||
}
|
||||
location ^~ /media/ {
|
||||
proxy_pass http://$serve;
|
||||
}
|
||||
# 空闲连接由 serve 断开,这里的超时只作兜底。
|
||||
location = /ws {
|
||||
proxy_pass http://$serve;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 1h;
|
||||
proxy_send_timeout 1h;
|
||||
}
|
||||
# 只转发 link/ 下的读取请求:RustFS 的其他接口也在同一端口上。对象来自用户提交的链接且与页面同源,依靠这两个响应头阻止其中的脚本运行。
|
||||
# 用 return 而不用 limit_except:拒绝时不写纯文本的错误日志。关闭缓冲:缓冲时超出内存缓冲区的响应会写入 /tmp 的 tmpfs。
|
||||
location ^~ /storage/${STORAGE_BUCKET_NAME}/link/ {
|
||||
if ($request_method !~ ^(GET|HEAD)$) {
|
||||
return 405;
|
||||
}
|
||||
rewrite ^/storage(/.*)$ $1 break;
|
||||
proxy_pass http://$rustfs;
|
||||
proxy_buffering off;
|
||||
add_header Content-Security-Policy "sandbox; default-src 'none'" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS build
|
||||
ARG TARGETOS TARGETARCH
|
||||
ARG VERSION=dev
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
COPY cmd ./cmd
|
||||
COPY db ./db
|
||||
COPY internal ./internal
|
||||
COPY protocol ./protocol
|
||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -tags grpcnotrace \
|
||||
-ldflags "-s -w -X github.com/dihemoe/alisten/internal/bootstrap.version=$VERSION" \
|
||||
-o /out/alisten ./cmd/alisten
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS tools
|
||||
ARG TARGETARCH
|
||||
ARG YTDLP_VERSION=2026.10.21
|
||||
ARG DENO_VERSION=v2.9.8
|
||||
RUN case "$TARGETARCH" in amd64) ytdlp=yt-dlp_linux deno=x86_64 ;; arm64) ytdlp=yt-dlp_linux_aarch64 deno=aarch64 ;; esac \
|
||||
&& wget -q --tries=3 --timeout=30 -O /tmp/yt-dlp.zip "https://github.com/yt-dlp/yt-dlp/releases/download/$YTDLP_VERSION/$ytdlp.zip" \
|
||||
&& wget -q --tries=3 --timeout=30 -O /tmp/deno.zip "https://github.com/denoland/deno/releases/download/$DENO_VERSION/deno-$deno-unknown-linux-gnu.zip" \
|
||||
&& mkdir -p /out/opt/yt-dlp /out/usr/local/bin \
|
||||
&& unzip -q /tmp/yt-dlp.zip -d /out/opt/yt-dlp \
|
||||
&& unzip -q /tmp/deno.zip -d /out/usr/local/bin \
|
||||
&& ln -s "/opt/yt-dlp/$ytdlp" /out/usr/local/bin/yt-dlp
|
||||
|
||||
FROM debian:trixie-slim AS alisten-worker
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends ca-certificates ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --system --gid 65532 alisten \
|
||||
&& useradd --system --uid 65532 --gid alisten --home-dir /var/cache/alisten --no-create-home --shell /usr/sbin/nologin alisten \
|
||||
&& install -d -o alisten -g alisten /var/cache/alisten
|
||||
COPY LICENSE /usr/share/doc/alisten/
|
||||
COPY --from=tools /out /
|
||||
COPY --from=build /out/alisten /usr/local/bin/alisten
|
||||
ENV XDG_CACHE_HOME=/var/cache/alisten
|
||||
USER 65532:65532
|
||||
EXPOSE 9090
|
||||
HEALTHCHECK --start-period=30s --start-interval=1s --interval=10s --timeout=3s --retries=3 \
|
||||
CMD ["/usr/local/bin/alisten", "healthcheck"]
|
||||
ENTRYPOINT ["/usr/local/bin/alisten"]
|
||||
CMD ["worker"]
|
||||
Reference in new issue
Block a user