diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..202a898 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,56 @@ +name: Build and Deploy + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.REGISTRY_URL }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and push postgres image + uses: docker/build-push-action@v5 + if: | + contains(github.event.commits[0].modified, 'infra/docker/postgres-pg-cron') || + contains(github.event.commits[0].added, 'infra/docker/postgres-pg-cron') + with: + context: . + file: infra/docker/postgres-pg-cron/Dockerfile + push: true + tags: | + ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/familyhub-postgres:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push app image + uses: docker/build-push-action@v5 + with: + context: . + file: infra/docker/application/Dockerfile + push: true + tags: | + ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/familyhub:latest + ${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/familyhub:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Trigger deploy + run: | + curl -s -X POST \ + -H "X-Webhook-Secret: ${{ secrets.WEBHOOK_SECRET }}" \ + "http://10.0.0.2:9001/deploy?container=familyhub" \ No newline at end of file diff --git a/111 b/111 deleted file mode 100644 index fca3d07..0000000 --- a/111 +++ /dev/null @@ -1,2 +0,0 @@ -Portainer -admin - 4c#;=H36$s^J \ No newline at end of file diff --git a/backend/src/api/server.go b/backend/src/api/server.go index bf07bb1..f391793 100644 --- a/backend/src/api/server.go +++ b/backend/src/api/server.go @@ -128,6 +128,8 @@ func NewServer(cfg config.Config) *Server { authRouter := routers.NewAuthRouter(authService) authRouter.RegisterRouter(apiV1) + // подключаем статику Vue — должно быть последним + registerStaticFiles(router) return &Server{ httpServer: &http.Server{ Addr: cfg.APIHost + ":" + cfg.APIPort, diff --git a/backend/src/api/static.go b/backend/src/api/static.go new file mode 100644 index 0000000..b453a11 --- /dev/null +++ b/backend/src/api/static.go @@ -0,0 +1,27 @@ +package api + +import ( + "embed" + "io/fs" + "net/http" + + "github.com/gin-gonic/gin" +) + +//go:embed dist +var staticFiles embed.FS + +func registerStaticFiles(router *gin.Engine) { + // вырезаем префикс dist/ чтобы / отдавал index.html + distFS, err := fs.Sub(staticFiles, "dist") + if err != nil { + panic(err) + } + + fileServer := http.FileServer(http.FS(distFS)) + + // все маршруты которые не /api и не /openapi — отдаём Vue + router.NoRoute(func(c *gin.Context) { + fileServer.ServeHTTP(c.Writer, c.Request) + }) +} diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index ffaa02d..4ec4f7d 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -1,12 +1,28 @@ version: '3.9' services: + app: + image: git.myhomecloud.tech/admin/familyhub:latest + container_name: familyhub + restart: unless-stopped + ports: + - "10.0.0.2:8000:8000" # только через WireGuard + environment: + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=familyUser + - DB_PASSWORD=familyPass + - DB_NAME=familyHubDB + depends_on: + - db + networks: + - family-hub-net + db: - build: - context: .. - dockerfile: infra/docker/postgres-pg-cron/Dockerfile + image: git.myhomecloud.tech/admin/familyhub-postgres:latest container_name: postgres restart: always + pull_policy: always command: - postgres - -c @@ -17,8 +33,31 @@ services: POSTGRES_USER: familyUser POSTGRES_PASSWORD: familyPass POSTGRES_DB: familyHubDB - ports: - - "5432:5432" volumes: - - ./volumes/postgres:/var/lib/postgresql/data - - ./docker/postgres-pg-cron/init:/docker-entrypoint-initdb.d + - postgres-data:/var/lib/postgresql/data + - ./init:/docker-entrypoint-initdb.d + networks: + - family-hub-net + + webhook: + image: git.myhomecloud.tech/admin/familyhub-webhook:latest + container_name: webhook + restart: unless-stopped + pull_policy: always + ports: + - "10.0.0.2:9001:9001" + environment: + - WEBHOOK_SECRET=${WEBHOOK_SECRET} + - COMPOSE_FILE=/compose/docker-compose.yml + - COMPOSE_PROJECT=familyhub + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./docker-compose.yml:/compose/docker-compose.yml:ro + networks: + - family-hub-net + +networks: + family-hub-net: + +volumes: + postgres-data: diff --git a/infra/docker/application/Dockerfile b/infra/docker/application/Dockerfile new file mode 100644 index 0000000..0f62880 --- /dev/null +++ b/infra/docker/application/Dockerfile @@ -0,0 +1,48 @@ +# ================================ +# Stage 1: сборка Vue +# ================================ +FROM node:20-alpine AS frontend + +WORKDIR /app + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +RUN npm run build + +# ================================ +# Stage 2: сборка Go +# ================================ +FROM golang:1.25-alpine AS backend + +WORKDIR /app + +# зависимости отдельно — используем кэш слоёв +COPY backend/go.mod backend/go.sum ./ +RUN go mod download + +# исходники +COPY backend/ ./ + +# встраиваем собранную статику Vue +COPY --from=frontend /app/dist ./src/api/dist + +# сборка бинарника +RUN CGO_ENABLED=0 GOOS=linux go build -o server ./src/ + +# ================================ +# Stage 3: финальный образ +# ================================ +FROM alpine:3.19 + +# нужен для корректной работы TLS и временных зон +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app + +COPY --from=backend /app/server ./server + +EXPOSE 8080 + +ENTRYPOINT ["./server"] \ No newline at end of file diff --git a/infra/webhook/Dockerfile b/infra/webhook/Dockerfile new file mode 100644 index 0000000..fea7be5 --- /dev/null +++ b/infra/webhook/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /app +COPY infra/webhook/ . +RUN go mod download && \ + CGO_ENABLED=0 GOOS=linux go build -o webhook . + +FROM alpine:3.19 +RUN apk add --no-cache docker-cli ca-certificates +COPY --from=builder /app/webhook /webhook +EXPOSE 9001 +ENTRYPOINT ["/webhook"] \ No newline at end of file diff --git a/infra/webhook/go.mod b/infra/webhook/go.mod new file mode 100644 index 0000000..a3261a0 --- /dev/null +++ b/infra/webhook/go.mod @@ -0,0 +1,5 @@ +module webhook + +go 1.25 + +require github.com/docker/docker v26.1.0+incompatible \ No newline at end of file diff --git a/infra/webhook/go.sum b/infra/webhook/go.sum new file mode 100644 index 0000000..e358dc9 --- /dev/null +++ b/infra/webhook/go.sum @@ -0,0 +1 @@ +github.com/docker/docker v26.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= diff --git a/infra/webhook/main.go b/infra/webhook/main.go new file mode 100644 index 0000000..50c05a0 --- /dev/null +++ b/infra/webhook/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "os" + "os/exec" + + "github.com/docker/docker/client" +) + +func main() { + secret := os.Getenv("WEBHOOK_SECRET") + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + log.Fatal("failed to create docker client:", err) + } + defer cli.Close() + + http.HandleFunc("/deploy", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if r.Header.Get("X-Webhook-Secret") != secret { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + // имя контейнера берём из query параметра + containerName := r.URL.Query().Get("container") + if containerName == "" { + http.Error(w, "container parameter is required", http.StatusBadRequest) + return + } + + ctx := context.Background() + + // получаем инфо о контейнере чтобы узнать имя образа + inspect, err := cli.ContainerInspect(ctx, containerName) + if err != nil { + log.Printf("failed to inspect container %s: %v", containerName, err) + http.Error(w, "container not found", http.StatusNotFound) + return + } + + imageName := inspect.Config.Image + log.Printf("Container: %s, Image: %s", containerName, imageName) + + // тянем новый образ + log.Println("Pulling new image...") + pull := exec.Command("docker", "pull", imageName) + pull.Stdout = os.Stdout + pull.Stderr = os.Stderr + if err := pull.Run(); err != nil { + log.Println("pull failed:", err) + http.Error(w, "pull failed", http.StatusInternalServerError) + return + } + + // перезапускаем контейнер + log.Printf("Restarting container %s...", containerName) + restart := exec.Command("docker", "restart", containerName) + restart.Stdout = os.Stdout + restart.Stderr = os.Stderr + if err := restart.Run(); err != nil { + log.Println("restart failed:", err) + http.Error(w, "restart failed", http.StatusInternalServerError) + return + } + + log.Printf("Deploy of %s completed", containerName) + w.WriteHeader(http.StatusOK) + + json.NewEncoder(w).Encode(map[string]string{ + "status": "ok", + "container": containerName, + "image": imageName, + }) + }) + + log.Println("Webhook server listening on :9001") + log.Fatal(http.ListenAndServe(":9001", nil)) +}