diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..032ae770fa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,52 @@ +# Dependencies (will be installed fresh) +node_modules/ + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Git +.git/ +.gitignore + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Test artifacts +coverage/ +.nyc_output/ + +# Development files +*.log +*.local +.env +.env.* +!.env.example + +# Documentation build +docs/book/ + +# Mobile app (not needed for server) +mobile/ + +# Electron-specific (not needed for server) +*.dmg +*.exe +*.AppImage +*.snap +*.deb +*.rpm + +# Storybook +storybook-static/ + +# Misc +.DS_Store +Thumbs.db +*.md +!README.md +LICENSE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86bcea98b0..85b02738ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,6 +171,73 @@ jobs: env: ELECTRON_DISABLE_SANDBOX: 1 + docker-smoke-test: + name: Docker Smoke Test + runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }} + # Only run in merge queue (Docker builds are slow) + if: github.event_name == 'merge_group' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for git describe to find tags + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + load: true + tags: mux-server:test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Start container + run: | + docker run -d --name mux-test -p 3000:3000 mux-server:test + # Wait for server to be ready + for i in {1..30}; do + if curl -sf http://localhost:3000/health; then + echo "Server is ready" + break + fi + echo "Waiting for server... ($i/30)" + sleep 1 + done + + - name: Health check + run: | + response=$(curl -sf http://localhost:3000/health) + echo "Health response: $response" + if ! echo "$response" | grep -q '"status":"ok"'; then + echo "Health check failed" + exit 1 + fi + + - name: Version check + run: | + response=$(curl -sf http://localhost:3000/version) + echo "Version response: $response" + # Verify response contains expected fields + if ! echo "$response" | grep -q '"mode":"server"'; then + echo "Version check failed: missing mode=server" + exit 1 + fi + if ! echo "$response" | grep -q '"version"'; then + echo "Version check failed: missing version field" + exit 1 + fi + + - name: Show container logs on failure + if: failure() + run: docker logs mux-test + + - name: Cleanup + if: always() + run: docker rm -f mux-test || true + check-codex-comments: name: Check Codex Comments runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..eeb113bda9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,103 @@ +# mux server Docker image +# Multi-stage build for minimal runtime image size +# +# Build: docker build -t mux-server . +# Run: docker run -p 3000:3000 -v ~/.mux:/root/.mux mux-server +# +# See docker-compose.yml for easier orchestration + +# ============================================================================== +# Stage 1: Build +# ============================================================================== +FROM node:22-slim AS builder + +WORKDIR /app + +# Install bun (used for package management and build tooling) +RUN npm install -g bun@1.2 + +# Install git (needed for version generation) +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +# Copy package files first for better layer caching +COPY package.json bun.lock bunfig.toml ./ + +# Copy postinstall script (needed by bun install) +COPY scripts/postinstall.sh scripts/ + +# Install build tools needed for native modules +RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* + +# Install dependencies (postinstall detects server mode and skips Electron rebuild) +# Note: node-pty is in optionalDependencies and will be built for Node.js +RUN bun install --frozen-lockfile + +# Copy source files needed for build +COPY src/ src/ +COPY tsconfig.json tsconfig.main.json ./ +COPY scripts/generate-version.sh scripts/ +COPY index.html terminal.html vite.config.ts ./ +COPY public/ public/ +COPY static/ static/ + +# Remove test files (they import from tests/ which is outside rootDir) +RUN find src -name '*.test.ts' -delete + +# Initialize git for version script (uses placeholder if not a real repo) +RUN git init && \ + git config user.email "docker@build" && \ + git config user.name "Docker Build" && \ + git add -A && \ + git commit -m "docker build" --allow-empty || true + +# Generate version info +RUN ./scripts/generate-version.sh + +# Build main process (server + backend) +RUN NODE_ENV=production bun x tsc -p tsconfig.main.json && \ + NODE_ENV=production bun x tsc-alias -p tsconfig.main.json + +# Build renderer (frontend) +RUN bun x vite build + +# Copy static assets +RUN mkdir -p dist/static && cp -r static/* dist/static/ 2>/dev/null || true + +# ============================================================================== +# Stage 2: Runtime +# ============================================================================== +FROM node:22-slim + +WORKDIR /app + +# Install runtime dependencies +# - git: required for workspace operations (clone, worktree, etc.) +# - openssh-client: required for SSH runtime support +RUN apt-get update && \ + apt-get install -y git openssh-client && \ + rm -rf /var/lib/apt/lists/* + +# Copy built artifacts from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ + +# Create mux data directory +RUN mkdir -p /root/.mux + +# Default environment variables +ENV NODE_ENV=production +ENV MUX_HOME=/root/.mux + +# Expose server port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "fetch('http://localhost:3000/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" + +# Run mux server +# --host 0.0.0.0: bind to all interfaces (required for Docker networking) +# --port 3000: default port (can be remapped via docker run -p) +ENTRYPOINT ["node", "dist/cli/index.js", "server"] +CMD ["--host", "0.0.0.0", "--port", "3000"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..1ecfa39534 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +# mux server docker-compose configuration +# +# Quick start: +# docker compose up -d +# +# Add a project: +# docker compose run --rm mux-server --add-project /projects/my-repo + +services: + mux-server: + build: . + container_name: mux-server + ports: + - "3000:3000" + volumes: + - mux-data:/root/.mux + # Mount project directories as needed: + # - ~/projects:/projects:rw + restart: unless-stopped + +volumes: + mux-data: