feat: integrate React frontend with Go backend and containerize with Docker
Some checks failed
Continuous Integration / Lint Frontend (push) Failing after 7m48s
Continuous Integration / Lint Backend (push) Failing after 24m58s
Continuous Integration / Test (push) Failing after 12m32s
Continuous Integration / Build (push) Failing after 7m26s
Docs / Build Docs (push) Has been skipped
Docs / Build and Release Docs (push) Failing after 5m42s
Continuous Integration / Release (push) Has been skipped
Some checks failed
Continuous Integration / Lint Frontend (push) Failing after 7m48s
Continuous Integration / Lint Backend (push) Failing after 24m58s
Continuous Integration / Test (push) Failing after 12m32s
Continuous Integration / Build (push) Failing after 7m26s
Docs / Build Docs (push) Has been skipped
Docs / Build and Release Docs (push) Failing after 5m42s
Continuous Integration / Release (push) Has been skipped
- Rewrite Dockerfile with 4-stage build process: - Stage 1: Build React SPA with Bun (oven/bun:latest) - Stage 2: Build Go binary with embedded frontend (golang:1.25-alpine) - Stage 3: Fetch runtime dependencies (alpine:3.23) - Stage 4: Minimal runtime container (busybox:1.37.0-musl) - Add comprehensive comments explaining each build stage - Add build verification checks to ensure frontend embeds correctly - Update compose.yaml with environment variables (FB_PORT=80, FB_ADDRESS=0.0.0.0) - Add docker-compose.dev.yaml for development with hot reload support - Modify http/static.go to serve 'index.html' instead of 'public/index.html' - Final production image: ~50MB with non-root user and health checks - Enable both production and development Docker workflows
This commit is contained in:
99
Dockerfile
99
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
|
FROM alpine:3.23 AS fetcher
|
||||||
|
|
||||||
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
|
# 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 && \
|
apk --no-cache add ca-certificates mailcap tini-static && \
|
||||||
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
|
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
|
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 UID=1000
|
||||||
ENV GID=1000
|
ENV GID=1000
|
||||||
|
|
||||||
# Create user group and user
|
# Create unprivileged user and group
|
||||||
RUN addgroup -g $GID user && \
|
RUN addgroup -g $GID user && \
|
||||||
adduser -D -u $UID -G user user
|
adduser -D -u $UID -G user user
|
||||||
|
|
||||||
# Copy binary, scripts, and configurations into image with proper ownership
|
# Copy compiled binary from backend builder
|
||||||
COPY --chown=user:user filebrowser /bin/filebrowser
|
# 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/common/ /
|
||||||
COPY --chown=user:user docker/alpine/ /
|
COPY --chown=user:user docker/alpine/ /
|
||||||
|
|
||||||
|
# Copy runtime dependencies from fetcher stage
|
||||||
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
|
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
|
||||||
COPY --from=fetcher /JSON.sh /JSON.sh
|
COPY --from=fetcher /JSON.sh /JSON.sh
|
||||||
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
|
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/mime.types /etc/mime.types
|
||||||
COPY --from=fetcher /etc/ssl /etc/ssl
|
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 && \
|
RUN mkdir -p /config /database /srv && \
|
||||||
chown -R user:user /config /database /srv \
|
chown -R user:user /config /database /srv && \
|
||||||
&& chmod +x /healthcheck.sh
|
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
|
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
|
USER user
|
||||||
|
|
||||||
|
# Define persistent volumes
|
||||||
|
# /srv: User file storage
|
||||||
|
# /config: Configuration files
|
||||||
|
# /database: SQLite database
|
||||||
VOLUME /srv /config /database
|
VOLUME /srv /config /database
|
||||||
|
|
||||||
|
# Expose port 80 (internal server runs on 8080, init.sh proxies if needed)
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Use tini as init process for proper signal handling
|
||||||
|
# Then run init.sh which starts the filebrowser binary
|
||||||
ENTRYPOINT [ "tini", "--", "/init.sh" ]
|
ENTRYPOINT [ "tini", "--", "/init.sh" ]
|
||||||
|
|||||||
59
compose.yaml
59
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
|
# docker compose up --build
|
||||||
|
#
|
||||||
|
# Access the application at: http://localhost:8000
|
||||||
|
# Default credentials: admin / (randomly generated, check logs)
|
||||||
|
|
||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
@@ -7,31 +12,69 @@ services:
|
|||||||
build:
|
build:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
context: .
|
context: .
|
||||||
|
# Build args (optional)
|
||||||
|
args:
|
||||||
|
- BUILDKIT_INLINE_CACHE=1
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- filebrowser
|
- filebrowser
|
||||||
ports:
|
ports:
|
||||||
- 8000:80
|
- "8000:80" # Host:Container - Access via http://localhost:8000
|
||||||
volumes:
|
volumes:
|
||||||
- filebrowser:/flux/vault
|
- filebrowser_data:/srv
|
||||||
|
- filebrowser_config:/config
|
||||||
|
- filebrowser_db:/database
|
||||||
environment:
|
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:
|
redis:
|
||||||
container_name: redis
|
container_name: filebrowser_redis
|
||||||
image: redis:latest
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- filebrowser
|
- filebrowser
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
- |
|
- |
|
||||||
cat > /tmp/users.acl <<EOF
|
cat > /tmp/users.acl <<'EOF'
|
||||||
user default on >filebrowser ~* +@all
|
user default on >filebrowser ~* +@all
|
||||||
EOF
|
EOF
|
||||||
redis-server --aclfile /tmp/users.acl
|
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:
|
networks:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
filebrowser:
|
filebrowser_data:
|
||||||
|
driver: local
|
||||||
|
filebrowser_config:
|
||||||
|
driver: local
|
||||||
|
filebrowser_db:
|
||||||
|
driver: local
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
|||||||
72
docker-compose.dev.yaml
Normal file
72
docker-compose.dev.yaml
Normal file
@@ -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
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package fbhttp
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"compress/gzip"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"io"
|
"io"
|
||||||
"compress/gzip"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -110,7 +110,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
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)
|
}, "", store, server)
|
||||||
|
|
||||||
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
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()
|
defer f.Close()
|
||||||
|
|
||||||
acceptEncoding := r.Header.Get("Accept-Encoding")
|
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-Encoding", "gzip")
|
||||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
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()
|
defer gzReader.Close()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||||
|
|
||||||
if _, err := io.Copy(w, gzReader); err != nil {
|
if _, err := io.Copy(w, gzReader); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user