diff --git a/backend/src/main/java/com/sshmanager/config/SpaForwardConfig.java b/backend/src/main/java/com/sshmanager/config/SpaForwardConfig.java new file mode 100644 index 0000000..18fedbf --- /dev/null +++ b/backend/src/main/java/com/sshmanager/config/SpaForwardConfig.java @@ -0,0 +1,33 @@ +package com.sshmanager.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.PathResourceResolver; + +import java.io.IOException; + +/** + * SPA 前端路由回退:未匹配到静态资源时返回 index.html,供 Vue Router history 模式使用。 + */ +@Configuration +public class SpaForwardConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**") + .addResourceLocations("classpath:/static/") + .resourceChain(true) + .addResolver(new PathResourceResolver() { + @Override + protected Resource getResource(String path, Resource location) throws IOException { + Resource resource = location.createRelative(path); + if (resource.exists() && resource.isReadable()) { + return resource; + } + return location.createRelative("index.html"); + } + }); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 6f9e2e3..4ac0336 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -2,6 +2,9 @@ server: port: 48080 spring: + web: + resources: + add-mappings: false # 使用 SpaForwardConfig 统一处理静态与 SPA 回退 datasource: url: jdbc:h2:file:./data/sshmanager;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver diff --git a/docker/.npmrc b/docker/.npmrc new file mode 100644 index 0000000..75b8496 --- /dev/null +++ b/docker/.npmrc @@ -0,0 +1,6 @@ +# 使用国内 npm 镜像(npmmirror 淘宝镜像) +registry=https://registry.npmmirror.com +sass_binary_site=https://npmmirror.com/mirrors/node-sass +phantomjs_cdnurl=https://npmmirror.com/mirrors/phantomjs +electron_mirror=https://npmmirror.com/mirrors/electron/ +chromedriver_cdnurl=https://npmmirror.com/mirrors/chromedriver diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..f4ed130 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,48 @@ +# ========== 阶段一:前端构建(国内 npm 源) ========== +FROM node:20-alpine AS frontend + +# 使用国内 npm 镜像(npmmirror) +COPY docker/.npmrc /root/.npmrc +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --prefer-offline --no-audit + +COPY frontend/ ./ +RUN npm run build + +# ========== 阶段二:后端构建(国内 Maven 源) ========== +FROM maven:3.9-eclipse-temurin-8-alpine AS backend + +COPY docker/maven-settings.xml /root/.m2/settings.xml +WORKDIR /build + +# 先复制 pom,利用层缓存 +COPY backend/pom.xml ./ +RUN mvn dependency:go-offline -B -q + +# 复制后端源码 +COPY backend/src ./src + +# 将前端打包结果放入 Spring Boot 静态资源目录 +COPY --from=frontend /app/dist ./src/main/resources/static + +RUN mvn package -DskipTests -B -q + +# ========== 阶段三:运行(单容器,仅 Java) ========== +FROM eclipse-temurin:8-jre-alpine + +RUN apk add --no-cache tzdata \ + && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone + +WORKDIR /app + +COPY --from=backend /build/target/*.jar app.jar + +ENV DATA_DIR=/app/data +RUN mkdir -p ${DATA_DIR} + +EXPOSE 48080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..beedc02 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,35 @@ +# Docker 单容器部署 + +前端打包后放入 Spring Boot `static`,与 Java 一起在同一个容器内启动,不使用 Nginx。 + +## 国内源 + +- **npm**:`docker/.npmrc` 使用 npmmirror(淘宝镜像) +- **Maven**:`docker/maven-settings.xml` 使用阿里云仓库 + +## 构建与运行 + +在**项目根目录**执行: + +```bash +# 构建镜像 +docker compose -f docker/docker-compose.yml build + +# 前台运行 +docker compose -f docker/docker-compose.yml up + +# 后台运行 +docker compose -f docker/docker-compose.yml up -d +``` + +访问:http://localhost:48080 + +## 环境变量(可选) + +- `SSHMANAGER_ENCRYPTION_KEY`:连接密码加密密钥(生产务必修改) +- `SSHMANAGER_JWT_SECRET`:JWT 密钥(生产务必修改) +- `TZ`:时区,默认 `Asia/Shanghai` + +## 数据持久化 + +H2 数据目录通过 volume `app-data` 挂载到 `/app/data`,重启容器数据保留。 diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile new file mode 100644 index 0000000..58090a1 --- /dev/null +++ b/docker/backend.Dockerfile @@ -0,0 +1,33 @@ +# Backend: Maven 使用阿里云镜像,多阶段构建 +FROM maven:3.9-eclipse-temurin-8-alpine AS builder + +# 使用国内 Maven 配置(阿里云) +COPY docker/maven-settings.xml /root/.m2/settings.xml + +WORKDIR /build + +# 先复制 pom,利用 Docker 层缓存 +COPY backend/pom.xml . +RUN mvn dependency:go-offline -B -q + +COPY backend/src ./src +RUN mvn package -DskipTests -B -q + +# 运行阶段 +FROM eclipse-temurin:8-jre-alpine + +RUN apk add --no-cache tzdata \ + && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone + +WORKDIR /app + +COPY --from=builder /build/target/*.jar app.jar + +# 数据目录(H2 数据库文件) +ENV DATA_DIR=/app/data +RUN mkdir -p ${DATA_DIR} + +EXPOSE 48080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..f979ee4 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,24 @@ +# 单容器运行:前端已打包进 JAR,由 Spring Boot 统一提供静态资源与 API +# 构建:在项目根目录执行 docker compose -f docker/docker-compose.yml build +# 运行:docker compose -f docker/docker-compose.yml up -d + +services: + app: + build: + context: .. + dockerfile: docker/Dockerfile + image: ssh-manager:latest + container_name: ssh-manager + ports: + - "48080:48080" + environment: + - TZ=Asia/Shanghai + # 生产环境建议设置并挂载密钥 + # - SSHMANAGER_ENCRYPTION_KEY=... + # - SSHMANAGER_JWT_SECRET=... + volumes: + - app-data:/app/data + restart: unless-stopped + +volumes: + app-data: diff --git a/docker/maven-settings.xml b/docker/maven-settings.xml new file mode 100644 index 0000000..c7ea6a3 --- /dev/null +++ b/docker/maven-settings.xml @@ -0,0 +1,61 @@ + + + + + aliyun-central + Aliyun Maven Central + https://maven.aliyun.com/repository/central + central + + + aliyun-public + Aliyun Public + https://maven.aliyun.com/repository/public + * + + + + + aliyun + + + central + https://maven.aliyun.com/repository/central + true + false + + + spring + https://maven.aliyun.com/repository/spring + true + false + + + spring-plugin + https://maven.aliyun.com/repository/spring-plugin + true + false + + + + + central + https://maven.aliyun.com/repository/central + true + false + + + spring-plugin + https://maven.aliyun.com/repository/spring-plugin + true + false + + + + + + aliyun + + diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..fd32b29 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +# 脚本所在目录为 docker/,项目根目录为其上级 +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +echo ">>> 项目根目录: $ROOT" +echo ">>> 构建并启动..." +docker compose -f docker/docker-compose.yml build +docker compose -f docker/docker-compose.yml up -d + +echo "" +echo ">>> 已启动。访问: http://localhost:48080" +echo ">>> 查看日志: docker compose -f docker/docker-compose.yml logs -f"