Skip to content

Repository files navigation

deploy-everything

A Go toolkit for managing Easypanel deployments — both a CLI for direct tRPC control and a gRPC server that exposes panel-wide deployment operations to authenticated clients.

Architecture

┌─────────────────────────────────────────────────────┐
│  Clients (grpcurl, your app, SDKs)                  │
│  Auth: Bearer token in "authorization" metadata     │
└──────────────────┬──────────────────────────────────┘
                   │ gRPC (port 50051)
┌──────────────────▼──────────────────────────────────┐
│  gRPC PaaS Server (cmd/grpc-server)                 │
│  • Auth interceptor (constant-time token compare)   │
│  • 17 RPC methods                                   │
│  • Compose port sanitization                        │
│  • Auto-domain provisioning                         │
└──────────────────┬──────────────────────────────────┘
                   │ HTTP POST (tRPC)
┌──────────────────▼──────────────────────────────────┐
│  Easypanel (76 tRPC routes)                         │
│  • Docker/Compose orchestration                     │
│  • Traefik reverse proxy + HTTPS                    │
│  • Let's Encrypt auto-certs                         │
└─────────────────────────────────────────────────────┘

Features

  • CLI — Direct Easypanel tRPC control (projects, services, domains, users, certs, settings)
  • gRPC Server — Multi-tenant PaaS adapter with bearer token authentication
  • Container Deployments — Deploy Docker images with auto-assigned domains
  • Compose Stacks — Deploy docker-compose with per-service domain routing (ports auto-stripped for security), and replace the content of a live stack in place
  • Domain Management — Default subdomains + custom domains with HTTPS
  • Lifecycle Control — Start, stop, restart, scale, update env/resources
  • Resource Limits — CPU and memory constraints per service
  • Runtime Status — Live container state, image, and compose sub-services per service
  • Version Pinned — Panel route surface pinned per Easypanel release, checked by tests and at startup

Quick Start

For complete copy-paste examples covering gRPC server setup, Docker images, inline Dockerfiles, Git/GitHub sources, Compose stacks, domains, logs, resources, lifecycle, and troubleshooting, see Deployment Guide.

Prerequisites

  • Go 1.25+
  • Access to an Easypanel instance with API key
  • (Optional) protoc + protoc-gen-go + protoc-gen-go-grpc for proto regeneration

Setup

git clone git@github.com:igun997/deploy-everything.git
cd deploy-everything
cp .env.example .env
# Edit .env with your credentials

Environment Variables

