Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ blob-report/
*.pem

docker-compose.override.yml
.claude/
.claude/docker-compose.override.yml
155 changes: 155 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Automaker Multi-Stage Dockerfile
# Single Dockerfile for both server and UI builds
# Usage:
# docker build --target server -t automaker-server .
# docker build --target ui -t automaker-ui .
# Or use docker-compose which selects targets automatically

# =============================================================================
# BASE STAGE - Common setup for all builds (DRY: defined once, used by all)
# =============================================================================
FROM node:22-alpine AS base

# Install build dependencies for native modules (node-pty)
RUN apk add --no-cache python3 make g++

WORKDIR /app

# Copy root package files
COPY package*.json ./

# Copy all libs package.json files (centralized - add new libs here)
COPY libs/types/package*.json ./libs/types/
COPY libs/utils/package*.json ./libs/utils/
COPY libs/prompts/package*.json ./libs/prompts/
COPY libs/platform/package*.json ./libs/platform/
COPY libs/model-resolver/package*.json ./libs/model-resolver/
COPY libs/dependency-resolver/package*.json ./libs/dependency-resolver/
COPY libs/git-utils/package*.json ./libs/git-utils/

# Copy scripts (needed by npm workspace)
COPY scripts ./scripts

# =============================================================================
# SERVER BUILD STAGE
# =============================================================================
FROM base AS server-builder

# Copy server-specific package.json
COPY apps/server/package*.json ./apps/server/

# Install dependencies (--ignore-scripts to skip husky/prepare, then rebuild native modules)
RUN npm ci --ignore-scripts && npm rebuild node-pty

# Copy all source files
COPY libs ./libs
COPY apps/server ./apps/server

# Build packages in dependency order, then build server
RUN npm run build:packages && npm run build --workspace=apps/server

# =============================================================================
# SERVER PRODUCTION STAGE
# =============================================================================
FROM node:22-alpine AS server

# Install git, curl, bash (for terminal), and GitHub CLI (pinned version, multi-arch)
RUN apk add --no-cache git curl bash && \
GH_VERSION="2.63.2" && \
ARCH=$(uname -m) && \
case "$ARCH" in \
x86_64) GH_ARCH="amd64" ;; \
aarch64|arm64) GH_ARCH="arm64" ;; \
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
esac && \
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" -o gh.tar.gz && \
tar -xzf gh.tar.gz && \
mv gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh /usr/local/bin/gh && \
rm -rf gh.tar.gz gh_${GH_VERSION}_linux_${GH_ARCH}

# Install Claude CLI globally
RUN npm install -g @anthropic-ai/claude-code

WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S automaker && \
adduser -S automaker -u 1001

# Copy root package.json (needed for workspace resolution)
COPY --from=server-builder /app/package*.json ./

# Copy built libs (workspace packages are symlinked in node_modules)
COPY --from=server-builder /app/libs ./libs

# Copy built server
COPY --from=server-builder /app/apps/server/dist ./apps/server/dist
COPY --from=server-builder /app/apps/server/package*.json ./apps/server/

# Copy node_modules (includes symlinks to libs)
COPY --from=server-builder /app/node_modules ./node_modules

# Create data and projects directories
RUN mkdir -p /data /projects && chown automaker:automaker /data /projects

# Configure git for mounted volumes and authentication
# Use --system so it's not overwritten by mounted user .gitconfig
RUN git config --system --add safe.directory '*' && \
# Use gh as credential helper (works with GH_TOKEN env var)
git config --system credential.helper '!gh auth git-credential'

# Switch to non-root user
USER automaker

# Environment variables
ENV NODE_ENV=production
ENV PORT=3008
ENV DATA_DIR=/data

# Expose port
EXPOSE 3008

# Health check (using curl since it's already installed, more reliable than busybox wget)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3008/api/health || exit 1

# Start server
CMD ["node", "apps/server/dist/index.js"]

# =============================================================================
# UI BUILD STAGE
# =============================================================================
FROM base AS ui-builder

# Copy UI-specific package.json
COPY apps/ui/package*.json ./apps/ui/

# Install dependencies (--ignore-scripts to skip husky and build:packages in prepare script)
RUN npm ci --ignore-scripts

# Copy all source files
COPY libs ./libs
COPY apps/ui ./apps/ui

# Build packages in dependency order, then build UI
# VITE_SERVER_URL tells the UI where to find the API server
# Use ARG to allow overriding at build time: --build-arg VITE_SERVER_URL=http://api.example.com
ARG VITE_SERVER_URL=http://localhost:3008
ENV VITE_SKIP_ELECTRON=true
ENV VITE_SERVER_URL=${VITE_SERVER_URL}
RUN npm run build:packages && npm run build --workspace=apps/ui

# =============================================================================
# UI PRODUCTION STAGE
# =============================================================================
FROM nginx:alpine AS ui

