Compare commits

..

1 Commits

Author SHA1 Message Date
fffe58a9ac build: add docker support
- Add Dockerfile for containerized deployment
- Add .drone.yml for CI/CD pipeline configuration
- Add docker and docker-run targets to Makefile
- Configure port 8080 binding and persistent volume mount
2026-02-20 21:53:48 -05:00
10 changed files with 76 additions and 72 deletions

View File

@@ -1,30 +1,68 @@
# Step 1: Build Frontend # Multi-stage build for Aethera
# Stage 1: Build frontend assets
FROM oven/bun:1 AS frontend-builder FROM oven/bun:1 AS frontend-builder
WORKDIR /app/frontend WORKDIR /app/frontend
# Copy frontend package files
COPY frontend/package.json frontend/bun.lock ./ COPY frontend/package.json frontend/bun.lock ./
# Install dependencies
RUN bun install --frozen-lockfile RUN bun install --frozen-lockfile
# Copy frontend source code
COPY frontend/ ./ COPY frontend/ ./
# Build frontend assets
RUN bun run build RUN bun run build
# Stage 2: Build Backend # Stage 2: Build Go binary
FROM golang:1.25-alpine AS backend-builder FROM golang:1.25-alpine AS backend-builder
WORKDIR /app
RUN apk add --no-cache git
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
COPY --from=frontend-builder /app/frontend/public/ ./web/static/
RUN go build -ldflags="-w -s" -o aethera ./cmd
# Stage 3: Minimal Runtime
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
WORKDIR /app WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git
# Copy go mod files
COPY backend/go.mod backend/go.sum ./
# Download Go dependencies
RUN go mod download
# Copy backend source code
COPY backend/ ./
# Copy frontend assets from previous stage
COPY --from=frontend-builder /app/frontend/public/dist ./public/dist
COPY --from=frontend-builder /app/frontend/public/index.html ./public/
COPY --from=frontend-builder /app/frontend/public/pages ./public/pages
# Build the Go binary
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o aethera ./cmd
# Stage 3: Create minimal runtime image
FROM alpine:3.21
# Install ca-certificates for HTTPS calls
RUN apk add --no-cache ca-certificates
WORKDIR /app
# Copy the binary from the builder stage
COPY --from=backend-builder /app/aethera . COPY --from=backend-builder /app/aethera .
# Copy static assets
COPY --from=backend-builder /app/public ./public
# Create data directory
RUN mkdir -p /app/data RUN mkdir -p /app/data
# Expose the default port
EXPOSE 8080 EXPOSE 8080
ENV AETHERA_LISTEN=0.0.0.0
ENV AETHERA_PORT=8080 # Set the entrypoint
ENV AETHERA_DATA_DIR=/app/data
ENTRYPOINT ["./aethera"] ENTRYPOINT ["./aethera"]
# Default command with recommended production settings
CMD ["--listen", "0.0.0.0", "--port", "8080", "--data-dir", "/app/data"]

View File

@@ -1,12 +1,9 @@
.PHONY: all frontend backend clean dev docker docker-run tests .PHONY: all frontend backend clean dev docker docker-run
all: frontend backend all: frontend backend
frontend: frontend:
cd frontend && bun run build cd frontend && bun run build
mkdir -p backend/web/static
cp frontend/public/index.html backend/web/static/ 2>/dev/null || true
cp -r frontend/public/pages backend/web/static/ 2>/dev/null || true
backend: backend:
cd backend && go build -o ./dist/aethera ./cmd cd backend && go build -o ./dist/aethera ./cmd
@@ -14,7 +11,6 @@ backend:
clean: clean:
rm -rf frontend/public/dist rm -rf frontend/public/dist
rm -rf backend/dist rm -rf backend/dist
rm -rf backend/web/static
dev: dev:
cd backend && go run ./cmd --listen 0.0.0.0 & cd backend && go run ./cmd --listen 0.0.0.0 &
@@ -25,6 +21,3 @@ docker:
docker-run: docker-run:
docker run -p 8080:8080 -v aethera-data:/app/data aethera docker run -p 8080:8080 -v aethera-data:/app/data aethera
tests:
cd backend && go test ./...

2
backend/.gitignore vendored
View File