Variable Required Description
EASYPANEL_ENDPOINT Yes Easypanel panel URL (e.g. https://panel.example.com)
EASYPANEL_API_KEY Yes Easypanel API key
EASYPANEL_HTTP_TIMEOUT No Panel request timeout as Go duration (default 5m; deploy calls block while images pull)
GRPC_PORT No gRPC listen port (default: 50051)
GRPC_AUTH_TOKENS Yes* Comma-separated bearer tokens for gRPC clients
DEFAULT_DOMAIN Yes* Easypanel wildcard domain (e.g. xyz.easypanel.host)

*Required for gRPC server only.


CLI Usage

# Build
go build -o deploy-everything .

# Load env
source .env
export EASYPANEL_ENDPOINT EASYPANEL_API_KEY

Projects

./deploy-everything projects list
./deploy-everything projects inspect <project>
./deploy-everything projects create <project>
./deploy-everything projects destroy <project>

Services

# Create services (types: app, mysql, mariadb, postgres, mongo, redis, compose)
./deploy-everything services create <project> <service> <type>
./deploy-everything services destroy <project> <service> <type>

# Docker image management
./deploy-everything services set-image <project> <service> <image>
./deploy-everything services deploy <project> <service> <type>

# Lifecycle
./deploy-everything services start <project> <service> <type>
./deploy-everything services stop <project> <service> <type>
./deploy-everything services restart <project> <service> <type>
./deploy-everything services inspect <project> <service> <type>

# Environment (saves config, then automatically redeploys app/compose)
./deploy-everything services set-env <project> <service> <type> "KEY1=val1" "KEY2=val2"

# App sources (all save, then automatically redeploy)
./deploy-everything services set-git <project> <service> <repo> <ref> [--path /]
./deploy-everything services set-github <project> <service> <owner> <repo> <ref> [--path /]
./deploy-everything services set-dockerfile <project> <service> '<Dockerfile content>'

# Compose sources (save, then automatically redeploy)
./deploy-everything services set-source-inline <project> <service> '<compose content>'
./deploy-everything services set-source-git-compose <project> <service> <repo> <ref> docker-compose.yml [--root-path /]

# Logs
./deploy-everything services logs <project> <service> [--type app|compose] [--compose-service web] [--limit 200]

# WordPress
./deploy-everything wordpress create <project> <service>
./deploy-everything wordpress deploy <project> <service>

Domains

./deploy-everything domains list <project> <service>
./deploy-everything domains create <project> <service> <host> [--type app|compose] [--compose-service web] [--port 80] [--https]
./deploy-everything domains delete <project> <service> <type> <domain-id>

# Domain create/delete automatically redeploys the target app/compose service.
# Easypanel applies domain routing only while generating deployment config.

Other Commands

./deploy-everything whoami
./deploy-everything users list
./deploy-everything actions list
./deploy-everything certs list
./deploy-everything settings ip
./deploy-everything settings domain
./deploy-everything license info

gRPC Server

Build & Run

go build -o grpc-server ./cmd/grpc-server/
./grpc-server
# Output: gRPC PaaS server listening on :50051

Authentication

All gRPC calls require the authorization metadata header with a valid token:

grpcurl -plaintext -H "authorization: your-token" \
  localhost:50051 paas.PaaS/ListServices

Tokens are configured via GRPC_AUTH_TOKENS (comma-separated). Tokens grant panel-wide API access; they are not scoped to individual projects. Use separate Easypanel instances or add an authorization layer when strict tenant isolation is required.

RPC Methods

Method Description
DeployContainer Deploy Docker image → create service + set image + env + deploy + auto-domain
DeployCompose Create and deploy a docker-compose stack (strips ports:, routes via composeService). Fails if the service already exists
UpdateComposeSource Replace the compose content of an existing compose service, then redeploy. Set skip_deploy to store without deploying
DestroyService Remove service and all associated domains
GetServiceStatus Service config (name, type, image, env, domains, deploy URL) plus runtime state (status, running containers, compose sub-services). Env values are redacted unless include_env is set
ListServices All services in a project
AddDomain Attach domain to service (supports compose sub-services)
RemoveDomain Detach domain by ID
ListDomains List all domains for a service
UpdateEnv Set environment variables
RestartService Restart service
StopService Stop service
StartService Start service
GetLogs Fetch app/compose logs with structured entries and flat text. Requires panel log aggregation (see Logs)
ScaleService Set replica count
UpdateResources Set CPU/memory limits
UpdateDeploy Set app replicas, zero-downtime behavior, and command

Examples

Deploy a container

grpcurl -plaintext -H "authorization: your-token" -d '{
  "project": "myproject",
  "service": "webapp",
  "image": "nginx:alpine",
  "env": {"NODE_ENV": "production"},
  "port": 80
}' localhost:50051 paas.PaaS/DeployContainer

Response:

{
  "serviceId": "myproject/webapp",
  "defaultDomain": "myproject-webapp.xyz.easypanel.host",
  "deployUrl": "http://...",
  "status": "deploying"
}

Deploy a compose stack

grpcurl -plaintext -H "authorization: your-token" -d '{
  "project": "myproject",
  "service": "stack",
  "composeContent": "services:\n  web:\n    image: wordpress:6\n    expose:\n      - \"80\"\n  db:\n    image: mysql:8\n    environment:\n      MYSQL_ROOT_PASSWORD: secret\n",
  "domains": [
    {"composeService": "web", "port": 80}
  ]
}' localhost:50051 paas.PaaS/DeployCompose

