Skip to main content

EasyPanel

EasyPanel is a modern server control panel that simplifies Docker-based deployments with a web UI. Contract Lucidity includes an automated deployment script that provisions all services on an EasyPanel server in minutes.

What EasyPanel Provides

  • Web-based UI for managing containers, domains, and SSL
  • Automatic Let's Encrypt SSL certificates
  • GitHub integration for building from source
  • Built-in PostgreSQL and Redis service templates
  • Log viewing and service restart from the browser
  • No Kubernetes knowledge required

Prerequisites

  1. A VPS or dedicated server with EasyPanel installed (Ubuntu 22.04+, minimum 4 GB RAM / 2 vCPUs)
  2. EasyPanel API access — generate an API key from the EasyPanel settings
  3. Source code pushed to your GitHub — push the contract-lucidity folder to a private repo under your organization's GitHub account
  4. GitHub token configured in EasyPanel (Settings → GitHub) so EasyPanel can pull from your repo
  5. curl and jq installed on the machine running the deploy script
  6. A domain pointed at the server IP (optional but recommended)
Before You Start

The deployment script pulls source code from GitHub during the build process. You must push the Contract Lucidity source code to your own GitHub repository before running the script. The script will ask for your GitHub owner/org and repo name.

GitHub Only

The automated deployment script only supports GitHub repositories. EasyPanel itself supports GitHub, GitLab, Bitbucket, and generic Git URLs — but the script uses the GitHub-specific API ("type": "github" with owner and repo fields).

If your organization uses Azure DevOps, GitLab, Bitbucket, or another provider, you have two options:

  1. Mirror to GitHub — push a copy of the source code to a private GitHub repo and use that for deployment
  2. Modify the script — change the service creation calls (lines ~250-300 in deploy-easypanel.sh) to use "type": "git" with a full repository URL instead of "type": "github". Refer to the EasyPanel documentation for supported source types.

Automated Deployment

The deploy-easypanel.sh script automates the entire deployment.

Step 1: Locate the Script

The deployment script is included with your Contract Lucidity source code:

contract-lucidity/
├── deploy-easypanel.sh ← This file
├── backend/
├── frontend/
├── docs-admin/
└── ...

You can also download it directly from this documentation.

Step 2: Run the Script

Copy the script to the machine where you'll run the deployment (any machine with curl and jq — does not need to be the server itself):

chmod +x deploy-easypanel.sh
./deploy-easypanel.sh

The script will prompt you for:

