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
112 changes: 70 additions & 42 deletions QUICKSTART-DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,20 @@ This guide will help you get the Unkey deployment platform up and running locall
## Prerequisites

- Docker and Docker Compose
- Go 1.24 or later
- A terminal/command line

## Step 1: Start the Platform

1. Start all services using Docker Compose:

```bash
docker-compose up -d
docker-compose up metald-aio dashboard ctrl -d
```

This will start:
2. Wait for all services to be healthy

- MySQL database (port 3306)
- Dashboard (port 3000)
- Control plane services
- Supporting infrastructure

2. Wait for all services to be healthy (this may take 1-2 minutes):

```bash
docker-compose ps
```
The platform now uses a Docker backend that creates containers instead of VMs, making it much faster and easier to run locally.

## Step 2: Set Up Your Workspace

Expand All @@ -36,31 +28,17 @@ docker-compose ps
http://localhost:3000
```

2. Sign in or create an account through the authentication flow
2. Create a workspace and copy its id

3. Once logged in, you'll automatically have a workspace created. Navigate to:
3. Create a new project by filling out the form:

```
http://localhost:3000/projects
```
Go to http://localhost:3000/projects

4. Create a new project by filling out the form:
- **Name**: Choose any name (e.g., "My Test App")
- **Slug**: This will auto-generate based on the name
- **Git URL**: Optional, leave blank for testing

- **Name**: Choose any name (e.g., "My Test App")
- **Slug**: This will auto-generate based on the name
- **Git URL**: Optional, leave blank for testing

5. After creating the project, **copy the Project ID** from the project details. It will look like:

```
proj_xxxxxxxxxxxxxxxxxx
```

6. Also note your **Workspace ID** (you can find this settings). It will look like:

```
ws_xxxxxxxxxxxxxxxxxx
```
4. After creating the project, **copy the Project ID** from the project details. It will look like:

## Step 3: Deploy a Version

Expand All @@ -82,21 +60,71 @@ go run . version create \
Keep the context as shown, there's a demo api in that folder.
Replace `YOUR_WORKSPACE_ID` and `YOUR_PROJECT_ID` with the actual values you copied from the dashboard.

3. The CLI will show real-time progress as your deployment goes through these stages:
- Downloading Docker image
- Building rootfs
- Uploading rootfs
- Creating VM
- Booting VM
- Assigning domains
- Completed
3. The CLI will:
- Always build a fresh Docker image from your code
- Set the PORT environment variable to 8080 in the container
- Use the Docker backend to create a container instead of a VM
- Automatically allocate a random host port (e.g., 35432) to avoid conflicts
- Show real-time progress as your deployment goes through the stages

## Step 4: View Your Deployment

1. Return to the dashboard and navigate to:
1. Once the deployment completes, the CLI will show you the available domains:

```
Deployment Complete
Version ID: v_xxxxxxxxxxxxxxxxxx
Status: Ready
Environment: Production