@@ -1,3 +1 @@
dist dist
web/static/*
!web/static/.gitkeep

View File

@@ -4,11 +4,8 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strconv"
) )
const envPrefix = "AETHERA_"
type cliParams struct { type cliParams struct {
ListenAddr string ListenAddr string
ListenPort int ListenPort int
@@ -16,24 +13,6 @@ type cliParams struct {
SettingsFile string SettingsFile string
} }
// getEnvOrDefault returns the value of an environment variable or a default value
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(envPrefix + key); value != "" {
return value
}
return defaultValue
}
// getEnvIntOrDefault returns the integer value of an environment variable or a default value
func getEnvIntOrDefault(key string, defaultValue int) int {
if value := os.Getenv(envPrefix + key); value != "" {
if intVal, err := strconv.Atoi(value); err == nil {
return intVal
}
}
return defaultValue
}
func (p *cliParams) Validate() error { func (p *cliParams) Validate() error {
// Ensure Generated Directories // Ensure Generated Directories
imgDir := path.Join(p.DataDir, "generated/images") imgDir := path.Join(p.DataDir, "generated/images")

View File

@@ -13,17 +13,17 @@ import (
var ( var (
params = cliParams{ params = cliParams{
ListenAddr: getEnvOrDefault("LISTEN", "localhost"), ListenAddr: "localhost",
ListenPort: getEnvIntOrDefault("PORT", 8080), ListenPort: 8080,
DataDir: getEnvOrDefault("DATA_DIR", "./data"), DataDir: "./data",
} }
rootCmd = &cobra.Command{Use: "aethera"} rootCmd = &cobra.Command{Use: "aethera"}
) )
func init() { func init() {
rootCmd.PersistentFlags().StringVar(&params.DataDir, "data-dir", params.DataDir, "Directory to store generated images (env: AETHERA_DATA_DIR)") rootCmd.PersistentFlags().StringVar(&params.DataDir, "data-dir", "data", "Directory to store generated images")
rootCmd.PersistentFlags().StringVar(&params.ListenAddr, "listen", params.ListenAddr, "Address to listen on (env: AETHERA_LISTEN)") rootCmd.PersistentFlags().StringVar(&params.ListenAddr, "listen", "localhost", "Address to listen on")
rootCmd.PersistentFlags().IntVar(&params.ListenPort, "port", params.ListenPort, "Port to listen on (env: AETHERA_PORT)") rootCmd.PersistentFlags().IntVar(&params.ListenPort, "port", 8080, "Port to listen on")
} }
func main() { func main() {

View File

@@ -2,7 +2,6 @@ package server
import ( import (
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"path" "path"
"time" "time"
@@ -10,7 +9,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"reichard.io/aethera/internal/api" "reichard.io/aethera/internal/api"
"reichard.io/aethera/internal/store" "reichard.io/aethera/internal/store"
"reichard.io/aethera/web"
) )
func StartServer(settingsStore store.Store, dataDir, listenAddress string, listenPort int) { func StartServer(settingsStore store.Store, dataDir, listenAddress string, listenPort int) {
@@ -19,13 +17,12 @@ func StartServer(settingsStore store.Store, dataDir, listenAddress string, liste
// Create API Instance - use settingsStore as the unified store for both settings and chat // Create API Instance - use settingsStore as the unified store for both settings and chat
logger := logrus.New() logger := logrus.New()
api := api.New(settingsStore, dataDir, logger) api := api.New(settingsStore, dataDir, logger)
feFS := http.FileServer(http.Dir("../frontend/public/"))
mux.Handle("GET /", feFS)
// Serve embedded static assets // Serve UI Pages
staticFS, err := fs.Sub(web.Assets, "static") pagesFS := http.FileServer(http.Dir("../frontend/public/pages/"))
if err != nil { mux.Handle("GET /pages/", http.StripPrefix("/pages/", pagesFS))
logrus.Fatal("Failed to create static filesystem: ", err)
}
mux.Handle("GET /", http.FileServer(http.FS(staticFS)))
// Serve Generated Data // Serve Generated Data
genFS := http.FileServer(http.Dir(path.Join(dataDir, "generated"))) genFS := http.FileServer(http.Dir(path.Join(dataDir, "generated")))

View File

@@ -1,6 +0,0 @@
package web
import "embed"
//go:embed static/*
var Assets embed.FS

View File

@@ -21,6 +21,11 @@
config.allowUnfree = true; config.allowUnfree = true;
} }
); );
oc = pkgs.writeShellScriptBin "oc" ''
PRJ_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
cd "$PRJ_ROOT" && OPENCODE_EXPERIMENTAL_LSP_TOOL=true opencode
'';
in in
{ {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
@@ -33,11 +38,11 @@
# Frontend # Frontend
bun bun
watchman watchman
]; tailwindcss_4
shellHook = '' # Custom Commands
export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH oc
''; ];
}; };
} }
); );

View File

@@ -2,8 +2,8 @@
"name": "aethera", "name": "aethera",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "mkdir -p public/dist && bun build src/main.ts --outdir public/dist --target browser --watch & bunx tailwindcss -i styles.css -o public/dist/styles.css --watch", "dev": "bun build src/main.ts --outdir public/dist --target browser --watch & bunx @tailwindcss/cli -i styles.css -o public/dist/styles.css --watch",
"build": "bun build src/main.ts --outdir public/dist --target browser && bunx tailwindcss -i styles.css -o public/dist/styles.css --minify", "build": "bun build src/main.ts --outdir public/dist --target browser && bunx @tailwindcss/cli -i styles.css -o public/dist/styles.css --minify",
"lint": "eslint ./src/**" "lint": "eslint ./src/**"
}, },
"devDependencies": { "devDependencies": {