PromptDefaultDescription
EasyPanel URL(required)Your EasyPanel instance URL (e.g., https://easypanel.example.com)
EasyPanel API Key(required)API key from EasyPanel settings
Deployment typeclientclient (single-tenant) or product (multi-tenant)
Project namecontract-lucidityEasyPanel project name
GitHub owner(required)Your GitHub org or username where you pushed the source code
GitHub repo namecontract-lucidityRepository name
GitHub branchmasterBranch to deploy
Postgres password<your-strong-password>Database password
Admin email[email protected]Initial admin account
Admin password<your-strong-password>Initial admin password
Pipeline concurrency2Number of Celery worker processes
External storage path(empty)Mount path for external storage (e.g., /mnt/cl-storage)

Step 3: Wait for Build

The script triggers Docker builds for all three app services. This takes 3-5 minutes. It will poll the frontend URL and report when the application is healthy.

Step 4: Access the Application

Once complete, the script prints:

Application:  https://cl-frontend-contract-lucidity.<service-domain>
Admin Email: [email protected]
Admin Pass: <your-strong-password>

Concurrency Sizing Guide

The script displays this table to help you choose the right concurrency:

WorkersRAMCPUsUse Case
24 GB2 vCPUDemo / small team
48 GB2-4 vCPUSmall firm (< 50 users)
816 GB4-8 vCPUMid-size firm
1632 GB8+ vCPUAm Law 200 / Enterprise
32+64 GB+16+ vCPUAm Law 100 / High volume

What the Script Creates

The script provisions the following in EasyPanel:

  1. Project -- an EasyPanel project container
  2. cl-db -- PostgreSQL service using pgvector/pgvector:pg16
  3. cl-redis -- Redis service
  4. cl-backend -- App service built from backend/Dockerfile
  5. cl-worker -- App service built from backend/Dockerfile.worker
  6. cl-frontend -- App service built from frontend/Dockerfile
  7. Domain -- HTTPS domain with Let's Encrypt certificate pointing to cl-frontend on port 3000

Manual Deployment (Without the Script)

If you prefer to set up services manually through the EasyPanel UI:

1. Create a Project

In the EasyPanel UI, create a new project named contract-lucidity.

2. Create the Database Services

  • PostgreSQL: Create a Postgres service named cl-db with image pgvector/pgvector:pg16
  • Redis: Create a Redis service named cl-redis

3. Create the App Services

For each app service, configure GitHub as the source with repository your-org/contract-lucidity.

Build Type and Mounts MUST Be Set at Creation Time

EasyPanel's updateBuild and updateDeploy API calls silently fail when trying to change the build type or mounts after service creation. You must set the correct Dockerfile and volume mounts when you first create each service. If you get them wrong, delete the service and recreate it.

cl-backend

SettingValue
SourceGitHub: your-org/contract-lucidity, path: /backend
Build typeDockerfile
DockerfileDockerfile
Volume mountNamed volume cl-storage at /data/storage

cl-worker

SettingValue
SourceGitHub: your-org/contract-lucidity, path: /backend
Build typeDockerfile
DockerfileDockerfile.worker
Volume mountBind mount (see below) at /data/storage
Worker Uses a Separate Dockerfile

The worker uses Dockerfile.worker, not Dockerfile. The worker Dockerfile bakes in the Celery command and respects the CELERY_CONCURRENCY environment variable. Using the wrong Dockerfile will start a second API server instead of a worker.

cl-frontend

SettingValue
SourceGitHub: your-org/contract-lucidity, path: /frontend
Build typeDockerfile
DockerfileDockerfile

4. Configure Shared Storage

The backend and worker must share the same filesystem at /data/storage. In EasyPanel, this is achieved differently depending on whether you use external storage:

With external storage (recommended for production): Both cl-backend and cl-worker get bind mounts to the same external path:

Host path: /mnt/cl-storage  →  Container path: /data/storage

Without external storage (local disk):

  • cl-backend gets a named volume called cl-storage mounted at /data/storage
  • cl-worker gets a bind mount to the backend's volume directory on the host:
Host path: /etc/easypanel/projects/{project}/cl-backend/volumes/cl-storage
Container path: /data/storage
Why the Bind Mount?

EasyPanel does not support sharing named volumes between services. The workaround is to bind-mount the physical directory where EasyPanel stores the backend's named volume. This path follows the convention /etc/easypanel/projects/{project-name}/{service-name}/volumes/{volume-name}.

5. Configure Environment Variables

Set environment variables for each service through the EasyPanel UI (Service > Environment).

cl-backend:

APP_NAME=Contract Lucidity
APP_ENV=production
LOG_LEVEL=info
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<your-pg-password>
POSTGRES_DB=<database-name>
POSTGRES_HOST=<project-name>_cl-db
POSTGRES_PORT=5432
REDIS_HOST=<project-name>_cl-redis
REDIS_PORT=6379
REDIS_URL=redis://:<redis-password>@<project-name>_cl-redis:6379/0
CELERY_BROKER_URL=redis://:<redis-password>@<project-name>_cl-redis:6379/0
CELERY_RESULT_BACKEND=redis://:<redis-password>@<project-name>_cl-redis:6379/1
JWT_SECRET_KEY=<generated-secret>
JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=1440
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
STORAGE_PATH=/data/storage
CONFIG_PATH=/data/config
CORS_ORIGINS=*
[email protected]
DEFAULT_ADMIN_PASSWORD=<strong-password>
MAX_UPLOAD_SIZE_MB=100

cl-worker:

APP_NAME=Contract Lucidity
APP_ENV=production
LOG_LEVEL=info
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<your-pg-password>
POSTGRES_DB=<database-name>
POSTGRES_HOST=<project-name>_cl-db
POSTGRES_PORT=5432
REDIS_HOST=<project-name>_cl-redis
REDIS_PORT=6379
REDIS_URL=redis://:<redis-password>@<project-name>_cl-redis:6379/0
CELERY_BROKER_URL=redis://:<redis-password>@<project-name>_cl-redis:6379/0
CELERY_RESULT_BACKEND=redis://:<redis-password>@<project-name>_cl-redis:6379/1
STORAGE_PATH=/data/storage
CONFIG_PATH=/data/config
CELERY_CONCURRENCY=2

cl-frontend:

BACKEND_INTERNAL_URL=http://<project-name>_cl-backend:8000
NEXT_PUBLIC_DEPLOYMENT_MODE=client
Deployment Mode

Set NEXT_PUBLIC_DEPLOYMENT_MODE=client for client tenant deployments. This hides multi-tenant features in the UI. Omit this variable (or set to product) for the SaaS product deployment.

6. Configure Domain

In the EasyPanel UI, add a domain to cl-frontend pointing to port 3000. Enable HTTPS with Let's Encrypt.

7. Deploy

Click "Deploy" on each service in order: databases first, then backend, worker, and finally frontend.

Key Gotchas

IssueDetail
Build type immutable after creationupdateBuild / updateDeploy silently fail. You must set the correct Dockerfile and mounts at service creation time. Delete and recreate if wrong.
Worker DockerfileWorker uses Dockerfile.worker which bakes in the Celery command. Using the standard Dockerfile will start uvicorn instead.
Shared storageBackend gets a named volume; worker must bind-mount to the backend's volume directory at /etc/easypanel/projects/{project}/cl-backend/volumes/cl-storage.
Service hostnamesIn EasyPanel, service hostnames follow the pattern {project-name}_{service-name} (underscore, not hyphen).
Redis passwordEasyPanel auto-generates a Redis password. You must include it in the Redis URL: redis://:<password>@host:6379/0.
Timeout on deployThe deploy API call may return HTTP 524 (timeout). This is normal -- the build continues in the background. The script handles this gracefully.
pgvector initThe pgvector/pgvector:pg16 image has the extension pre-installed, but you still need to run CREATE EXTENSION IF NOT EXISTS vector; in the database. The backend's Alembic migrations handle this automatically on first boot.

Updating an Existing Deployment

To deploy a new version:

  1. In the EasyPanel UI, navigate to each app service
  2. Click "Deploy" to pull the latest code from GitHub and rebuild
  3. Deploy in order: cl-backend first, then cl-worker, then cl-frontend

Or use the API:

# Replace with your values
EP_URL="https://easypanel.example.com"
EP_API_KEY="your-api-key"
PROJECT="contract-lucidity"

for service in cl-backend cl-worker cl-frontend; do
curl -X POST "$EP_URL/api/trpc/services.app.deployService" \
-H "Authorization: Bearer $EP_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"json\": {\"projectName\": \"$PROJECT\", \"serviceName\": \"$service\"}}"
done