diff --git a/Dockerfile b/Dockerfile index 25c179c..adba33c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -101,9 +101,45 @@ ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"] -# Default production stage (for Dokku - runs API) -FROM go-base AS production +# Default production stage (for Dokku - runs API + Admin) +FROM node:20-alpine AS production + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates tzdata curl + +# Create non-root user +RUN addgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app + +WORKDIR /app + +# Copy Go binaries +COPY --from=builder /app/api /app/api +COPY --from=builder /app/worker /app/worker + +# Copy templates directory +COPY --from=builder /app/templates /app/templates + +# Copy migrations and seeds +COPY --from=builder /app/migrations /app/migrations +COPY --from=builder /app/seeds /app/seeds + +# Copy admin panel standalone build +COPY --from=admin-builder /app/.next/standalone /app/admin +COPY --from=admin-builder /app/.next/static /app/admin/.next/static +COPY --from=admin-builder /app/public /app/admin/public + +# Copy start script +COPY start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# Create uploads directory +RUN mkdir -p /app/uploads && chown -R app:app /app + +USER app + EXPOSE 5000 + HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD curl -f http://localhost:${PORT:-5000}/api/health/ || exit 1 -CMD ["/app/api"] + +CMD ["/app/start.sh"] diff --git a/internal/admin/routes.go b/internal/admin/routes.go index 86c3b46..879808d 100644 --- a/internal/admin/routes.go +++ b/internal/admin/routes.go @@ -2,9 +2,9 @@ package admin import ( "net/http" + "net/http/httputil" + "net/url" "os" - "path/filepath" - "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -252,75 +252,32 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe } } - // Serve admin panel static files - setupStaticFiles(router) + // Proxy admin panel requests to Next.js server + setupAdminProxy(router) } -// setupStaticFiles configures serving the admin panel static files -func setupStaticFiles(router *gin.Engine) { - // Determine the static files directory - // Check multiple possible locations - possiblePaths := []string{ - "admin/out", // Development: from project root - "./admin/out", // Development: relative - "/app/admin/out", // Docker: absolute path +// setupAdminProxy configures reverse proxy to the Next.js admin panel +func setupAdminProxy(router *gin.Engine) { + // Get admin panel URL from env, default to localhost:3000 + adminURL := os.Getenv("ADMIN_PANEL_URL") + if adminURL == "" { + adminURL = "http://127.0.0.1:3000" } - var staticDir string - for _, path := range possiblePaths { - if _, err := os.Stat(path); err == nil { - staticDir = path - break - } - } - - if staticDir == "" { - // Admin panel not built yet, skip static file serving + target, err := url.Parse(adminURL) + if err != nil { return } - // Serve static files at /admin/* - router.GET("/admin/*filepath", func(c *gin.Context) { - filePath := c.Param("filepath") + proxy := httputil.NewSingleHostReverseProxy(target) - // Clean the path - filePath = strings.TrimPrefix(filePath, "/") - if filePath == "" { - filePath = "index.html" - } + // Handle all /admin/* requests + router.Any("/admin/*filepath", func(c *gin.Context) { + proxy.ServeHTTP(c.Writer, c.Request) + }) - fullPath := filepath.Join(staticDir, filePath) - - // Check if file exists - if _, err := os.Stat(fullPath); os.IsNotExist(err) { - // For SPA routing, serve index.html for non-existent paths - // But only if it's not a file request (no extension or .html) - ext := filepath.Ext(filePath) - if ext == "" || ext == ".html" { - // Try to find the specific page's index.html - pagePath := filepath.Join(staticDir, filePath, "index.html") - if _, err := os.Stat(pagePath); err == nil { - c.File(pagePath) - return - } - // Fall back to root index.html - c.File(filepath.Join(staticDir, "index.html")) - return - } - c.Status(http.StatusNotFound) - return - } - - // If it's a directory, serve index.html - info, _ := os.Stat(fullPath) - if info.IsDir() { - indexPath := filepath.Join(fullPath, "index.html") - if _, err := os.Stat(indexPath); err == nil { - c.File(indexPath) - return - } - } - - c.File(fullPath) + // Also handle /admin without trailing path + router.Any("/admin", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/admin/") }) } diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..ad4309d --- /dev/null +++ b/start.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Start Next.js admin panel in background on port 3000 +cd /app/admin && node server.js & + +# Start Go API on port 5000 (Dokku's default) +/app/api