Domains
https://main-commit-workspace.unkey.app
http://localhost:35432
```

2. If you're using the `demo_api` you can curl the `/v1/liveness` endpoint
3. Return to the dashboard and navigate to:

```
http://localhost:3000/versions
http://localhost:3000/deployments
```

### Important: Your Application Must Listen on the PORT Environment Variable

**Your deployed application MUST read the `PORT` environment variable and listen on that port.** The platform sets `PORT=8080` in the container, and your code needs to use this value.

**Example for different languages:**

```javascript
// Node.js
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
```

```python
# Python
import os
port = int(os.environ.get('PORT', 3000))
app.run(host='0.0.0.0', port=port)
```

```go
// Go
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
http.ListenAndServe(":"+port, handler)
```

The demo_api already follows this pattern and listens on the PORT environment variable.

## Troubleshooting

- If you see "port is already allocated" errors, the system will automatically retry with a new random port
- Check container logs: `docker logs <container-name>`
- Verify the demo_api is listening on the PORT environment variable (should be 8080)
- Make sure your Dockerfile exposes the correct port (8080 in the demo_api example)
50 changes: 30 additions & 20 deletions go/cmd/version/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ var createCmd = &cli.Command{
Usage: "Docker image tag (e.g., ghcr.io/user/app:tag). If not provided, builds from current directory",
Required: false,
},
&cli.BoolFlag{
Name: "force-build",
Usage: "Force build Docker image even if --docker-image is provided",
},
&cli.StringFlag{
Name: "dockerfile",
Usage: "Path to Dockerfile",
Expand Down Expand Up @@ -123,29 +127,34 @@ func createAction(ctx context.Context, cmd *cli.Command) error {
dockerfile := cmd.String("dockerfile")
buildContext := cmd.String("context")

// Always build the image, ignoring any provided docker-image
dockerImage = ""

return runDeploymentSteps(ctx, cmd, workspaceID, projectID, branch, dockerImage, dockerfile, buildContext, commit, logger)
}

func printDeploymentComplete(versionID, workspace, branch string) {
// Use actual Git info for hostname generation
gitInfo := git.GetInfo()
identifier := versionID
if gitInfo.IsRepo && gitInfo.CommitSHA != "" {
identifier = gitInfo.CommitSHA
}

func printDeploymentComplete(version *ctrlv1.Version) {
fmt.Println()
fmt.Println("Deployment Complete")
fmt.Printf(" Version ID: %s\n", versionID)
fmt.Printf(" Version ID: %s\n", version.GetId())
fmt.Printf(" Status: Ready\n")
fmt.Printf(" Environment: Production\n")

fmt.Println()
fmt.Println("Domains")
// Replace underscores with dashes for valid hostname format
cleanIdentifier := strings.ReplaceAll(identifier, "_", "-")
fmt.Printf(" https://%s-%s-%s.unkey.app\n", branch, cleanIdentifier, workspace)
fmt.Printf(" https://api.acme.com\n")
hostnames := version.GetHostnames()
if len(hostnames) > 0 {
for _, hostname := range hostnames {
// Check if it's a localhost hostname (don't add https://)
if strings.HasPrefix(hostname, "localhost:") {
fmt.Printf(" http://%s\n", hostname)
} else {
fmt.Printf(" https://%s\n", hostname)
}
}
} else {
fmt.Printf(" No hostnames assigned\n")
}
}

func runDeploymentSteps(ctx context.Context, cmd *cli.Command, workspace, project, branch, dockerImage, dockerfile, buildContext, commit string, logger logging.Logger) error {
Expand Down Expand Up @@ -324,17 +333,18 @@ func runDeploymentSteps(ctx context.Context, cmd *cli.Command, workspace, projec
fmt.Printf(" Version ID: %s\n", versionID)

// Poll for version status updates
if err := pollVersionStatus(ctx, logger, client, versionID); err != nil {
finalVersion, err := pollVersionStatus(ctx, logger, client, versionID)
if err != nil {
return fmt.Errorf("deployment failed: %w", err)
}

printDeploymentComplete(versionID, workspace, branch)
printDeploymentComplete(finalVersion)

return nil
}

// pollVersionStatus polls the control plane API and displays deployment steps as they occur
func pollVersionStatus(ctx context.Context, logger logging.Logger, client ctrlv1connect.VersionServiceClient, versionID string) error {
func pollVersionStatus(ctx context.Context, logger logging.Logger, client ctrlv1connect.VersionServiceClient, versionID string) (*ctrlv1.Version, error) {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()

Expand All @@ -346,10 +356,10 @@ func pollVersionStatus(ctx context.Context, logger logging.Logger, client ctrlv1
for {
select {
case <-ctx.Done():
return ctx.Err()
return nil, ctx.Err()
case <-timeout.C:
fmt.Printf("Error: Deployment timeout after 5 minutes\n")
return fmt.Errorf("deployment timeout")
return nil, fmt.Errorf("deployment timeout")
case <-ticker.C:
// Always poll version status
getReq := connect.NewRequest(&ctrlv1.GetVersionRequest{
Expand Down Expand Up @@ -377,12 +387,12 @@ func pollVersionStatus(ctx context.Context, logger logging.Logger, client ctrlv1

// Check if deployment is complete
if version.GetStatus() == ctrlv1.VersionStatus_VERSION_STATUS_ACTIVE {
return nil
return version, nil
}

// Check if deployment failed
if version.GetStatus() == ctrlv1.VersionStatus_VERSION_STATUS_FAILED {
return fmt.Errorf("deployment failed")
return nil, fmt.Errorf("deployment failed")
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion go/demo_api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ WORKDIR /root/

COPY --from=builder /app/main .

EXPOSE 8080
ENV PORT 8080

CMD ["./main"]
60 changes: 26 additions & 34 deletions go/deploy/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,61 +1,53 @@
# Dockerfile.dev - Development environment for all Unkey deploy services
# Based on LOCAL_DEPLOYMENT_GUIDE.md for maximum production parity

# Build stage - compile all services
FROM fedora:42 AS builder
# Install stage - install all dependencies once
FROM fedora:42 AS install

# Install development tools (following LOCAL_DEPLOYMENT_GUIDE.md)
# Install all dependencies (dev tools + runtime deps + Docker CLI)
RUN dnf install -y dnf-plugins-core && \
dnf group install -y development-tools && \
dnf install -y git make golang curl wget iptables-legacy && \
dnf install -y git make golang curl wget iptables-legacy \
systemd systemd-devel procps-ng util-linux && \
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo && \
dnf install -y docker-ce-cli && \
dnf clean all


# Set up Go environment
ENV GOPATH=/go
ENV PATH=$PATH:/go/bin:/usr/local/go/bin

# Base build stage with source code
FROM install AS build-base

# Copy source code
COPY . /src/go
WORKDIR /src/go

# Protobuf files are already generated in go/proto/ - no need to generate them again

# Build all services directly using go build (protobufs already generated)
# Go will download dependencies as needed during build
# Build assetmanagerd
FROM build-base AS build-assetmanagerd
WORKDIR /src/go/deploy/assetmanagerd
RUN go build -o assetmanagerd ./cmd/assetmanagerd

# Build billaged
FROM build-base AS build-billaged
WORKDIR /src/go/deploy/billaged
RUN go build -o billaged ./cmd/billaged

# Build builderd
FROM build-base AS build-builderd
WORKDIR /src/go/deploy/builderd
RUN go build -o builderd ./cmd/builderd

# Build metald
FROM build-base AS build-metald
WORKDIR /src/go/deploy/metald
RUN go build -o metald ./cmd/metald

# Runtime stage - Fedora with systemd
FROM fedora:42

# Install runtime dependencies
RUN dnf update -y && \
dnf install -y \
systemd \
systemd-devel \
iptables-legacy \
curl \
wget \
procps-ng \
util-linux \
&& \
dnf clean all

# Install Docker CLI for metald Docker backend
RUN dnf install -y dnf-plugins-core && \
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo && \
dnf install -y docker-ce-cli && \
dnf clean all
# Runtime stage - reuse install stage (all deps already installed)
FROM install AS runtime

# Create billaged user (following systemd service requirements)
RUN useradd -r -s /bin/false billaged
Expand All @@ -72,11 +64,11 @@ RUN mkdir -p /opt/assetmanagerd/{cache,data} && \
# Set ownership for service directories
RUN chown -R billaged:billaged /opt/billaged /var/log/billaged

# Copy built binaries from builder stage
COPY --from=builder /src/go/deploy/assetmanagerd/assetmanagerd /usr/local/bin/
COPY --from=builder /src/go/deploy/billaged/billaged /usr/local/bin/
COPY --from=builder /src/go/deploy/builderd/builderd /usr/local/bin/
COPY --from=builder /src/go/deploy/metald/metald /usr/local/bin/
# Copy built binaries from respective build stages
COPY --from=build-assetmanagerd /src/go/deploy/assetmanagerd/assetmanagerd /usr/local/bin/
COPY --from=build-billaged /src/go/deploy/billaged/billaged /usr/local/bin/
COPY --from=build-builderd /src/go/deploy/builderd/builderd /usr/local/bin/
COPY --from=build-metald /src/go/deploy/metald/metald /usr/local/bin/


# Make binaries executable
Expand Down Expand Up @@ -217,7 +209,7 @@ LABEL org.unkey.component="deploy-services" \
# AIDEV-NOTE: This Dockerfile follows the LOCAL_DEPLOYMENT_GUIDE.md as closely as possible
# Key features:
# 1. Uses Fedora 42 (production parity)
# 2. Multi-stage build with development tools
# 2. Multi-stage build with parallel service compilation for faster builds
# 3. systemd as process manager
# 4. All services built using existing Makefiles
# 5. TLS disabled for development
Expand Down
Loading