Update an existing compose stack

DeployCompose creates: it calls the panel's createService first, so it fails with create compose: [BAD_REQUEST] Service already exists. on a service that is already there. Use UpdateComposeSource to replace the content of a live stack.

grpcurl -plaintext -H "authorization: your-token" -d '{
  "project": "myproject",
  "service": "stack",
  "composeContent": "services:\n  web:\n    image: wordpress:6.5\n    expose:\n      - \"80\"\n"
}' localhost:50051 paas.PaaS/UpdateComposeSource
{
  "deployed": true,
  "status": "deploying"
}

The content fully replaces what was stored, and goes through the same gate as DeployCompose: invalid YAML and network_mode: host are rejected, host-published ports: are stripped, expose: is kept. Set "skipDeploy": true to store the content without redeploying, then deploy once after a batch of edits. A service that does not exist, or exists under another type, returns NotFound.

Add a custom domain

grpcurl -plaintext -H "authorization: your-token" -d '{
  "project": "myproject",
  "service": "webapp",
  "host": "app.yourdomain.com",
  "port": 80,
  "https": true
}' localhost:50051 paas.PaaS/AddDomain

Scale service

grpcurl -plaintext -H "authorization: your-token" -d '{
  "project": "myproject",
  "service": "webapp",
  "serviceType": "app",
  "replicas": 3
}' localhost:50051 paas.PaaS/ScaleService

Project Structure

.
├── main.go                     # CLI entrypoint
├── cmd/
│   ├── root.go                 # Cobra root command
│   ├── panel_client.go         # Direct Easypanel tRPC HTTP client
│   ├── projects.go             # CLI: project commands
│   ├── services.go             # CLI: service commands (app/db/compose)
│   ├── domains.go              # CLI: domain commands
│   ├── actions.go              # CLI: deployment actions
│   ├── users.go                # CLI: user management
│   ├── settings.go             # CLI: panel settings
│   ├── certs.go                # CLI: certificates
│   ├── license.go              # CLI: license info
│   ├── wordpress.go            # CLI: WordPress shortcuts
│   ├── whoami.go               # CLI: auth check
│   ├── version.go              # CLI: version command (ldflags metadata)
│   └── grpc-server/
│       └── main.go             # gRPC server entrypoint
├── internal/
│   ├── auth/
│   │   └── interceptor.go     # gRPC auth interceptor (unary + stream)
│   ├── easypanel/
│   │   ├── client.go          # Reusable Easypanel HTTP client
│   │   ├── routes.go          # Every panel route this project calls
│   │   ├── version.go         # Panel version probe + supported range + log settings
│   │   └── testdata/          # Pinned panel route surfaces (2.32.2 / 2.33.0 / 2.33.1)
│   ├── server/
│   │   ├── paas.go            # PaaS gRPC service implementation
│   │   └── logs.go            # Panel log response flattening
│   └── version/
│       └── version.go         # Build metadata injected via -ldflags
├── proto/
│   ├── paas.proto             # Protobuf service definition
│   ├── paas.pb.go             # Generated message types
│   └── paas_grpc.pb.go        # Generated gRPC stubs
├── .github/workflows/
│   ├── ci.yml                 # PR checks: fmt, vet, test, build, image build
│   └── release.yml            # master -> :dev images, tags -> release + binaries
├── scripts/
│   ├── panel-surface.sh       # Re-pin panel route surfaces from official images
│   └── extract-panel-surface.py  # Route/input extractor for the panel bundle
├── Dockerfile                 # Multi-stage, multi-arch (targets: server, cli)
├── Makefile                   # build / test / cross / docker targets
├── .dockerignore
├── grpc_e2e_test.go           # E2E tests (live panel)
├── CHANGELOG.md               # Release notes per version
├── .env.example               # Environment template
├── .gitignore
├── go.mod
└── go.sum

Domain Routing

Default Domains

Services automatically get a subdomain: {project}-{service}.{DEFAULT_DOMAIN}

