17 KiB
Coolify CLI Architecture
This document describes the architecture and design principles of the Coolify CLI.
Overview
The Coolify CLI is a command-line interface for managing Coolify instances, servers, projects, and deployments. It follows a layered architecture pattern that separates concerns and promotes maintainability.
Architecture Layers
┌─────────────────────────────────────────────────────────┐
│ User Interface │
│ (Terminal/Shell) │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Command Layer (cmd/) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ servers │ │ deploy │ │ projects │ ... │
│ └──────────┘ └──────────┘ └──────────┘ │
│ • CLI parsing & validation │
│ • Flag handling │
│ • Output formatting │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Service Layer (internal/service/) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ServerService│ │DeployService │ │ProjectService│ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ • Business logic │
│ • Request validation │
│ • Response transformation │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ API Client Layer (internal/api/) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ HTTP Client (api.Client) │ │
│ └───────────────────────────────────────────────────┘ │
│ • HTTP requests/responses │
│ • Authentication (Bearer tokens) │
│ • Retry logic │
│ • Error handling │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Coolify API (External) │
│ https://instance.coolify.io/api/v1/ │
└─────────────────────────────────────────────────────────┘
Supporting Components
┌─────────────────────────────────────────────────────────┐
│ Configuration (internal/config/) │
│ • Multi-instance management │
│ • Default instance selection │
│ • Token storage │
│ • ~/.config/coolify/config.json │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Output Formatters (internal/output/) │
│ ┌─────────┐ ┌────────┐ ┌─────────┐ │
│ │ Table │ │ JSON │ │ Pretty │ │
│ └─────────┘ └────────┘ └─────────┘ │
│ • Flexible output formats │
│ • Sensitive data masking │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Data Models (internal/models/) │
│ • Server, Project, Resource, Deployment │
│ • Request/Response structures │
│ • JSON marshaling/unmarshaling │
└─────────────────────────────────────────────────────────┘
Layer Responsibilities
1. Command Layer (cmd/)
Purpose: Handle CLI user interface and interaction
Responsibilities:
- Parse command-line arguments and flags
- Validate user input
- Coordinate with service layer
- Format and display output
- Handle errors gracefully
Key Files:
root.go- Root command, global flags, initializationservers.go- Server management commandsdeploy.go- Deployment commandscontext.go- Context (instance) configuration commandsprojects.go- Project listing and inspection- etc.
Example:
var serversListCmd = &cobra.Command{
Use: "list",
Short: "List all servers",
RunE: func(cmd *cobra.Command, args []string) error {
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return err
}
// Use service layer
service := service.NewServerService(client)
servers, err := service.List(cmd.Context())
if err != nil {
return err
}
// Format and display output
formatter, _ := getFormatter(cmd)
return formatter.Format(servers)
},
}
2. Service Layer (internal/service/)
Purpose: Implement business logic and coordinate API calls
Responsibilities:
- Validate business rules
- Coordinate multiple API calls if needed
- Transform API responses to CLI-friendly format
- Handle service-specific error cases
Key Files:
server.go- Server operationsdeployment.go- Deployment operationsproject.go- Project operationsresource.go- Resource operationsprivatekey.go- SSH key operationsdomain.go- Domain operations
Example:
type ServerService struct {
client *api.Client
}
func (s *ServerService) List(ctx context.Context) ([]models.Server, error) {
var servers []models.Server
err := s.client.Get(ctx, "servers", &servers)
return servers, err
}
3. API Client Layer (internal/api/)
Purpose: Handle all HTTP communication with Coolify API
Responsibilities:
- Construct HTTP requests
- Add authentication headers
- Retry failed requests with exponential backoff
- Parse HTTP responses
- Convert HTTP errors to meaningful error messages
Key Files:
client.go- HTTP client implementationerror.go- API error handlingoptions.go- Client configuration options
Example:
type Client struct {
baseURL string
token string
httpClient *http.Client
retries int
timeout time.Duration
}
func (c *Client) Get(ctx context.Context, path string, result interface{}) error {
return c.doRequest(ctx, "GET", path, nil, result)
}
4. Configuration Layer (internal/config/)
Purpose: Manage CLI configuration and multiple instances
Responsibilities:
- Load/save configuration from disk
- Manage multiple Coolify instances
- Select default instance
- Store API tokens securely (file permissions)
Key Files:
config.go- Configuration structure and methodsinstance.go- Instance definitionloader.go- File I/O operations
Configuration File (~/.config/coolify/config.json):
{
"instances": [
{
"name": "prod",
"fqdn": "https://coolify.example.com",
"token": "your-api-token",
"default": true
},
{
"name": "staging",
"fqdn": "https://staging.coolify.example.com",
"token": "staging-token"
}
]
}
5. Output Layer (internal/output/)
Purpose: Format data for display to users
Responsibilities:
- Format data as tables, JSON, or pretty-printed JSON
- Hide sensitive information unless
--show-sensitiveis used - Handle different data types (slices, structs, primitives)
Key Files:
formatter.go- Formatter interfacetable.go- Table formattingjson.go- JSON formattingpretty.go- Pretty JSON formatting
Supported Formats:
table- Default, human-readable tablesjson- Compact JSON for scriptingpretty- Indented JSON for debugging
6. Models Layer (internal/models/)
Purpose: Define data structures
Responsibilities:
- Define API request/response structures
- JSON tags for marshaling
- Common types and timestamps
Key Files:
server.go- Server-related typesproject.go- Project-related typesresource.go- Resource typesdeployment.go- Deployment typescommon.go- Shared types
Data Flow
Example: Listing Servers
-
User Input:
coolify servers list --format=table -
Command Layer (
cmd/servers.go):- Cobra parses the command
serversListCmd.RunEis executed- Gets API client using
getAPIClient() - Creates ServerService instance
-
Service Layer (
internal/service/server.go):ServerService.List()is called- Validates context (if needed)
- Calls API client
-
API Client Layer (
internal/api/client.go):- Constructs GET request to
/api/v1/servers - Adds Bearer token authentication
- Sends HTTP request
- Retries on failure (with backoff)
- Parses JSON response
- Constructs GET request to
-
Response Processing:
- JSON unmarshaled to
[]models.Server - Returns to service layer
- Returns to command layer
- JSON unmarshaled to
-
Output Layer (
internal/output/table.go):- Command layer creates table formatter
- Formatter processes server data
- Formats as table with columns
- Writes to stdout
-
User Output: Table displayed in terminal
Design Patterns
1. Dependency Injection
Services receive the API client as a constructor parameter:
func NewServerService(client *api.Client) *ServerService {
return &ServerService{client: client}
}
Benefits:
- Easy to test (can inject mock client)
- Clear dependencies
- Flexible configuration
2. Strategy Pattern (Output Formatters)
Different formatters implement the same interface:
type Formatter interface {
Format(data interface{}) error
}
Benefits:
- Easy to add new formats
- Consistent API
- Runtime format selection
3. Options Pattern (API Client)
Client configuration uses functional options:
client := api.NewClient(url, token,
api.WithDebug(true),
api.WithRetries(5),
api.WithTimeout(60 * time.Second),
)
Benefits:
- Optional parameters
- Clear intent
- Backward compatible
4. Error Wrapping
Errors are wrapped with context at each layer:
if err != nil {
return fmt.Errorf("failed to list servers: %w", err)
}
Benefits:
- Error context preserved
- Stack trace maintained
- Better debugging
Testing Strategy
Unit Tests
Each layer has comprehensive unit tests:
- Commands: Mock services, test flag parsing
- Services: Mock API client, test business logic
- API Client: Use
httptest.Server, test HTTP handling - Config: Test file I/O with temp directories
- Output: Test formatting with buffers
Integration Tests
Test multiple layers together:
- Commands + Services + Mock API
- Config + File System
- End-to-end workflows
Coverage Goals
- Overall: 70%+
- New features: 80%+
- Critical paths: 90%+
Configuration Files
CLI Configuration
Location: ~/.config/coolify/config.json (Linux/macOS)
Location: %APPDATA%\coolify\config.json (Windows)
Structure:
{
"instances": [
{
"name": "prod",
"fqdn": "https://coolify.example.com",
"token": "your-token",
"default": true
}
],
"lastUpdateCheckTime": "2025-01-15T10:30:00Z"
}
API Communication
Base URL
All API calls use: {fqdn}/api/v1/{endpoint}
Example: https://coolify.example.com/api/v1/servers
Authentication
Bearer token authentication:
Authorization: Bearer {token}
Request/Response
Content-Type: application/json
Request Body (POST):
{
"name": "my-server",
"ip": "192.168.1.100"
}
Response Body:
{
"uuid": "abc123",
"name": "my-server",
"ip": "192.168.1.100"
}
Error Handling
HTTP errors are converted to CLI-friendly messages:
401→ "Unauthenticated. Check your API token."404→ "Resource not found."500→ "Server error. Please try again."
Retry Logic
Failed requests are retried with exponential backoff:
- Attempt 1: Immediate
- Attempt 2: Wait 1s
- Attempt 3: Wait 2s
- Attempt 4: Wait 4s
Does not retry on 4xx errors (except 429 rate limit).
Security Considerations
API Token Storage
- Stored in config file with restricted permissions (0600)
- Never logged (even in debug mode)
- Masked in output by default (use
-sto show)
Sensitive Data Handling
- Tokens masked as
********in output - Use
--show-sensitiveflag to reveal - Debug logs sanitize sensitive data
HTTPS
- All API communication uses HTTPS
- Certificate validation enabled
Performance Optimizations
Concurrent Operations
Batch deployments run in parallel:
// Deploy multiple resources concurrently
var wg sync.WaitGroup
for _, name := range names {
wg.Add(1)
go func(n string) {
defer wg.Done()
deployResource(n)
}(name)
}
wg.Wait()
Connection Reuse
HTTP client reuses connections:
c.httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second,
},
}
Minimal Dependencies
- Use Go standard library when possible
- Only essential external dependencies
- Keep binary size small
Extensibility
Adding a New Command
- Create
cmd/newfeature.go - Define Cobra command
- Create service if needed (
internal/service/newfeature.go) - Add models if needed (
internal/models/newfeature.go) - Register command in
init() - Write tests
Adding a New Output Format
- Create
internal/output/newformat.go - Implement
Formatterinterface - Add format constant
- Update
NewFormatter()switch
Adding API Client Features
- Add method to
internal/api/client.go - Add tests in
internal/api/client_test.go - Use in service layer
Build & Release
Build Process
# Local build
go build -o coolify ./coolify
# Install locally
go install ./coolify
# Multi-platform release
goreleaser release --clean
Release Artifacts
- Linux: amd64, arm64
- macOS: amd64, arm64 (Apple Silicon)
- Windows: amd64
Distribution
- GitHub Releases
- Install script:
scripts/install.sh - Package managers (planned)
Future Enhancements
- Shell completion improvements
- Interactive mode
- Configuration wizard
- Plugin system
- Telemetry (opt-in)
- Cache layer for frequent queries