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
- A VPS or dedicated server with EasyPanel installed (Ubuntu 22.04+, minimum 4 GB RAM / 2 vCPUs)
- EasyPanel API access — generate an API key from the EasyPanel settings
- Source code pushed to your GitHub — push the
contract-lucidityfolder to a private repo under your organization's GitHub account - GitHub token configured in EasyPanel (Settings → GitHub) so EasyPanel can pull from your repo
curlandjqinstalled on the machine running the deploy script- A domain pointed at the server IP (optional but recommended)
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.
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:
- Mirror to GitHub — push a copy of the source code to a private GitHub repo and use that for deployment
- 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:
| Prompt | Default | Description |
|---|---|---|
| EasyPanel URL | (required) | Your EasyPanel instance URL (e.g., https://easypanel.example.com) |
| EasyPanel API Key | (required) | API key from EasyPanel settings |
| Deployment type | client | client (single-tenant) or product (multi-tenant) |
| Project name | contract-lucidity | EasyPanel project name |
| GitHub owner | (required) | Your GitHub org or username where you pushed the source code |
| GitHub repo name | contract-lucidity | Repository name |
| GitHub branch | master | Branch to deploy |
| Postgres password | <your-strong-password> | Database password |
| Admin email | admin@contractlucidity.local | Initial admin account |
| Admin password | <your-strong-password> | Initial admin password |
| Pipeline concurrency | 2 | Number 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: admin@contractlucidity.local
Admin Pass: <your-strong-password>
Concurrency Sizing Guide
The script displays this table to help you choose the right concurrency:
| Workers | RAM | CPUs | Use Case |
|---|---|---|---|
| 2 | 4 GB | 2 vCPU | Demo / small team |
| 4 | 8 GB | 2-4 vCPU | Small firm (< 50 users) |
| 8 | 16 GB | 4-8 vCPU | Mid-size firm |
| 16 | 32 GB | 8+ vCPU | Am Law 200 / Enterprise |
| 32+ | 64 GB+ | 16+ vCPU | Am Law 100 / High volume |
What the Script Creates
The script provisions the following in EasyPanel:
- Project -- an EasyPanel project container
- cl-db -- PostgreSQL service using
pgvector/pgvector:pg16 - cl-redis -- Redis service
- cl-backend -- App service built from
backend/Dockerfile - cl-worker -- App service built from
backend/Dockerfile.worker - cl-frontend -- App service built from
frontend/Dockerfile - 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-dbwith imagepgvector/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.
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
| Setting | Value |
|---|---|
| Source | GitHub: your-org/contract-lucidity, path: /backend |
| Build type | Dockerfile |
| Dockerfile | Dockerfile |
| Volume mount | Named volume cl-storage at /data/storage |
cl-worker
| Setting | Value |
|---|---|
| Source | GitHub: your-org/contract-lucidity, path: /backend |
| Build type | Dockerfile |
| Dockerfile | Dockerfile.worker |
| Volume mount | Bind mount (see below) at /data/storage |
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
| Setting | Value |
|---|---|
| Source | GitHub: your-org/contract-lucidity, path: /frontend |
| Build type | Dockerfile |
| Dockerfile | Dockerfile |
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-backendgets a named volume calledcl-storagemounted at/data/storagecl-workergets 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
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=*
DEFAULT_ADMIN_EMAIL=admin@contractlucidity.local
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
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
| Issue | Detail |
|---|---|
| Build type immutable after creation | updateBuild / updateDeploy silently fail. You must set the correct Dockerfile and mounts at service creation time. Delete and recreate if wrong. |
| Worker Dockerfile | Worker uses Dockerfile.worker which bakes in the Celery command. Using the standard Dockerfile will start uvicorn instead. |
| Shared storage | Backend 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 hostnames | In EasyPanel, service hostnames follow the pattern {project-name}_{service-name} (underscore, not hyphen). |
| Redis password | EasyPanel auto-generates a Redis password. You must include it in the Redis URL: redis://:<password>@host:6379/0. |
| Timeout on deploy | The deploy API call may return HTTP 524 (timeout). This is normal -- the build continues in the background. The script handles this gracefully. |
| pgvector init | The 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:
- In the EasyPanel UI, navigate to each app service
- Click "Deploy" to pull the latest code from GitHub and rebuild
- Deploy in order:
cl-backendfirst, thencl-worker, thencl-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