This commit is contained in:
Your Name
2026-08-27 14:04:28 +08:00
parent f7720831be
commit 334890171e
3016 changed files with 263403 additions and 27971 deletions
+18
View File
@@ -0,0 +1,18 @@
# Copy to deploy/.env.production and replace every CHANGE_ME value.
APP_DOMAIN=app.example.com
ADMIN_DOMAIN=admin.example.com
API_DOMAIN=api.example.com
MYSQL_DATABASE=im
MYSQL_USER=xingyu_app
MYSQL_PASSWORD=CHANGE_ME_DATABASE_PASSWORD
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
IM_JWT_SECRET=CHANGE_ME_AT_LEAST_64_RANDOM_CHARACTERS
IM_CONFIG_ENCRYPTION_KEY=CHANGE_ME_DIFFERENT_64_RANDOM_CHARACTERS
IM_BOOTSTRAP_ADMIN_USERNAME=admin
IM_BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_12_CHARS_UPPER_LOWER_DIGIT_SPECIAL
IM_BOOTSTRAP_ADMIN_REAL_NAME=平台管理员
# Set blank after the first successful bootstrap if desired.
TZ=Asia/Shanghai
+18
View File
@@ -0,0 +1,18 @@
FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/xingyu-api ./cmd/server
FROM alpine:3.22
RUN apk add --no-cache ca-certificates tzdata \
&& addgroup -S xingyu \
&& adduser -S -G xingyu xingyu \
&& mkdir -p /data/uploads \
&& chown -R xingyu:xingyu /data
WORKDIR /app
COPY --from=builder /out/xingyu-api /app/xingyu-api
USER xingyu
EXPOSE 8888
ENTRYPOINT ["/app/xingyu-api"]
+29
View File
@@ -0,0 +1,29 @@
FROM node:22-alpine AS mobile-builder
WORKDIR /src/mobile
ARG VITE_API_ORIGIN
ARG VITE_WS_ORIGIN
ENV VITE_API_ORIGIN=${VITE_API_ORIGIN}
ENV VITE_WS_ORIGIN=${VITE_WS_ORIGIN}
COPY mobile/package.json mobile/package-lock.json ./
RUN npm ci
COPY mobile/ ./
RUN npm run build:h5
FROM node:22-alpine AS admin-builder
WORKDIR /src/admin
ENV CI=true
RUN corepack enable
COPY admin/package.json admin/pnpm-lock.yaml admin/pnpm-workspace.yaml ./
COPY admin/.npmrc ./
COPY admin/internal ./internal
COPY admin/packages ./packages
COPY admin/scripts ./scripts
COPY admin/apps/web-ele ./apps/web-ele
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @vben/web-ele build
FROM nginx:1.28-alpine
COPY --from=mobile-builder /src/mobile/dist/build/h5 /srv/mobile
COPY --from=admin-builder /src/admin/apps/web-ele/dist /srv/admin
COPY deploy/nginx.conf.template /etc/nginx/templates/default.conf.template
EXPOSE 80 443
+98
View File
@@ -0,0 +1,98 @@
services:
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
TZ: ${TZ:-Asia/Shanghai}
command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=$${MYSQL_PASSWORD} mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} --silent"]
interval: 10s
timeout: 5s
retries: 20
networks: [internal]
migrate:
image: mysql:8.4
restart: "no"
depends_on:
mysql:
condition: service_healthy
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- ../backend/migrations:/migrations:ro
- ./migrate.sh:/migrate.sh:ro
entrypoint: ["sh", "/migrate.sh"]
networks: [internal]
api:
build:
context: ..
dockerfile: deploy/Dockerfile.backend
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
migrate:
condition: service_completed_successfully
environment:
IM_ENV: production
IM_HOST: 0.0.0.0
IM_PORT: 8888
IM_DB_DSN: ${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(mysql:3306)/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local
IM_JWT_SECRET: ${IM_JWT_SECRET}
IM_CONFIG_ENCRYPTION_KEY: ${IM_CONFIG_ENCRYPTION_KEY}
IM_ALLOWED_ORIGINS: https://${APP_DOMAIN},https://${ADMIN_DOMAIN}
IM_SEED_DEMO: "false"
IM_MEDIA_DIR: /data/uploads
IM_BOOTSTRAP_ADMIN_USERNAME: ${IM_BOOTSTRAP_ADMIN_USERNAME}
IM_BOOTSTRAP_ADMIN_PASSWORD: ${IM_BOOTSTRAP_ADMIN_PASSWORD}
IM_BOOTSTRAP_ADMIN_REAL_NAME: ${IM_BOOTSTRAP_ADMIN_REAL_NAME}
TZ: ${TZ:-Asia/Shanghai}
volumes:
- uploads:/data/uploads
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8888/healthz"]
interval: 15s
timeout: 5s
retries: 10
networks: [internal]
edge:
build:
context: ..
dockerfile: deploy/Dockerfile.edge
args:
VITE_API_ORIGIN: https://${API_DOMAIN}
VITE_WS_ORIGIN: wss://${API_DOMAIN}
restart: unless-stopped
depends_on:
api:
condition: service_healthy
environment:
APP_DOMAIN: ${APP_DOMAIN}
ADMIN_DOMAIN: ${ADMIN_DOMAIN}
API_DOMAIN: ${API_DOMAIN}
ports:
- "80:80"
- "443:443"
volumes:
- ./certs:/etc/nginx/certs:ro
networks: [internal]
networks:
internal:
driver: bridge
volumes:
mysql_data:
uploads:
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
set -eu
mysql_args="--host=mysql --user=${MYSQL_USER} --database=${MYSQL_DATABASE} --default-character-set=utf8mb4 --batch --skip-column-names"
until MYSQL_PWD="${MYSQL_PASSWORD}" mysql ${mysql_args} --execute "SELECT 1" >/dev/null 2>&1; do
sleep 2
done
MYSQL_PWD="${MYSQL_PASSWORD}" mysql ${mysql_args} --execute "CREATE TABLE IF NOT EXISTS schema_migrations (version VARCHAR(255) NOT NULL PRIMARY KEY, checksum CHAR(64) NOT NULL, applied_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
for file in /migrations/*.sql; do
version="$(basename "${file}")"
checksum="$(sha256sum "${file}" | awk '{print $1}')"
existing="$(MYSQL_PWD="${MYSQL_PASSWORD}" mysql ${mysql_args} --execute "SELECT checksum FROM schema_migrations WHERE version='${version}'" 2>/dev/null || true)"
if [ -n "${existing}" ]; then
if [ "${existing}" != "${checksum}" ]; then
echo "Migration checksum mismatch: ${version}" >&2
exit 1
fi
echo "Already applied: ${version}"
continue
fi
echo "Applying: ${version}"
MYSQL_PWD="${MYSQL_PASSWORD}" mysql ${mysql_args} < "${file}"
MYSQL_PWD="${MYSQL_PASSWORD}" mysql ${mysql_args} --execute "INSERT INTO schema_migrations(version,checksum) VALUES('${version}','${checksum}')"
done
+85
View File
@@ -0,0 +1,85 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ${APP_DOMAIN} ${ADMIN_DOMAIN} ${API_DOMAIN};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name ${APP_DOMAIN};
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
root /srv/mobile;
index index.html;
server_tokens off;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy "camera=(self), microphone=(self), geolocation=(self)" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: blob: https:; media-src 'self' blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' https://${API_DOMAIN} wss://${API_DOMAIN}; frame-src https:;" always;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:js|css|png|jpg|jpeg|gif|webp|svg|ico|woff2?)$ {
expires 7d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
}
server {
listen 443 ssl http2;
server_name ${ADMIN_DOMAIN};
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
root /srv/admin;
index index.html;
server_tokens off;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header X-Frame-Options DENY always;
location /admin/v1/ {
proxy_pass http://api:8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
location / {
try_files $uri $uri/ /index.html;
}
}
server {
listen 443 ssl http2;
server_name ${API_DOMAIN};
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
server_tokens off;
client_max_body_size 16m;
location /ws {
proxy_pass http://api:8888;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_read_timeout 70s;
}
location / {
proxy_pass http://api:8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
}