build: add container and deployment configuration

This commit is contained in:
DiheChen committed 2026-10-30 01:34:19 +08:00
1 parent 50039353ad
commit c41b1d350a
26 files changed
+1238

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
.git
.codegraph
.superpowers
.worktrees
.tmp
.playwright-mcp
.env
**/node_modules
**/dist
**/*_test.go
**/testdata
+38
View File
@@ -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
View File
@@ -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"]
+20
View File
@@ -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
+107
View File
@@ -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
View File
@@ -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:
+7
View File
@@ -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
+10
View File
@@ -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 }}
+103
View File
@@ -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 -}}
+23
View File
@@ -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 }}
+38
View File
@@ -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 }}
+55
View File
@@ -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 }}
+48
View File
@@ -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 }}
+16
View File
@@ -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 }}
+19
View File
@@ -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 }}
+95
View File
@@ -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 }}
+38
View File
@@ -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 }}
+64
View File
@@ -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
+81
View File
@@ -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: {}
+96
View File
@@ -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
+20
View File
@@ -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
+59
View File
@@ -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;
}
}
+28
View File
@@ -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;
}
+42
View File
@@ -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"]