feat: restructure Dockerfile to build Vite frontend and embed assets in Go application

This commit is contained in:
Dominik Schröter 2025-02-03 10:37:57 +01:00
parent d013bc909e
commit fb1e512a21
5 changed files with 129 additions and 46 deletions

View file

@ -24,14 +24,6 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: 1.22
- name: Install mupdf
run: sudo apt-get install -y mupdf
- name: Set library path
run: echo "/usr/lib" | sudo tee -a /etc/ld.so.conf.d/mupdf.conf && sudo ldconfig
- name: Install dependencies
run: go mod download
- name: Run Go tests
run: go test ./...
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@ -46,10 +38,20 @@ jobs:
- name: Install frontend dependencies
run: npm install
working-directory: web-app
- name: Build frontend
run: npm run build && cp -r dist/ ../dist/
working-directory: web-app
- name: Run frontend tests
run: npm test
working-directory: web-app
- name: Install mupdf
run: sudo apt-get install -y mupdf
- name: Set library path
run: echo "/usr/lib" | sudo tee -a /etc/ld.so.conf.d/mupdf.conf && sudo ldconfig
- name: Install dependencies
run: go mod download
- name: Run Go tests
run: go test ./...
build-amd64:
runs-on: ubuntu-latest
needs: test

View file

@ -3,7 +3,28 @@ ARG VERSION=docker-dev
ARG COMMIT=unknown
ARG BUILD_DATE=unknown
# Stage 1: Build the Go binary
# Stage 1: Build Vite frontend
FROM node:20-alpine AS frontend
# Set the working directory inside the container
WORKDIR /app
# Install necessary packages
RUN apk add --no-cache git
# Copy package.json and package-lock.json
COPY web-app/package.json web-app/package-lock.json ./
# Install dependencies
RUN npm install
# Copy the frontend code
COPY web-app /app/
# Build the frontend
RUN npm run build
# Stage 2: Build the Go binary
FROM golang:1.23.5-alpine3.21 AS builder
# Set the working directory inside the container
@ -28,6 +49,7 @@ RUN apk add --no-cache \
"mupdf=${MUPDF_VERSION}" \
"mupdf-dev=${MUPDF_DEV_VERSION}" \
"sed=${SED_VERSION}"
# Copy go.mod and go.sum files
COPY go.mod go.sum ./
@ -37,7 +59,10 @@ RUN go mod download
# Pre-compile go-sqlite3 to avoid doing this every time
RUN CGO_ENABLED=1 go build -tags musl -o /dev/null github.com/mattn/go-sqlite3
# Now copy the actual source files
# Copy the frontend build
COPY --from=frontend /app/dist /app/dist
# Copy the Go source files
COPY *.go .
# Import ARGs from top level
@ -55,28 +80,7 @@ RUN sed -i \
# Build the binary using caching for both go modules and build cache
RUN CGO_ENABLED=1 GOMAXPROCS=$(nproc) go build -tags musl -o paperless-gpt .
# Stage 2: Build Vite frontend
FROM node:20-alpine AS frontend
# Set the working directory inside the container
WORKDIR /app
# Install necessary packages
RUN apk add --no-cache git
# Copy package.json and package-lock.json
COPY web-app/package.json web-app/package-lock.json ./
# Install dependencies
RUN npm install
# Copy the frontend code
COPY web-app /app/
# Build the frontend
RUN npm run build
# Stage 3: Create a lightweight image with the Go binary and frontend
# Stage 3: Create a lightweight image with just the binary
FROM alpine:latest
ENV GIN_MODE=release
@ -91,9 +95,6 @@ WORKDIR /app/
# Copy the Go binary from the builder stage
COPY --from=builder /app/paperless-gpt .
# Copy the frontend build
COPY --from=frontend /app/dist /app/web-app/dist
# Expose the port the app runs on
EXPOSE 8080

View file

@ -36,6 +36,20 @@
4. Build and run paperless-gpt
5. Access web interface
### Testing Steps (Required Before Commits)
1. **Unit Tests**:
```bash
go test .
```
2. **E2E Tests**:
```bash
docker build . -t icereed/paperless-gpt:e2e
cd web-app && npm run test:e2e
```
These tests MUST be run and pass before considering any task complete.
## Configuration
### Environment Variables

54
embedded_assets.go Normal file
View file

@ -0,0 +1,54 @@
package main
import (
"embed"
"io"
"io/fs"
"net/http"
"path"
"strings"
"github.com/gin-gonic/gin"
)
//go:embed dist/*
var webappContent embed.FS
// CreateEmbeddedFileServer creates a http.FileSystem from our embedded files
func createEmbeddedFileServer() http.FileSystem {
// Strip the "dist" prefix from the embedded files
stripped, err := fs.Sub(webappContent, "dist")
if err != nil {
panic(err)
}
return http.FS(stripped)
}
// ServeEmbeddedFile serves a file from the embedded filesystem
func serveEmbeddedFile(c *gin.Context, prefix string, filepath string) {
// If the path is empty or ends with "/", serve index.html
if filepath == "" || strings.HasSuffix(filepath, "/") {
filepath = path.Join(filepath, "index.html")
}
// Try to open the file from our embedded filesystem
fullPath := path.Join("dist", prefix, filepath)
f, err := webappContent.Open(fullPath)
if err != nil {
// If file not found, serve 404
log.Warnf("File not found: %s", fullPath)
http.Error(c.Writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
c.Status(http.StatusNotFound)
return
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
// Serve the file
http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), f.(io.ReadSeeker))
}

32
main.go
View file

@ -45,7 +45,6 @@ var (
visionLlmModel = os.Getenv("VISION_LLM_MODEL")
logLevel = strings.ToLower(os.Getenv("LOG_LEVEL"))
listenInterface = os.Getenv("LISTEN_INTERFACE")
webuiPath = os.Getenv("WEBUI_PATH")
autoGenerateTitle = os.Getenv("AUTO_GENERATE_TITLE")
autoGenerateTags = os.Getenv("AUTO_GENERATE_TAGS")
autoGenerateCorrespondents = os.Getenv("AUTO_GENERATE_CORRESPONDENTS")
@ -247,16 +246,29 @@ func main() {
})
}
if webuiPath == "" {
webuiPath = "./web-app/dist"
}
// Serve static files for the frontend under /assets
router.StaticFS("/assets", gin.Dir(webuiPath+"/assets", true))
router.StaticFile("/vite.svg", webuiPath+"/vite.svg")
// Serve embedded web-app files
// router.GET("/*filepath", func(c *gin.Context) {
// filepath := c.Param("filepath")
// // Remove leading slash from filepath
// filepath = strings.TrimPrefix(filepath, "/")
// // Handle static assets under /assets/
// serveEmbeddedFile(c, "", filepath)
// })
// Catch-all route for serving the frontend
router.NoRoute(func(c *gin.Context) {
c.File(webuiPath + "/index.html")
// Instead of wildcard, serve specific files
router.GET("/favicon.ico", func(c *gin.Context) {
serveEmbeddedFile(c, "", "favicon.ico")
})
router.GET("/vite.svg", func(c *gin.Context) {
serveEmbeddedFile(c, "", "vite.svg")
})
router.GET("/assets/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
fmt.Printf("Serving asset: %s\n", filepath)
serveEmbeddedFile(c, "assets", filepath)
})
router.GET("/", func(c *gin.Context) {
serveEmbeddedFile(c, "", "index.html")
})
// Start OCR worker pool