mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-02 13:17:57 +00:00
proper src structure, dockerfile, entrypoint
This commit is contained in:
86
Dockerfile
Normal file
86
Dockerfile
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Build stage - using Debian to avoid Alpine musl thread creation issues
|
||||||
|
# Alpine's musl libc causes rayon/tokio thread pool panics during svelte-adapter-bun build
|
||||||
|
FROM oven/bun:1.3.5-debian AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends jq git && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy package files and install ALL dependencies (needed for build)
|
||||||
|
COPY package.json bun.lock* bunfig.toml ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code and build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build with parallelism - dedicated build VM has 16 CPUs and 32GB RAM
|
||||||
|
# Increased memory limits for parallel compilation with larger semi-space for GC
|
||||||
|
RUN NODE_OPTIONS="--max-old-space-size=8192 --max-semi-space-size=128" bun run build
|
||||||
|
|
||||||
|
# Production stage - minimal Alpine with Bun runtime
|
||||||
|
FROM oven/bun:1.3.5-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies, create user
|
||||||
|
# Add sqlite for emergency scripts, git for stack git operations, curl for healthchecks
|
||||||
|
# Add docker-cli and docker-cli-compose for stack management (uses host's docker socket)
|
||||||
|
# Add openssh-client for SSH key authentication with git repositories
|
||||||
|
# Upgrade all packages to latest versions for security patches
|
||||||
|
RUN apk upgrade --no-cache \
|
||||||
|
&& apk add --no-cache curl git tini su-exec sqlite docker-cli docker-cli-compose openssh-client iproute2 \
|
||||||
|
&& addgroup -g 1001 dockhand \
|
||||||
|
&& adduser -u 1001 -G dockhand -h /home/dockhand -D dockhand
|
||||||
|
|
||||||
|
# Copy package files and install production dependencies
|
||||||
|
# This is needed because svelte-adapter-bun externalizes some packages (croner, etc.)
|
||||||
|
# that need to be available at runtime. Installing at build time is more reliable
|
||||||
|
# than Bun's auto-install which requires network access and writable cache.
|
||||||
|
COPY package.json bun.lock* ./
|
||||||
|
RUN bun install --production --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy built application (Bun adapter output)
|
||||||
|
COPY --from=builder /app/build ./build
|
||||||
|
|
||||||
|
# Copy bundled subprocess scripts (built by scripts/build-subprocesses.ts)
|
||||||
|
COPY --from=builder /app/build/subprocesses/ ./subprocesses/
|
||||||
|
|
||||||
|
# Copy database migrations
|
||||||
|
COPY drizzle/ ./drizzle/
|
||||||
|
COPY drizzle-pg/ ./drizzle-pg/
|
||||||
|
|
||||||
|
# Copy legal documents
|
||||||
|
COPY LICENSE.txt PRIVACY.txt ./
|
||||||
|
|
||||||
|
# Copy entrypoint script
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Copy emergency scripts (only the emergency subfolder, not license generation scripts)
|
||||||
|
COPY scripts/emergency/ ./scripts/
|
||||||
|
RUN chmod +x ./scripts/*.sh 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create directories with proper ownership
|
||||||
|
RUN mkdir -p /home/dockhand/.dockhand/stacks /app/data \
|
||||||
|
&& chown -R dockhand:dockhand /app /home/dockhand
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Runtime configuration
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV DATA_DIR=/app/data
|
||||||
|
ENV HOME=/home/dockhand
|
||||||
|
|
||||||
|
# User/group IDs - customize with -e PUID=1000 -e PGID=1000
|
||||||
|
# The entrypoint will recreate the dockhand user with these IDs
|
||||||
|
ENV PUID=1001
|
||||||
|
ENV PGID=1001
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000/ || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
CMD ["bun", "run", "./build/index.js"]
|
||||||
9
bunfig.toml
Normal file
9
bunfig.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Bun configuration for Dockhand
|
||||||
|
|
||||||
|
[install]
|
||||||
|
# Use exact versions for reproducible builds
|
||||||
|
exact = true
|
||||||
|
|
||||||
|
[run]
|
||||||
|
# Enable source maps for better error messages
|
||||||
|
sourcemap = "external"
|
||||||
16
components.json
Normal file
16
components.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
122
docker-entrypoint.sh
Normal file
122
docker-entrypoint.sh
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Dockhand Docker Entrypoint
|
||||||
|
# === Configuration ===
|
||||||
|
PUID=${PUID:-1001}
|
||||||
|
PGID=${PGID:-1001}
|
||||||
|
|
||||||
|
# === Detect if running as root ===
|
||||||
|
RUNNING_AS_ROOT=false
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
RUNNING_AS_ROOT=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === User Setup ===
|
||||||
|
# Root mode: PUID=0 requested OR already running as root with default PUID/PGID
|
||||||
|
if [ "$PUID" = "0" ]; then
|
||||||
|
echo "Running as root user (PUID=0)"
|
||||||
|
RUN_USER="root"
|
||||||
|
elif [ "$RUNNING_AS_ROOT" = "true" ] && [ "$PUID" = "1001" ] && [ "$PGID" = "1001" ]; then
|
||||||
|
echo "Running as root user"
|
||||||
|
RUN_USER="root"
|
||||||
|
else
|
||||||
|
RUN_USER="dockhand"
|
||||||
|
# Only modify if PUID/PGID differ from image defaults (1001:1001)
|
||||||
|
if [ "$PUID" != "1001" ] || [ "$PGID" != "1001" ]; then
|
||||||
|
echo "Configuring user with PUID=$PUID PGID=$PGID"
|
||||||
|
|
||||||
|
# Remove existing dockhand user/group (only dockhand, not others)
|
||||||
|
deluser dockhand 2>/dev/null || true
|
||||||
|
delgroup dockhand 2>/dev/null || true
|
||||||
|
|
||||||
|
# Check for UID conflicts - warn but don't delete other users
|
||||||
|
if getent passwd "$PUID" >/dev/null 2>&1; then
|
||||||
|
EXISTING=$(getent passwd "$PUID" | cut -d: -f1)
|
||||||
|
echo "WARNING: UID $PUID already in use by '$EXISTING'. Using default UID 1001."
|
||||||
|
PUID=1001
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle GID - reuse existing group or create new
|
||||||
|
if getent group "$PGID" >/dev/null 2>&1; then
|
||||||
|
TARGET_GROUP=$(getent group "$PGID" | cut -d: -f1)
|
||||||
|
else
|
||||||
|
addgroup -g "$PGID" dockhand
|
||||||
|
TARGET_GROUP="dockhand"
|
||||||
|
fi
|
||||||
|
|
||||||
|
adduser -u "$PUID" -G "$TARGET_GROUP" -h /home/dockhand -D dockhand
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Directory Ownership ===
|
||||||
|
chown -R dockhand:dockhand /app/data /home/dockhand 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ -n "$DATA_DIR" ] && [ "$DATA_DIR" != "/app/data" ] && [ "$DATA_DIR" != "./data" ]; then
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
chown -R dockhand:dockhand "$DATA_DIR" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Docker Socket Access (Optional) ===
|
||||||
|
# Check if Docker socket is mounted and accessible
|
||||||
|
# Socket path can be configured via environment-specific settings in the app
|
||||||
|
SOCKET_PATH="/var/run/docker.sock"
|
||||||
|
|
||||||
|
if [ -S "$SOCKET_PATH" ]; then
|
||||||
|
# Socket exists - check if readable
|
||||||
|
if [ "$RUN_USER" != "root" ]; then
|
||||||
|
if ! su-exec "$RUN_USER" test -r "$SOCKET_PATH" 2>/dev/null; then
|
||||||
|
SOCKET_GID=$(stat -c '%g' "$SOCKET_PATH" 2>/dev/null || echo "unknown")
|
||||||
|
echo "WARNING: Docker socket at $SOCKET_PATH is not readable by dockhand user"
|
||||||
|
echo ""
|
||||||
|
echo "To use local Docker, fix with one of these options:"
|
||||||
|
echo ""
|
||||||
|
echo " 1. Add container to docker group (GID: $SOCKET_GID):"
|
||||||
|
echo " docker run --group-add $SOCKET_GID ..."
|
||||||
|
echo ""
|
||||||
|
echo " 2. Use a socket proxy:"
|
||||||
|
echo " Configure a 'direct' environment pointing to tcp://socket-proxy:2375"
|
||||||
|
echo ""
|
||||||
|
echo " 3. Make socket world-readable (less secure):"
|
||||||
|
echo " chmod 666 /var/run/docker.sock"
|
||||||
|
echo ""
|
||||||
|
echo "Continuing startup - configure environments via the web UI..."
|
||||||
|
else
|
||||||
|
echo "Docker socket accessible at $SOCKET_PATH"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Docker socket accessible at $SOCKET_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Detect Docker Host Hostname (for license validation) ===
|
||||||
|
# Query Docker API to get the real host hostname (not container ID)
|
||||||
|
if [ -z "$DOCKHAND_HOSTNAME" ]; then
|
||||||
|
DETECTED_HOSTNAME=$(curl -s --unix-socket "$SOCKET_PATH" http://localhost/info 2>/dev/null | sed -n 's/.*"Name":"\([^"]*\)".*/\1/p')
|
||||||
|
if [ -n "$DETECTED_HOSTNAME" ]; then
|
||||||
|
export DOCKHAND_HOSTNAME="$DETECTED_HOSTNAME"
|
||||||
|
echo "Detected Docker host hostname: $DOCKHAND_HOSTNAME"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Using configured hostname: $DOCKHAND_HOSTNAME"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No Docker socket found at $SOCKET_PATH"
|
||||||
|
echo "Configure Docker environments via the web UI (Settings > Environments)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Run Application ===
|
||||||
|
if [ "$RUN_USER" = "root" ]; then
|
||||||
|
# Running as root - execute directly
|
||||||
|
if [ "$1" = "" ]; then
|
||||||
|
exec bun run ./build/index.js
|
||||||
|
else
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Running as dockhand user
|
||||||
|
if [ "$1" = "" ]; then
|
||||||
|
exec su-exec dockhand bun run ./build/index.js
|
||||||
|
else
|
||||||
|
exec su-exec dockhand "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
16
drizzle.config.ts
Normal file
16
drizzle.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
const isPostgres = databaseUrl && (databaseUrl.startsWith('postgres://') || databaseUrl.startsWith('postgresql://'));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Use different schema files for SQLite vs PostgreSQL
|
||||||
|
schema: isPostgres
|
||||||
|
? './src/lib/server/db/schema/pg-schema.ts'
|
||||||
|
: './src/lib/server/db/schema/index.ts',
|
||||||
|
out: isPostgres ? './drizzle-pg' : './drizzle',
|
||||||
|
dialect: isPostgres ? 'postgresql' : 'sqlite',
|
||||||
|
dbCredentials: isPostgres
|
||||||
|
? { url: databaseUrl! }
|
||||||
|
: { url: `file:${process.env.DATA_DIR || './data'}/dockhand.db` }
|
||||||
|
});
|
||||||
110
package.json
Normal file
110
package.json
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"name": "dockhand",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.4",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bunx --bun vite dev",
|
||||||
|
"prebuild": "bunx license-checker --json --production | jq 'to_entries | map({name: (.key | split(\"@\")[0:-1] | join(\"@\")), version: (.key | split(\"@\")[-1]), license: .value.licenses, repository: .value.repository}) | sort_by(.name)' > src/lib/data/dependencies.json.tmp && mv src/lib/data/dependencies.json.tmp src/lib/data/dependencies.json || true",
|
||||||
|
"build": "bunx --bun vite build && bun scripts/patch-build.ts && bun scripts/build-subprocesses.ts",
|
||||||
|
"start": "bun ./build/index.js",
|
||||||
|
"preview": "bun ./build/index.js",
|
||||||
|
"prepare": "bunx --bun svelte-kit sync || echo ''",
|
||||||
|
"check": "bunx --bun svelte-kit sync && bunx --bun svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "bunx --bun svelte-kit sync && bunx --bun svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"test": "bun test",
|
||||||
|
"test:smoke": "bun test tests/api-smoke.test.ts",
|
||||||
|
"test:containers": "bun test tests/container-lifecycle.test.ts",
|
||||||
|
"test:notifications": "bun test tests/notifications.test.ts",
|
||||||
|
"test:hawser": "bun test tests/hawser-connection.test.ts",
|
||||||
|
"test:build": "SKIP_BUILD_TEST=1 bun test tests/build.test.ts",
|
||||||
|
"test:postgres": "bun test tests/database-postgres.test.ts",
|
||||||
|
"test:crud": "bun test tests/crud-operations.test.ts",
|
||||||
|
"test:scheduling": "bun test tests/scheduling.test.ts",
|
||||||
|
"test:images": "bun test tests/images.test.ts",
|
||||||
|
"test:volumes": "bun test tests/volumes-networks.test.ts",
|
||||||
|
"test:stacks": "bun test tests/stacks.test.ts",
|
||||||
|
"test:stacks:matrix": "bun test tests/stack-matrix.test.ts",
|
||||||
|
"test:stacks:git": "bun test tests/stack-git-flow.test.ts",
|
||||||
|
"test:stacks:env": "bun test tests/stack-env-vars.test.ts",
|
||||||
|
"test:stacks:all": "bun test tests/stack-*.test.ts tests/stacks.test.ts",
|
||||||
|
"test:files": "bun test tests/container-files.test.ts",
|
||||||
|
"test:license": "bun test tests/license.test.ts",
|
||||||
|
"test:activity": "bun test tests/activity-dashboard.test.ts",
|
||||||
|
"test:all": "bun test tests/",
|
||||||
|
"test:quick": "bun test tests/api-smoke.test.ts tests/notifications.test.ts",
|
||||||
|
"test:integration": "bun test tests/api-smoke.test.ts tests/crud-operations.test.ts tests/scheduling.test.ts tests/hawser-connection.test.ts",
|
||||||
|
"test:e2e": "bunx playwright test tests/e2e/",
|
||||||
|
"generate:legal": "bun scripts/generate-legal-pages.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "6.20.0",
|
||||||
|
"@codemirror/commands": "6.10.0",
|
||||||
|
"@codemirror/lang-css": "6.3.1",
|
||||||
|
"@codemirror/lang-html": "6.4.11",
|
||||||
|
"@codemirror/lang-javascript": "6.2.4",
|
||||||
|
"@codemirror/lang-json": "6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "6.5.0",
|
||||||
|
"@codemirror/lang-python": "6.2.1",
|
||||||
|
"@codemirror/lang-sql": "6.10.0",
|
||||||
|
"@codemirror/lang-xml": "6.1.0",
|
||||||
|
"@codemirror/language": "6.11.3",
|
||||||
|
"@codemirror/search": "6.5.11",
|
||||||
|
"@lezer/highlight": "1.2.3",
|
||||||
|
"@lucide/lab": "^0.1.2",
|
||||||
|
"croner": "9.1.0",
|
||||||
|
"cronstrue": "3.9.0",
|
||||||
|
"drizzle-orm": "0.45.0",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"ldapts": "^8.0.9",
|
||||||
|
"nodemailer": "^7.0.11",
|
||||||
|
"otpauth": "^9.4.1",
|
||||||
|
"postgres": "3.4.7",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"svelte-dnd-action": "0.9.68",
|
||||||
|
"svelte-sonner": "1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.38.8",
|
||||||
|
"@internationalized/date": "^3.10.0",
|
||||||
|
"@layerstack/tailwind": "^1.0.1",
|
||||||
|
"@lucide/svelte": "^0.544.0",
|
||||||
|
"@playwright/test": "1.57.0",
|
||||||
|
"@sveltejs/kit": "^2.48.5",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
|
"@types/bun": "^1.2.5",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/nodemailer": "^7.0.4",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"bits-ui": "^2.14.4",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"cytoscape": "^3.33.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
|
"drizzle-kit": "0.31.8",
|
||||||
|
"layerchart": "^1.0.12",
|
||||||
|
"lucide-svelte": "^0.555.0",
|
||||||
|
"mode-watcher": "^1.1.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"svelte": "^5.43.8",
|
||||||
|
"svelte-adapter-bun": "1.0.1",
|
||||||
|
"svelte-check": "^4.3.4",
|
||||||
|
"svelte-easy-crop": "^5.0.0",
|
||||||
|
"svelte-virtual-scroll-list": "^1.3.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
app.d.ts → src/app.d.ts
vendored
0
app.d.ts → src/app.d.ts
vendored
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user