For compose services: {project}-{service}-{composeService}.{DEFAULT_DOMAIN}

Custom Domains

Point a wildcard DNS (*.apps.example.com) to your Easypanel server IP, then attach domains via AddDomain.

HTTPS is auto-provisioned via Let's Encrypt through Easypanel's Traefik.

Compose Port Security

The gRPC server automatically strips ports: directives from compose content. Only expose: is allowed — Traefik handles external routing. This prevents containers from binding directly to host ports.


Service status

GetServiceStatus merges the panel's stored configuration with the live Docker view, so a single call answers both "how is it configured" and "is it up":

{
  "name": "hermes-a1",
  "type": "compose",
  "enabled": true,
  "image": "nousresearch/hermes-agent:v2026.8.3",
  "env": "API_KEY=<redacted>",       // values hidden unless include_env is set
  "domains": ["pods-hermes-a1-agent.cv911b.easypanel.host"],
  "status": "running",                // running | stopped | unknown
  "runningContainers": 1,
  "containers": [{
    "id": "21bb7f989f0f",
    "name": "pods_hermes-a1-agent-1",
    "image": "nousresearch/hermes-agent:v2026.8.3",
    "state": "running",
    "status": "Up 47 hours"
  }],
  "composeServices": ["agent"]       // compose only
}

The panel reports only running containers, so an empty list means stopped. If the panel refuses the container query the RPC still returns the configuration with status: "unknown" instead of failing. Compose services have no configured image, so image falls back to the running container's image.

Env values are redacted by default

Easypanel stores secrets in the same env blob its inspect routes return, and a gRPC token is panel-wide: any caller can read any project. GetServiceStatus therefore returns keys with <redacted> values. Set include_env: true to get clear text:

grpcurl -plaintext -H "authorization: Bearer $TOKEN" \
  -d '{"project":"demo","service":"api","serviceType":"app","includeEnv":true}' \
  localhost:50051 paas.PaaS/GetServiceStatus

Supported Easypanel versions

Version
Minimum supported 2.32.0
Newest tested 2.33.1

The panel's route surface is pinned in internal/easypanel/testdata/panel-surface-*.json, extracted from the official easypanel/easypanel images. Every route this project calls is listed in internal/easypanel/routes.go, and TestRequiredRoutesExistInEverySupportedPanel asserts each one exists in every pinned release. Both bounds of the window are backed by a pinned surface: 2.32.0, 2.32.2, 2.33.0 and 2.33.1 expose the same routes with identical input fields (374 procedures in 2.32.0, 375 from 2.32.2 on, the addition being unrelated to this project), which is why one code path covers the range.

The gRPC server probes the live panel version at startup (GET /api/openapi.json, the one version route present in all supported releases) and logs whether it falls inside that window. Set EASYPANEL_SKIP_VERSION_CHECK=1 to skip the probe.

# Which panel am I talking to?
deploy-everything settings panel-version

# Re-pin the route surface after a panel upgrade (needs docker + python3)
scripts/panel-surface.sh 2.32.0 2.32.2 2.33.0 2.33.1
go test ./internal/easypanel/ -run TestRequiredRoutes -v

After adding a newer release, bump MaxTestedVersion in internal/easypanel/version.go to match the newest pinned file. MinSupportedVersion must equal the oldest pinned file; the tests enforce both.


Logs

Easypanel serves service logs from its own Loki deployment and has no other log source: no docker logs route exists in the panel API. Loki and Promtail are only deployed when log aggregation is enabled under Settings -> Logs, which the panel gates behind a license with advanced monitoring.

When aggregation is off, the panel answers log queries with an opaque [BAD_REQUEST] fetch failed. GetLogs prechecks logs.getSettings and returns FailedPrecondition with the reason instead:

panel log aggregation is disabled, so Loki is not deployed and the panel has no
log source. Enable it in the Easypanel UI under Settings -> Logs (requires a
license with advanced monitoring), then retry GetLogs.

Check the current state with:

deploy-everything settings logs

