diff --git a/Dockerfile b/Dockerfile index 92bbe1d4..57d3d95d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,63 @@ -## Multistage build: First stage fetches dependencies +# ============================================================================ +# STAGE 1: Build Frontend (React 18 + Bun) +# ============================================================================ +# This stage builds the React SPA with TypeScript, Tailwind CSS, and Vite +# Output: frontend/dist/ with optimized static assets +# ============================================================================ +FROM oven/bun:latest AS frontend-builder + +WORKDIR /app/frontend + +# Copy frontend source +COPY frontend/ . + +# Install dependencies and build frontend +# - bun install: Fast package installation +# - bun run build: TypeScript type check + Vite optimization +RUN bun install && \ + bun run build + +# Verify build output exists +RUN test -f dist/index.html || (echo "Frontend build failed!" && exit 1) + +# ============================================================================ +# STAGE 2: Build Backend (Go 1.25) with embedded frontend +# ============================================================================ +# This stage builds the Go server with the React SPA embedded +# Uses Go's embed package to include all static assets +# Output: filebrowser binary (statically linked, no dependencies) +# ============================================================================ +FROM golang:1.25-alpine AS backend-builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +# Copy the entire project +COPY . . + +# Copy built frontend from stage 1 into the frontend Go package +# The frontend/frontend.go file uses //go:embed dist to include these files +COPY --from=frontend-builder /app/frontend/dist ./frontend/dist + +# Download Go dependencies +RUN go mod download + +# Build the Go binary with embedded frontend +# - CGO_ENABLED=0: Static linking (no libc dependency) +# - GOOS=linux: Linux target +# - GOARCH=amd64: 64-bit x86 architecture +# - The frontend/dist directory is embedded via the //go:embed directive +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o filebrowser . + +# Verify binary was created and contains embedded assets +RUN test -f filebrowser || (echo "Go build failed!" && exit 1) && \ + strings filebrowser | grep -q "index.html" || (echo "Frontend not embedded!" && exit 1) + +# ============================================================================ +# STAGE 3: Fetch dependencies and base tools +# ============================================================================ FROM alpine:3.23 AS fetcher # install and copy ca-certificates, mailcap, and tini-static; download JSON.sh @@ -6,21 +65,31 @@ RUN apk update && \ apk --no-cache add ca-certificates mailcap tini-static && \ wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh -## Second stage: Use lightweight BusyBox image for final runtime environment +# ============================================================================ +# STAGE 4: Final Runtime Image (Minimal) +# ============================================================================ +# Lightweight runtime using busybox (~50MB total image size) +# Only includes the compiled binary and essential runtime dependencies +# ============================================================================ FROM busybox:1.37.0-musl -# Define non-root user UID and GID +# Define non-root user UID and GID for security ENV UID=1000 ENV GID=1000 -# Create user group and user +# Create unprivileged user and group RUN addgroup -g $GID user && \ adduser -D -u $UID -G user user -# Copy binary, scripts, and configurations into image with proper ownership -COPY --chown=user:user filebrowser /bin/filebrowser +# Copy compiled binary from backend builder +# The binary includes the React SPA (frontend/dist) via Go's embed package +COPY --chown=user:user --from=backend-builder /app/filebrowser /bin/filebrowser + +# Copy initialization and configuration scripts COPY --chown=user:user docker/common/ / COPY --chown=user:user docker/alpine/ / + +# Copy runtime dependencies from fetcher stage COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini COPY --from=fetcher /JSON.sh /JSON.sh COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf @@ -28,19 +97,27 @@ COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates COPY --from=fetcher /etc/mime.types /etc/mime.types COPY --from=fetcher /etc/ssl /etc/ssl -# Create data directories, set ownership, and ensure healthcheck script is executable +# Create persistent data directories with proper ownership RUN mkdir -p /config /database /srv && \ - chown -R user:user /config /database /srv \ - && chmod +x /healthcheck.sh + chown -R user:user /config /database /srv && \ + chmod +x /healthcheck.sh /init.sh -# Define healthcheck script +# Health check configuration +# Verifies the frontend is being served correctly HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh -# Set the user, volumes and exposed ports +# Run as non-root user USER user +# Define persistent volumes +# /srv: User file storage +# /config: Configuration files +# /database: SQLite database VOLUME /srv /config /database +# Expose port 80 (internal server runs on 8080, init.sh proxies if needed) EXPOSE 80 +# Use tini as init process for proper signal handling +# Then run init.sh which starts the filebrowser binary ENTRYPOINT [ "tini", "--", "/init.sh" ] diff --git a/compose.yaml b/compose.yaml index be19b56a..ff8c9d28 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,5 +1,10 @@ -# Run using: +# WOLK - Modern File Browser with React Frontend and Go Backend +# +# Build and run everything with: # docker compose up --build +# +# Access the application at: http://localhost:8000 +# Default credentials: admin / (randomly generated, check logs) services: filebrowser: @@ -7,31 +12,69 @@ services: build: dockerfile: Dockerfile context: . + # Build args (optional) + args: + - BUILDKIT_INLINE_CACHE=1 + restart: unless-stopped networks: - filebrowser ports: - - 8000:80 + - "8000:80" # Host:Container - Access via http://localhost:8000 volumes: - - filebrowser:/flux/vault + - filebrowser_data:/srv + - filebrowser_config:/config + - filebrowser_db:/database environment: - - REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 # Use rediss:// for ssl + # Server configuration + - FB_PORT=80 + - FB_ADDRESS=0.0.0.0 + # Redis cache URL for multi-instance deployments (optional) + - REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 + # Token expiration time (optional) + - FB_TOKEN_EXPIRATION_TIME=2h + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s redis: - container_name: redis - image: redis:latest + container_name: filebrowser_redis + image: redis:7-alpine + restart: unless-stopped networks: - filebrowser command: - sh - -c - | - cat > /tmp/users.acl < /tmp/users.acl <<'EOF' user default on >filebrowser ~* +@all EOF redis-server --aclfile /tmp/users.acl + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 5s networks: filebrowser: + driver: bridge volumes: - filebrowser: + filebrowser_data: + driver: local + filebrowser_config: + driver: local + filebrowser_db: + driver: local + redis_data: + driver: local diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 00000000..952b096f --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,72 @@ +# Development environment - runs Go backend and serves frontend from Vite dev server +# +# This is useful for development as it: +# 1. Runs the Go backend in a container for isolated environment +# 2. Allows running React dev server locally with HMR +# +# Setup: +# 1. Start containers: docker compose -f docker-compose.dev.yaml up +# 2. In another terminal, run frontend dev server: cd frontend && bun run dev +# 3. Access the app at: http://localhost:5173 (frontend dev server) +# 4. API calls proxy to http://backend:8080 +# +# For production build, use: docker compose up --build + +services: + backend: + container_name: filebrowser_backend + build: + dockerfile: Dockerfile.dev + context: . + restart: unless-stopped + networks: + - filebrowser + ports: + - "8080:8080" + volumes: + - ./:/app + - /app/frontend/node_modules + environment: + - FB_PORT=8080 + - FB_ADDRESS=0.0.0.0 + - REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + + redis: + container_name: filebrowser_redis_dev + image: redis:7-alpine + restart: unless-stopped + networks: + - filebrowser + command: + - sh + - -c + - | + cat > /tmp/users.acl <<'EOF' + user default on >filebrowser ~* +@all + EOF + redis-server --aclfile /tmp/users.acl + volumes: + - redis_data_dev:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 5s + +networks: + filebrowser: + driver: bridge + +volumes: + redis_data_dev: + driver: local diff --git a/http/static.go b/http/static.go index 25f8539d..c9e41450 100644 --- a/http/static.go +++ b/http/static.go @@ -1,12 +1,12 @@ package fbhttp import ( + "compress/gzip" "encoding/json" "errors" "fmt" - "io/fs" "io" - "compress/gzip" + "io/fs" "log" "net/http" "os" @@ -110,7 +110,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs } w.Header().Set("x-xss-protection", "1; mode=block") - return handleWithStaticData(w, r, d, assetsFs, "public/index.html", "text/html; charset=utf-8") + return handleWithStaticData(w, r, d, assetsFs, "index.html", "text/html; charset=utf-8") }, "", store, server) static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { @@ -153,7 +153,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs defer f.Close() acceptEncoding := r.Header.Get("Accept-Encoding") - if strings.Contains(acceptEncoding, "gzip") { + if strings.Contains(acceptEncoding, "gzip") { w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Type", "application/javascript; charset=utf-8") @@ -168,7 +168,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs defer gzReader.Close() w.Header().Set("Content-Type", "application/javascript; charset=utf-8") - + if _, err := io.Copy(w, gzReader); err != nil { return http.StatusInternalServerError, err }