# Copy built files
COPY --from=ui-builder /app/apps/ui/dist /usr/share/nginx/html

# Copy nginx config for SPA routing
COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
101 changes: 99 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,111 @@ npm run build:electron:linux # Linux (AppImage + DEB, x64)

#### Docker Deployment

Docker provides the most secure way to run Automaker by isolating it from your host filesystem.

```bash
# Build and run with Docker Compose (recommended for security)
# Build and run with Docker Compose
docker-compose up -d

# Access at http://localhost:3007
# Access UI at http://localhost:3007
# API at http://localhost:3008

# View logs
docker-compose logs -f

# Stop containers
docker-compose down
```

##### Configuration

Create a `.env` file in the project root if using API key authentication:

```bash
# Optional: Anthropic API key (not needed if using Claude CLI authentication)
ANTHROPIC_API_KEY=sk-ant-...
```

**Note:** Most users authenticate via Claude CLI instead of API keys. See [Claude CLI Authentication](#claude-cli-authentication-optional) below.

##### Working with Projects (Host Directory Access)

By default, the container is isolated from your host filesystem. To work on projects from your host machine, create a `docker-compose.override.yml` file (gitignored):

```yaml
services:
server:
volumes:
# Mount your project directories
- /path/to/your/project:/projects/your-project
```

##### Claude CLI Authentication (Optional)

To use Claude Code CLI authentication instead of an API key, mount your Claude CLI config directory:

```yaml
services:
server:
volumes:
# Linux/macOS
- ~/.claude:/home/automaker/.claude
# Windows
- C:/Users/YourName/.claude:/home/automaker/.claude
```

**Note:** The Claude CLI config must be writable (do not use `:ro` flag) as the CLI writes debug files.

##### GitHub CLI Authentication (For Git Push/PR Operations)

To enable git push and GitHub CLI operations inside the container:

```yaml
services:
server:
volumes:
# Mount GitHub CLI config
# Linux/macOS
- ~/.config/gh:/home/automaker/.config/gh
# Windows
- 'C:/Users/YourName/AppData/Roaming/GitHub CLI:/home/automaker/.config/gh'

# Mount git config for user identity (name, email)
- ~/.gitconfig:/home/automaker/.gitconfig:ro
environment:
# GitHub token (required on Windows where tokens are in Credential Manager)
# Get your token with: gh auth token
- GH_TOKEN=${GH_TOKEN}
```

Then add `GH_TOKEN` to your `.env` file:

```bash
GH_TOKEN=gho_your_github_token_here
```

##### Complete docker-compose.override.yml Example

```yaml
services:
server:
volumes:
# Your projects
- /path/to/project1:/projects/project1
- /path/to/project2:/projects/project2

# Authentication configs
- ~/.claude:/home/automaker/.claude
- ~/.config/gh:/home/automaker/.config/gh
- ~/.gitconfig:/home/automaker/.gitconfig:ro
environment:
- GH_TOKEN=${GH_TOKEN}
```

##### Architecture Support

The Docker image supports both AMD64 and ARM64 architectures. The GitHub CLI and Claude CLI are automatically downloaded for the correct architecture during build.

### Testing

#### End-to-End Tests (Playwright)
Expand Down
67 changes: 0 additions & 67 deletions apps/server/Dockerfile

This file was deleted.

44 changes: 29 additions & 15 deletions apps/server/src/routes/setup/routes/gh-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,23 +94,37 @@ async function getGhStatus(): Promise<GhStatus> {
// Version command failed
}

// Check authentication status
// Check authentication status by actually making an API call
// gh auth status can return non-zero even when GH_TOKEN is valid
let apiCallSucceeded = false;
try {
const { stdout } = await execAsync('gh auth status', { env: execEnv });
// If this succeeds without error, we're authenticated
status.authenticated = true;

// Try to extract username from output
const userMatch =
stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
if (userMatch) {
status.user = userMatch[1];
const { stdout } = await execAsync('gh api user --jq ".login"', { env: execEnv });
const user = stdout.trim();
if (user) {
status.authenticated = true;
status.user = user;
apiCallSucceeded = true;
}
} catch (error: unknown) {
// Auth status returns non-zero if not authenticated
const err = error as { stderr?: string };
if (err.stderr?.includes('not logged in')) {
// If stdout is empty, fall through to gh auth status fallback
} catch {
// API call failed - fall through to gh auth status fallback
}

// Fallback: try gh auth status if API call didn't succeed
if (!apiCallSucceeded) {
try {
const { stdout } = await execAsync('gh auth status', { env: execEnv });
status.authenticated = true;

// Try to extract username from output
const userMatch =
stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
if (userMatch) {
status.user = userMatch[1];
}
} catch {
// Auth status returns non-zero if not authenticated
status.authenticated = false;
}
}
Expand Down
Loading