GetLogs filters map straight onto the panel's Loki query: stream (stdout/stderr), levels, search, start, end, and limit (clamped to the panel's maximum of 1000). Supplying start switches the query to oldest-first.


Testing

# Unit tests (no panel needed): route surface, version parsing, log/status mapping
go test ./internal/... ./cmd/... -count=1

# Run E2E tests (requires live Easypanel instance)
go test -v -run "TestGRPC_" -timeout 180s

# Individual tests
go test -v -run TestGRPC_AuthReject -timeout 30s
go test -v -run TestGRPC_DeployContainerE2E -timeout 120s
go test -v -run TestGRPC_DeployComposeE2E -timeout 120s
go test -v -run TestGRPC_UpdateResources -timeout 60s
go test -v -run TestGRPC_RuntimeStatusAndLogs -timeout 120s

Tests create real services on the panel, verify HTTP reachability, and clean up after themselves.


Regenerating Proto

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       proto/paas.proto

Build & Docker

Local builds (Makefile)

make help          # list targets
make build         # both binaries -> dist/ (version metadata injected)
make build-cross   # linux/darwin/windows, amd64+arm64 -> dist/
make test          # go test ./... (integration tests skip without EASYPANEL_* env)
make vet fmt-check # static checks, same as CI
make docker        # local server image  -> ghcr.io/igun997/deploy-everything:dev
make docker-cli    # local CLI image     -> ghcr.io/igun997/deploy-everything-cli:dev
make docker-buildx PUSH=1   # multi-arch (linux/amd64+arm64) build and push

Version metadata is injected at link time into internal/version:

./dist/deploy-everything version
# v1.2.3 (commit a1b2c3d, built 2026-01-01T00:00:00Z, linux/amd64, go1.25.0)
./dist/grpc-server version

Images

One Dockerfile, two final stages, both distroless + static (CGO_ENABLED=0), running as nonroot:

Stage Image Entrypoint
server (default) ghcr.io/igun997/deploy-everything grpc-server (port 50051; CLI also included)
cli ghcr.io/igun997/deploy-everything-cli deploy-everything
# Run the gRPC server
docker run --rm -p 50051:50051 --env-file .env \
  ghcr.io/igun997/deploy-everything:0.2.0

# Run a CLI command
docker run --rm --env-file .env \
  ghcr.io/igun997/deploy-everything-cli:0.2.0 projects list

# Bleeding edge (current master)
docker run --rm --env-file .env \
  ghcr.io/igun997/deploy-everything-cli:dev version

CI/CD

Workflow Trigger Does
.github/workflows/ci.yml pull request → master gofmt check, go vet, build both binaries, go test, build both images (no push) + version smoke test
.github/workflows/release.yml push to master, tag v* verify → multi-arch image build/push to GHCR (+ SBOM, provenance attestation); on tags also cross-compiled archives attached to a GitHub Release

Image tagging policy

Git event Image tags
push/merge to master dev, dev-<short-sha>
tag v0.2.0 0.2.0, 0.2, 0, latest
tag v1.2.3 1.2.3, 1.2, 1, latest
pre-release tag v1.2.3-rc1 1.2.3-rc1 only (no latest, no 1.2/1)

So :dev always points at the current master, :latest always points at the newest stable release.

The leading v is stripped from image tags: git tag v0.2.0 produces image tag 0.2.0. The binaries still report the full v0.2.0 from version.

Cutting a release

git tag -a v1.2.3 -m "v1.2.3"
git push origin v1.2.3

That publishes ghcr.io/igun997/deploy-everything:1.2.3 (+ 1.2, 1, latest), the matching -cli image, and a GitHub Release with tar.gz/zip archives plus checksums.txt for linux/darwin (amd64+arm64) and windows/amd64.

No secrets needed — GHCR push uses the built-in GITHUB_TOKEN. Packages inherit repo visibility on first publish; make them public in the repo's package settings if desired.

License

MIT

About

Turn your Easypanel to PaaS

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages