cipi api

Cipi can optionally enable a REST API layer on the server via cipi api <domain>. It is powered by the Laravel package cipi/api (current release 1.11.11), which exposes:

  • REST API — apps, aliases, deploy, SSL, databases, app logs, server status (/api/*)
  • MCP server — 29 tools at /mcp (Streamable HTTP)
  • Swagger UI — interactive reference at /docs

Requires PHP 8.2+ and Laravel 12+ on the panel host. This is server-level automation — distinct from the per-app Cipi Agent package (cipi/agent on each Laravel app).

The cipi/api package

On a normal Cipi server you never install the package manually — cipi api <domain> provisions Laravel at /opt/cipi/api, Nginx, SSL, SQLite job queue, and cipi-queue.service. For reference or custom setups:

bash
$ composer require cipi/api
$ php artisan vendor:publish --tag=cipi-config
$ php artisan vendor:publish --tag=cipi-assets
$ php artisan migrate
$ php artisan cipi:seed-api-user
$ php artisan cipi:token-create

Panel .env uses CIPI_APPS_JSON=/etc/cipi/apps.json (or apps-public.json projection for non-sensitive fields). Token abilities are defined in config/cipi.php — list them with php artisan cipi:token-abilities (same list as cipi api token create since Cipi 4.6.3).

Source and changelog: github.com/cipi-sh/api (MIT). Client wrapper: cipi-cli.

Commands

bash
$ cipi api <domain>           # configure API at root (e.g. api.myhosting.com)
$ cipi api ssl                  # install Let's Encrypt certificate for API domain
$ cipi api token list           # list tokens
$ cipi api token create         # create a new token (choose abilities)
$ cipi api token revoke <id>    # revoke a token
$ cipi api status               # Laravel + cipi-api versions, queue worker, pending jobs, FPM pool
$ cipi api fix-permissions      # repair panel storage/database ownership (www-data)
$ cipi api update               # soft update: composer update on Laravel and API packages
$ cipi api upgrade              # full rebuild with rollback at /opt/cipi/api.old

Panel API troubleshooting

After cipi self-update, root-owned files under /opt/cipi/api can prevent PHP-FPM (www-data) from writing logs or the SQLite job database — the browser shows a bare HTTP 500 on /docs or /mcp. Cipi normally repairs ownership automatically during self-update; if problems persist:

bash
$ cipi api fix-permissions   # chown storage, database, bootstrap/cache, .env → www-data
$ cipi api status              # confirm Laravel version, queue worker, pending jobs

cipi api status prints the installed Laravel and cipi/api package versions, whether cipi-queue.service is active, pending async jobs in the panel SQLite database, and PHP-FPM pool stats for the API vhost (including slow requests when configured).

Token creation and granular permissions

Authentication uses Sanctum. Each token can have one or more abilities that limit allowed operations:

  • apps-view — read apps
  • apps-create — create apps
  • apps-edit — edit apps (PHP, repository, branch, primary domain since API 1.9.0+ / Cipi 4.6.2+)
  • apps-suspend — suspend and unsuspend apps
  • apps-basicauth — enable, disable, and inspect HTTP Basic Auth on apps (API 1.10.0+)
  • apps-delete — delete apps
  • deploy-manage — deploy, rollback, unlock
  • ssl-manage — install and manage SSL certificates
  • aliases-view — read aliases
  • aliases-create — add aliases
  • aliases-delete — remove aliases
  • dbs-view — list databases
  • dbs-create — create databases
  • dbs-delete — delete databases
  • dbs-manage — backup, restore, regenerate password
  • status-view — read server status snapshot (GET /api/status, API 1.11.6+)
  • mcp-access — access the MCP server

Since Cipi 4.6.3 / API 1.11.7+, cipi api token create reads the canonical ability list from the panel API package (same entries as php artisan cipi:token-abilities on the server). Migration 4.6.3 retrofits existing servers with the updated list (including status-view, apps-suspend, and apps-basicauth).

REST endpoints

All endpoints require the Authorization: Bearer <token> header. Write operations (create, edit, delete, deploy, rollback, unlock, SSL, alias, database) are asynchronous: they return 202 Accepted with a job_id to poll via GET /api/jobs/{id}. Read-only endpoints such as GET /api/dbs, GET /api/status, and GET /api/apps/{name}/logs (API 1.11.9+) are synchronous. POST /api/apps accepts optional custom (boolean) and docroot (string) parameters for creating custom apps with classic deployment. The repository field is required for Laravel apps and optional for custom apps: omit it (or send empty) to provision an SFTP-only site aligned with Cipi v4.5.1+. When no repository is set, branch is omitted. The MCP tool AppCreate follows the same rules. Update the API package to 1.6.1+ on the server (cipi api update / cipi api upgrade) so validation and OpenAPI match this behavior.

POST /api/apps/{name}/suspend takes an app offline by swapping its Nginx vhost for a generic HTTP 503 maintenance page (HTTPS included) without deleting it, while POST /api/apps/{name}/unsuspend restores the normal vhost. Both require the apps-suspend ability and return 409 if the app is already in the target state. The suspended flag survives vhost regeneration and is exposed on GET /api/apps and GET /api/apps/{name}. These endpoints require the API package 1.8.1+ and Cipi 4.5.8+ on the server.

PUT /api/apps/{name} accepts an optional domain field to rename the app’s primary domain. The API validates the format synchronously and returns 409 if the domain is already used by another app (aliases of the current app are allowed, so promoting an alias to primary works). Requires the API package 1.9.0+ and Cipi 4.6.2+. The MCP tool AppEdit accepts the same domain parameter.

GET /api/apps and GET /api/apps/{name} expose boolean suspended and basic_auth flags per app (from apps.json).

HTTP Basic Auth endpoints under /api/apps/{name}/basicauth/* wrap cipi basicauth synchronously — they do not return a job_id. Enable accepts optional user and password (auto-generated when omitted; returned once in the response). Requires the apps-basicauth ability and API package 1.10.0+. This is distinct from Composer auth.json management — see cipi basicauth.

GET /api/dbs lists databases synchronously by running sudo cipi db list on the host (same as the server CLI). Other /api/dbs/* write operations are asynchronous jobs. Database commands require Cipi 4.4.17+ on the server (cipi db … entries in the API sudoers whitelist).

GET /api/status returns the same structured JSON as cipi status (system, resources, services, PHP pools, app count). Since API 1.11.8+ the endpoint prefers sudo cipi status on the host and falls back to direct host reads when sudo is unavailable. Requires the status-view ability (API 1.11.6+). The MCP tool ServerStatus returns the same payload and requires only mcp-access.

GET /api/apps/{name}/logs returns synchronous, paginated log snapshots for nginx, PHP-FPM, Laravel (when present), worker, and deploy logs — the REST counterpart to cipi app logs and cipi-cli apps logs. Query params: type (default all), page (default 1, most recent first), per_page (default 50, max 1000). Requires the apps-view ability and API package 1.11.9+. Log text is redacted for common secrets (same policy as MCP AppLogs since API 1.11.5+).

Method Endpoint Required ability
GET /api/apps apps-view
GET /api/apps/{name} apps-view
GET /api/apps/{name}/logs apps-view
POST /api/apps apps-create
PUT /api/apps/{name} apps-edit
POST /api/apps/{name}/suspend apps-suspend
POST /api/apps/{name}/unsuspend apps-suspend
DELETE /api/apps/{name} apps-delete
GET /api/apps/{name}/aliases aliases-view
POST /api/apps/{name}/aliases aliases-create
DELETE /api/apps/{name}/aliases aliases-delete
POST /api/apps/{name}/deploy deploy-manage
POST /api/apps/{name}/deploy/rollback deploy-manage
POST /api/apps/{name}/deploy/unlock deploy-manage
POST /api/apps/{name}/ssl ssl-manage
GET /api/apps/{name}/basicauth apps-basicauth
POST /api/apps/{name}/basicauth/enable apps-basicauth
POST /api/apps/{name}/basicauth/disable apps-basicauth
GET /api/dbs dbs-view
POST /api/dbs dbs-create
DELETE /api/dbs/{name} dbs-delete
POST /api/dbs/{name}/backup dbs-manage
POST /api/dbs/{name}/restore dbs-manage
POST /api/dbs/{name}/password dbs-manage
GET /api/status status-view
GET /api/jobs/{id} any authenticated token

REST examples (curl)

Set your API base URL and token (from cipi api token create):

bash
export CIPI_API_URL="https://api.myserver.com"
export CIPI_API_TOKEN="your-sanctum-token"

List apps (sync, 200):

bash
curl -sS "${CIPI_API_URL}/api/apps" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Accept: application/json"

Server status (sync, requires status-view):

bash
curl -sS "${CIPI_API_URL}/api/status" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

App logs (sync, requires apps-view, API 1.11.9+):

bash
curl -sS "${CIPI_API_URL}/api/apps/myapp/logs?type=deploy&page=1&per_page=50" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Accept: application/json"

Deploy an app (async, 202 + job_id):

bash
curl -sS -X POST "${CIPI_API_URL}/api/apps/myapp/deploy" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Accept: application/json"

# poll until completed
curl -sS "${CIPI_API_URL}/api/jobs/JOB_ID" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

Database backup (async, dbs-manage) — returns a real backup path in the job result, not an anonymized dump (see Agent anonymizer):

bash
curl -sS -X POST "${CIPI_API_URL}/api/dbs/myapp_db/backup" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

Host integration (sudoers)

The panel API runs as www-data and executes Cipi CLI commands via sudo using /etc/sudoers.d/cipi-api — an explicit whitelist of cipi subcommands. Vault and MariaDB credentials stay inside Cipi, not in PHP.

  • GET /api/dbs — runs sudo cipi db list (sync). Requires Cipi 4.4.17+ (migration adds cipi db … to sudoers). Without it: sudo: a terminal is required.
  • GET /api/status / MCP ServerStatus — prefer sudo cipi status (API 1.11.8+); host-read fallback when sudo fails.
  • MCP ServiceListsudo cipi service list
  • MCP AppArtisansudo cipi app artisan <app> …
  • Async jobs — cipi app, deploy, alias, ssl, db create|delete|backup|restore|password, etc.

After cipi self-update, run cipi api fix-permissions if /docs or /mcp return HTTP 500 (see troubleshooting above).

Swagger / OpenAPI

Interactive documentation is available at /docs (Swagger UI). The OpenAPI spec is generated from public/api-docs/openapi.json and covers apps (including suspend, unsuspend, domain rename, basic auth, and paginated logs), aliases, deploy, SSL, databases, server status, job polling with structured result types, and MCP tool schemas. Current API package version: 1.11.11.

MCP server

An MCP (Model Context Protocol) server is exposed at /mcp via Streamable HTTP. Since API package 1.11.1+, a token with the mcp-access ability is sufficient for all MCP tools — per-endpoint REST abilities (apps-view, deploy-manage, apps-basicauth, etc.) are not checked on /mcp. The server exposes 29 tools for app, alias, database, deploy, SSL, HTTP Basic Auth, job polling, logs, Artisan, and server monitoring. Write operations that dispatch async jobs return a job_id — poll with JobShow (API 1.11.0+). Basic auth actions and read-only tools run synchronously.

  • Applications: AppList, AppShow, AppCreate, AppEdit, AppSuspend, AppUnsuspend, AppDelete, AppDeploy, AppDeployRollback, AppDeployUnlock, AppArtisan (Laravel apps only; rejects custom apps and tinker)
  • HTTP Basic Auth: AppBasicAuthStatus, AppBasicAuthEnable, AppBasicAuthDisable
  • Aliases: AliasList, AliasAdd, AliasRemove
  • Databases: DbList, DbCreate, DbDelete, DbBackup, DbRestore, DbPassword
  • SSL: SslInstall
  • Jobs & logs: JobShow (poll async job status, parsed result, and CLI output), AppLogs (recent app logs by type: all, nginx, php, worker, deploy, laravel — same as cipi app logs; REST equivalent: GET /api/apps/{name}/logs since API 1.11.9+), ApiLogShow (recent Laravel logs for the panel API host)
  • Server monitoring: ServerStatus (structured JSON matching GET /api/status / cipi status), ServiceList (system service status via cipi service list)
Since API 1.11.5+, MCP log tools (AppLogs, ApiLogShow) prefix every response with a production-content warning and redact common secrets before delivery. Sensitive CLI output from JobShow and AppArtisan is redacted too; structured job result objects (e.g. app credentials from create jobs) are left intact so operators can still read them once.

Since Cipi 4.6.3, the panel API package is soft-updated nightly at 04:30 via /etc/cron.d/cipi-api (cipi api update), so MCP and REST endpoints stay current without manual intervention.

Installing the MCP server

The MCP endpoint is optional and loads only when the required MCP package is installed. To use it from VS Code, Cursor, or Claude Desktop:

  1. Configure the API with cipi api <domain> and cipi api ssl
  2. Create a token with cipi api token create and select at least mcp-access
  3. Add the MCP server to your client config (see below)

Cursor

Add to ~/.cursor/mcp.json (or Cursor → Settings → MCP):

json
{
  "mcpServers": {
    "cipi-api": {
      "type": "http",
      "url": "https://<your-api-domain>/mcp",
      "headers": {
        "Authorization": "Bearer <your-token>"
      }
    }
  }
}

Cursor connects natively over HTTP — no bridge needed.

VS Code

VS Code (with GitHub Copilot) supports MCP natively since 1.102. Add to .vscode/mcp.json or run MCP: Open User Configuration for a global setup. Use inputs to prompt for the token once and store it securely:

json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "cipi-token",
      "description": "Cipi API Token",
      "password": true
    }
  ],
  "servers": {
    "cipi-api": {
      "type": "http",
      "url": "https://<your-api-domain>/mcp",
      "headers": {
        "Authorization": "Bearer ${input:cipi-token}"
      }
    }
  }
}

Restart VS Code after saving. Use MCP: Add Server from the Command Palette for a guided setup.

Claude Code

Add the MCP server directly from the CLI:

bash
$ claude mcp add --transport http cipi-api https://<your-api-domain>/mcp \
    --header "Authorization: Bearer <your-token>"

Claude Desktop

Claude Desktop requires the mcp-remote bridge to convert stdio to HTTP. Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the equivalent config path on your OS:

json
{
  "mcpServers": {
    "cipi-api": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://<your-api-domain>/mcp",
        "--header",
        "Authorization: Bearer <your-token>"
      ]
    }
  }
}

Install mcp-remote once with npm install -g mcp-remote.

Replace <your-api-domain> with your API domain (e.g. api.myhosting.com) and <your-token> with the token created in step 2.

WHMCS Module

An official WHMCS provisioning module is available at github.com/cipi-sh/whmcs. It bridges the WHMCS provisioning lifecycle to the Cipi REST API — automating app creation, deletion, SSL certificates, deployments, and configuration changes for your hosting customers. No Composer dependencies; the module is a self-contained drop-in.

Requirements

  • WHMCS 8.x (provisioning module type “Server”)
  • Cipi server with API enabled: cipi api <domain> and cipi api ssl
  • Sanctum Bearer token with the required abilities:
Ability Required for
apps-view Test Connection, App Info
apps-create Create Account
apps-edit Change Package
apps-suspend Suspend / Unsuspend
apps-delete Terminate Account
deploy-manage Deploy, Rollback, Unlock
ssl-manage Install SSL, Auto-SSL

Installation

  1. Copy modules/servers/cipi/ into your WHMCS root:
    your-whmcs/
    └── modules/
        └── servers/
            └── cipi/
                ├── cipi.php
                └── lib/
                    └── CipiApiClient.php
  2. In WHMCS Admin → System Settings → Servers → Add New Server:
    • Type: Cipi (Laravel hosting)
    • Hostname: API base URL (e.g. https://api.example.com, no trailing slash)
    • Password: Bearer token from cipi api token create
    • Secure: Yes (recommended — enables TLS verification)
  3. Create a hosting product linked to this server and configure Module Settings:
Setting Description Default
PHP Version 8.2 / 8.3 / 8.4 / 8.5 8.5
App Type laravel or custom laravel
Git Repository (SSH) Required for Laravel; optional for custom
Git Branch Branch to deploy main
Auto SSL Install Let’s Encrypt after creation No

App types

App type Cipi equivalent Stack Git / Deploy
laravel (default) cipi app create Isolated Linux user, PHP-FPM pool, Nginx vhost, MariaDB, Supervisor workers, Deployer releases SSH repository URL required; branch from settings
custom cipi app create --custom htdocs/ directory, Nginx + PHP — ideal for static sites, SPAs, WordPress, or generic PHP apps Optional: leave Git Repository empty for SFTP-only hosting, or set a repo for Git-based deploy

Provisioning lifecycle

WHMCS action API call Behaviour
Test Connection GET /api/apps Validates token and API reachability
Create Account POST /api/apps Provisions a Cipi app (Laravel or custom); waits for async jobs; optionally installs SSL
Suspend POST /api/apps/{name}/suspend Takes the app offline (HTTP 503 maintenance page) without deleting it; waits for async jobs
Unsuspend POST /api/apps/{name}/unsuspend Restores the app’s normal Nginx vhost; waits for async jobs
Terminate Account DELETE /api/apps/{name} Removes the app; waits for async jobs
Change Package PUT /api/apps/{name} Updates PHP version, Git repository, or branch

Suspend / Unsuspend require Cipi 4.5.8+ (suspend/unsuspend endpoints), the API package 1.8.1+, and a token with the apps-suspend ability. Suspending swaps the app’s vhost for a generic HTTP 503 maintenance page; unsuspending restores it.

Admin buttons

From the WHMCS admin service view, operators can trigger one-click actions:

Button API call Description
Install SSL POST /api/apps/{name}/ssl Install a Let’s Encrypt certificate
Deploy POST /api/apps/{name}/deploy Trigger a zero-downtime deployment
Rollback Deploy POST /api/apps/{name}/deploy/rollback Revert to the previous release
Unlock Deploy POST /api/apps/{name}/deploy/unlock Unlock a stuck deployment
App Info GET /api/apps/{name} Fetch current app details into the Module Log

Auto-SSL on creation

Enable Auto SSL in the product module settings to automatically install a Let’s Encrypt certificate right after provisioning. If SSL installation fails, the app is still created successfully and a warning is logged.

Full API client

The bundled CipiApiClient covers the entire Cipi REST API surface. Even if a feature is not wired to a WHMCS hook, you can use the client in custom hooks or addons:

Area Methods
Apps listApps, getApp, createApp, editApp, suspendApp, unsuspendApp, deleteApp
Deploy deployApp, rollbackDeploy, unlockDeploy
SSL installSsl
Aliases listAliases, addAlias, removeAlias
Databases listDatabases, createDatabase, deleteDatabase, backupDatabase, restoreDatabase, resetDatabasePassword
Jobs getJob, waitForJob

Extending the module

php
// Example: add an alias from a WHMCS hook
require_once ROOTDIR . '/modules/servers/cipi/lib/CipiApiClient.php';

$client = new CipiApiClient('https://api.example.com', $token);
$client->addAlias('myapp', 'alias.example.com');

// Example: create an extra database
$client->createDatabase('myapp_extra');

// Example: backup a database
$client->backupDatabase('myapp');

Customer-facing behaviour

The module does not add a Client Area tab, custom buttons, or live status from Cipi. Customers see the standard WHMCS service view (domain, status, renewal dates). When Cipi provisions an app it generates one-time secrets (SSH password, DB password, deploy key, webhook URL). The REST API does not automatically push those secrets into WHMCS — you should extend the module, write a hook, or deliver credentials through your support workflow.

Module logging

All API calls are logged via logModuleCall() — Create, Terminate, Change Package, SSL, Deploy, Rollback, Unlock, and App Info. Enable Utilities → Logs → Module Log in WHMCS Admin for full visibility.

Full source code, project structure, and contribution guidelines are available on GitHub. The module is open-source under the MIT License.

cipi sync

Transfer, replicate, and back up entire Laravel applications between Cipi servers — including configuration, database dumps, storage files, SSH keys, workers, and crontabs. Every archive is encrypted with AES-256-CBC and protected by a user-defined passphrase, so credentials and sensitive data are safe at rest and during transfer.

Commands overview

bash
$ cipi sync export  [app ...] [--with-db] [--with-storage] [--output=<path>] [--passphrase=<secret>]
$ cipi sync import  <archive.tar.gz.enc> [app ...] [--update] [--deploy] [--yes] [--passphrase=<secret>]
$ cipi sync push    [app ...] [--host=IP] [--port=22] [--with-db] [--with-storage] [--import] [--passphrase=<secret>]
$ cipi sync list    <archive.tar.gz.enc> [--passphrase=<secret>]
$ cipi sync pubkey  # display the server's sync public key for inter-server trust
$ cipi sync trust   # add a remote server's public key to cipi's authorized_keys

Archive encryption

All sync archives are encrypted by default with AES-256-CBC. During export you are prompted for a passphrase (minimum 8 characters) that protects the archive. The same passphrase is required to import or inspect it. This protects SSH keys, .env files, database dumps, and credentials at rest and during transfer.

bash
# Interactive mode (default) — prompted for passphrase
$ cipi sync export --with-db
#   Enter passphrase to encrypt the archive: ********
#   Confirm passphrase: ********

# Non-interactive mode — for cron jobs and scripts
$ cipi sync export --with-db --passphrase="MyStr0ngP@ss"
For automated setups, store the passphrase in a secure file and reference it in your scripts: echo "MyStr0ngP@ss" > /etc/cipi/.sync_passphrase && chmod 400 /etc/cipi/.sync_passphrase. Then use --passphrase="$(cat /etc/cipi/.sync_passphrase)" in cron jobs.

Export

Packs app configs into an encrypted .tar.gz.enc archive. Optionally includes database dumps and storage files.

bash
# Export all apps (config only)
$ cipi sync export

# Export three specific apps with database + storage
$ cipi sync export shop blog api --with-db --with-storage

# Export to a custom path (non-interactive)
$ cipi sync export --with-db --output=/root/backups/cipi-march.tar.gz --passphrase="MyStr0ngP@ss"

What goes into the archive

File Description Included
env The app's .env from /home/<app>/shared/.env Always
auth.json Composer auth credentials (if exists) Always
deploy.php Deployer config Always
ssh/* Deploy key, known_hosts, authorized_keys, SSH config Always
supervisor.conf Queue workers config Always
crontab App's crontab (scheduler + deploy trigger) Always
db.sql.gz Gzipped MariaDB dump (schema + data + routines) --with-db
storage.tar.gz Archive of /home/<app>/shared/storage/ --with-storage

Plus global configs: apps.json (filtered to selected apps), databases.json, backup.json, api.json.

Import

Restores apps from an archive onto the current server.

bash
# Import all apps from archive
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc

# Import only two apps from an archive that contains ten
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc shop blog --passphrase="MyStr0ngP@ss"

# Import and deploy code from Git immediately
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc --deploy

# Non-interactive (skip all prompts)
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc --yes --passphrase="MyStr0ngP@ss"

What import does for a NEW app

When an app does not exist on the target server, import creates it from scratch — equivalent to cipi app create with all configs pre-filled from the archive:

  1. Linux user — Creates a new user with a random password
  2. Directories — Creates /home/<app>/shared/, logs/, .ssh/, .deployer/
  3. SSH deploy key — Restores from archive (same key works with GitHub/GitLab without reconfiguration)
  4. MariaDB database — Creates database + user with a new random password
  5. Database data — Imports the dump if --with-db was used during export
  6. .env — Copies from archive, then overwrites DB_PASSWORD, DB_USERNAME, DB_DATABASE, DB_HOST with the new server's values. Everything else (APP_KEY, MAIL_*, REDIS_*, custom vars) stays as-is
  7. PHP-FPM pool, Nginx vhost, Supervisor workers, Crontab, Deployer — Fully configured from archive data
At the end of import, Cipi prints the new SSH and DB passwords. Save them — they are shown only once.

Safety checks before import

The import runs pre-flight checks before touching anything:

  • App already exists — blocked unless --update is passed
  • Domain conflict — blocked if another app already uses the same domain
  • Missing PHP version — warning (the app is skipped; install the version first with cipi php install)

Update mode (--update)

The key feature for repeated sync (e.g. failover replication). Without --update, import refuses to touch apps that already exist. With --update, it updates existing apps and creates new ones.

bash
$ cipi sync import /tmp/archive.tar.gz.enc --update --passphrase="MyStr0ngP@ss"

What update does for an existing app

  • .env sync — The archive .env replaces the local one, but DB_PASSWORD, DB_USERNAME, DB_DATABASE, and DB_HOST are preserved from the local server. Everything else (APP_KEY, MAIL_*, REDIS_*, custom vars) comes from the source.
  • Database data — If the archive has a dump, drops all tables (with SET FOREIGN_KEY_CHECKS=0) and reimports. Uses local root credentials.
  • Storage — If the archive has storage, extracts over the existing directory (new files added, existing overwritten).
  • PHP version migration — If the source uses a different PHP version, the update migrates FPM pool, supervisor, crontab, deployer, and .env automatically.
  • Nginx vhost, Supervisor workers, Deployer config — Regenerated from archive data.
  • Deploy — If --deploy is passed, runs dep deploy to pull latest code.

What update does NOT change

  • Linux user password
  • SSH deploy keys (kept from first import)
  • MariaDB user credentials (target keeps its own)
  • SSL certificates (run cipi ssl install separately)

List (inspect archive)

View what is inside an archive without importing anything.

bash
$ cipi sync list /tmp/cipi-sync-aws01-20260306.tar.gz.enc --passphrase="MyStr0ngP@ss"

Cipi Sync Archive
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Cipi           v4.7.12
  Exported       2026-03-06T15:00:00Z
  Source         aws01 (3.120.xx.xx)
  Database       true
  Storage        true
 
  Apps
  APP            DOMAIN                       PHP    DB       STORAGE
  shop           shop.example.com             8.4    yes      yes
  blog           blog.example.com             8.4    yes      yes
  api            api.example.com              8.5    yes      yes
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Push (export + transfer + import)

Combines export, rsync transfer, and remote import in one command. Runs entirely from the source server.

bash
# Interactive push — prompted for target IP and passphrase
$ cipi sync push --with-db --with-storage --import

# Non-interactive push (for cron and scripts)
$ cipi sync push --host=51.195.xx.xx --port=22 --with-db --with-storage --import --passphrase="MyStr0ngP@ss"

# Push specific apps only
$ cipi sync push shop blog --host=51.195.xx.xx --with-db --import --passphrase="MyStr0ngP@ss"

# Push without auto-import (transfer only — import manually on remote)
$ cipi sync push --host=51.195.xx.xx --with-db --passphrase="MyStr0ngP@ss"

How push works

  1. Step 1: Runs cipi sync export locally (encrypts with passphrase)
  2. Step 2: Transfers the encrypted archive to the target via rsync
  3. Step 3: If --import is passed, runs cipi sync import --update --yes on the target via SSH

Push always adds --update and --yes when calling import on the remote. This means: first run creates all apps, subsequent runs incrementally update them. This is what makes push safe to run repeatedly via cron.

SSH setup for push

The source server needs SSH access to the target as the cipi user. Use the built-in trust mechanism for passwordless, key-based authentication between Cipi servers:

bash
# On the SOURCE server — display its sync public key
$ cipi sync pubkey

# On the TARGET server — add the source's public key to cipi's authorized_keys
$ cipi sync trust

Once trusted, cipi sync push connects as the cipi user automatically — no root access required.

Practical scenarios

Scenario 1: Migrate all apps from AWS to OVH

You have 20 apps on AWS. You bought an OVH VPS and installed Cipi on it.

bash
# On AWS (source server)
$ cipi sync push --host=51.195.xx.xx --with-db --with-storage --import

On the OVH target: 20 Linux users, 20 databases, 20 nginx vhosts, PHP-FPM pools, supervisor configs, crontabs — all created automatically. DB data imported, storage extracted, .env files copied with OVH's DB passwords, SSH deploy keys preserved (same keys work with GitHub). After import, install SSL and update DNS:

bash
# On OVH (target server)
$ cipi ssl install shop
$ cipi ssl install blog
# ... then update DNS A records to OVH IP

Scenario 2: Scheduled failover replication (cron)

Every 6 hours, Server 1 syncs all apps to Server 2. If Server 1 dies, change DNS and go live on Server 2.

bash
# One-time setup on Server 1 — trust Server 2 using cipi sync trust
$ cipi sync pubkey  # copy this key, then run "cipi sync trust" on Server 2
$ echo "YourStr0ngPassphrase!" > /etc/cipi/.sync_passphrase
$ chmod 400 /etc/cipi/.sync_passphrase

# First push (manual, to verify)
$ cipi sync push --host=server2-ip --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)"

# Add to crontab for automatic replication
$ crontab -e
cron
0 */6 * * * /usr/local/bin/cipi sync push --host=51.195.xx.xx --port=22 --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-replica.log 2>&1

Data loss window equals the cron interval (6 hours in this example). When Server 1 goes down: change DNS to Server 2, run cipi ssl install for each app, and you are live.

Scenario 3: Replicate to multiple servers

cron
# Stagger by 30 minutes so exports don't run simultaneously
0 */6 * * * /usr/local/bin/cipi sync push --host=51.195.xx.xx --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-ovh.log 2>&1
30 */6 * * * /usr/local/bin/cipi sync push --host=164.90.xx.xx --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-do.log 2>&1

Scenario 4: Daily encrypted backup (no transfer)

cron
0 3 * * * /usr/local/bin/cipi sync export --with-db --with-storage --output=/root/backups/cipi-$(date +\%Y\%m\%d).tar.gz --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/export.log 2>&1

Creates an encrypted portable archive every night. Restore on any Cipi server at any time with cipi sync import.

Limitations

  • SSL certificates are not included in the archive. Run cipi ssl install after importing on a new server.
  • DB sync is full replace, not incremental. Each update drops all tables and reimports.
  • Storage sync is full extract, not incremental rsync. Deleted files on the source remain on the target.
  • Deploy keys are the same on source and target — no GitHub/GitLab reconfiguration needed.

Vault & Encryption

Cipi encrypts all configuration files at rest using AES-256-CBC. The Vault system provides transparent encryption and decryption so that sensitive data — database passwords, API tokens, SSH keys, .env contents — is never stored in plaintext on disk.

Architecture

The system is built on two layers:

  • Vault — transparent encryption of JSON configuration files on disk (server.json, apps.json, databases.json, backup.json, smtp.json, api.json)
  • Sync encryption — passphrase-based encryption of export archives for secure transfer between servers

How Vault works

A master key is generated during installation with openssl rand -base64 32 and stored at /etc/cipi/.vault_key (chmod 400, root-only). Every JSON config file is encrypted on disk with openssl enc -aes-256-cbc -salt -pbkdf2. Files keep the .json extension — the content is simply an encrypted blob instead of readable JSON.

The vault_read function auto-detects whether a file is plaintext or encrypted (backward compatibility), so existing servers migrate seamlessly during update.

Vault functions

bash
# Core functions in lib/vault.sh
vault_init          # Generate .vault_key if not present
vault_read <file>   # Decrypt and output JSON to stdout (auto-detect plain/encrypted)
vault_write <file>  # Read JSON from stdin, encrypt and write to disk
vault_seal <file>   # Encrypt an existing plaintext file in-place
vault_get <file> <jq_query>  # Shortcut: vault_read | jq

Public projection

Cipi generates an apps-public.json file containing only non-sensitive fields (domain, aliases, PHP version, branch, repository, user, creation timestamp, plus suspended and basic_auth flags). The cipi-api group reads this plaintext projection instead of the encrypted file, keeping the vault key restricted to root.

Sync archive encryption

When you run cipi sync export, configs are decrypted from the vault into a staging area, then the entire archive is encrypted with your passphrase. On import, the archive is decrypted with the passphrase, and configs are re-encrypted with the destination server's vault key.

If the vault key is lost, configuration files become irrecoverable. The key is protected by chmod 400 and included in server backups. Consider exporting it manually for additional safety.

Email Notifications

Cipi can send email alerts when backup errors, deploy failures, system cron job failures, or security-relevant authentication events occur. SMTP configuration is stored encrypted in /etc/cipi/smtp.json and included in sync exports.

Commands

bash
$ cipi smtp configure      # interactive setup (Gmail, SendGrid, Mailgun, custom)
$ cipi smtp status          # display current notification settings
$ cipi smtp test            # send a verification email
$ cipi smtp enable          # enable notifications
$ cipi smtp disable         # disable without losing settings
$ cipi smtp delete          # remove SMTP configuration entirely

Granular notification triggers

Since v4.6.3, you can control which events send email when SMTP is configured. All triggers are on by default; events are always logged to /var/log/cipi/events.log regardless.

bash
$ cipi notifications list              # all triggers grouped by category
$ cipi notifications enable <trigger>   # turn one trigger on
$ cipi notifications disable <trigger>  # turn one trigger off
$ cipi notifications enable-all          # re-enable everything
$ cipi notifications disable-all         # mute all email alerts
$ cipi notifications reset               # restore defaults (all on)

Config: /etc/cipi/notifications.json. Run cipi notifications list on the server for the live on/off state. Trigger IDs for cipi notifications enable|disable <trigger>:

Trigger ID Category Event
app_createAppsApp created
app_editAppsApp modified
app_deleteAppsApp deleted
app_suspendAppsApp suspended
app_unsuspendAppsApp unsuspended
app_ssh_password_resetAppsApp SSH password reset
app_db_password_resetAppsApp DB password reset
alias_addDomainsAlias added
alias_removeDomainsAlias removed
auth_createAuthComposer auth.json created
auth_editAuthComposer auth.json edited
auth_deleteAuthComposer auth.json deleted
basicauth_enableBasic authHTTP basic auth enabled
basicauth_disableBasic authHTTP basic auth disabled
deploy_successDeployDeploy succeeded
deploy_failDeployDeploy failed
deploy_rollbackDeployDeploy rollback
ssl_installSSLSSL certificate installed
ssl_renewSSLSSL certificates renewed
php_installPHPPHP version installed
php_switchPHPSystem PHP switched
php_removePHPPHP version removed
db_createDatabaseDatabase created
db_deleteDatabaseDatabase deleted
worker_addWorkersWorker added
worker_removeWorkersWorker removed
ssh_key_addSSH keysSSH key added
ssh_key_renameSSH keysSSH key renamed
ssh_key_removeSSH keysSSH key removed
ssh_loginSecuritySSH login (cipi/root/sudo users)
sudoSecuritySudo elevation
suSecuritysu to root by cipi
backup_failBackupBackup failed
cron_failCronCron job failed
reset_root_passwordResetRoot SSH password reset
reset_db_passwordResetMariaDB root password reset
reset_valkey_passwordResetValkey password reset
api_configureAPIPanel API configured
api_updateAPIPanel API updated
api_upgradeAPIPanel API upgraded
api_sslAPIPanel API SSL installed
git_configureGitGit provider token configured
sync_exportSyncApps exported
sync_importSyncApps imported
sync_pushSyncApps pushed to remote
service_restartServicesService restarted
service_startServicesService started
service_stopServicesService stopped

Automatic alerts

Once configured, Cipi sends email notifications on:

  • Backup errors (S3 upload failures, dump errors)
  • Deploy failures (Deployer errors, rollback triggers)
  • System cron job failures (via the cipi-cron-notify wrapper)
  • App lifecycle events — notifies when an app is created, edited, or deleted, including server hostname, app name, domain, and PHP version
  • Sudo and su elevation — notifies when any user successfully elevates via sudo or su, including who ran it, target user (for su), SSH key, client IP, and TTY
  • Privileged SSH login — notifies when root or any sudoer logs in via SSH, including source IP, SSH key fingerprint, and key comment
  • SSH key changes — notifies when an SSH key is added to, removed from, or renamed on the cipi user, including hostname, IP, fingerprint, key comment, timestamp, and remaining key count. Rename alerts also include the old and new key name.

Every email notification includes a footer with the client IP (SSH_CLIENT) and the SSH key name used to authenticate, when applicable. The key name is resolved via SSH_USER_AUTH with an auth.log fallback when needed.

Security auth notifications

Cipi integrates PAM-based authentication notifications via pam_exec.so with ExposeAuthInfo enabled. When SMTP is configured, the system automatically sends email alerts on these security-relevant events:

  • Sudo and su elevation — triggered when any user successfully runs sudo or su. The notification includes the username, target user (for su), the TTY, SSH key, client IP, and timestamp.
  • Privileged SSH login — triggered when root or any user in the sudo group logs in via SSH. The notification includes the username, source IP address, SSH key fingerprint, and key comment (resolved from /var/log/auth.log fingerprint matching against authorized_keys).
  • SSH key changes — triggered when an SSH key is added to, removed from, or renamed on the cipi user via cipi ssh add, cipi ssh remove, or cipi ssh rename. The notification includes the hostname, server IP, key fingerprint, key comment, timestamp, and remaining key count. Rename alerts also include the old and new key name.
  • App lifecycle events — triggered when an app is created, edited, or deleted. The notification includes the server hostname, app name, domain, and PHP version.

Notifications run asynchronously in the background so they never delay login or command execution. If SMTP is not configured, the hooks fail silently with no impact on the system.

Security event log

Regardless of SMTP configuration, all notification events (SSH key changes, app lifecycle, password resets, sudo/su/SSH login, cron failures) are always logged to /var/log/cipi/events.log in a compact one-line format. The log is rotated daily with 1-year retention via logrotate.

Cron wrapper

The cipi-cron-notify utility wraps system cron jobs and sends a notification if the job exits with a non-zero code. This is useful for monitoring critical scheduled tasks.

Log Retention (GDPR)

Cipi enforces automatic log rotation policies designed to meet GDPR and general data protection requirements. Logs are rotated and deleted automatically — no manual cleanup needed.

Category Logs Retention
Application Laravel, PHP-FPM, workers, deploy, system 12 months
Security Fail2ban, UFW firewall, authentication, Cipi events (events.log) 12 months
HTTP / Navigation Nginx access and error logs 90 days
HTTP/navigation logs (nginx access logs) contain IP addresses, which are personal data under GDPR. The 90-day retention ensures compliance with the data minimization principle while preserving enough history for debugging and security analysis. Application and security logs are retained for 12 months to support audit trails and incident investigation.

Valkey

Valkey is the in-memory data store that Cipi installs as part of the default stack. Since v4.5.6 Cipi provisions Valkey instead of redis-server. It excels at caching, session storage, message queues, real-time broadcasting, and rate limiting.

Why Valkey instead of Redis

Valkey is the truly open-source, BSD-licensed fork of Redis, stewarded by the Linux Foundation. It was created in 2024 after Redis Inc. relicensed Redis away from the permissive BSD license to the source-available SSPL / RSALv2 — a change that no longer met the open-source definition. Backed by AWS, Google Cloud, Oracle, and a large community, Valkey continues the same battle-tested codebase under a license that stays free forever. That makes it a perfect fit for Cipi's MIT, no-vendor-lock-in philosophy, and it ships natively in Ubuntu 24.04's Universe repository (packages valkey-server + valkey-tools) — no third-party PPA to trust.

Just as importantly, Valkey is a drop-in replacement: it speaks the exact same RESP protocol on the same port (127.0.0.1:6379), honours the same requirepass / bind directives, and reads the same RDB/AOF data format. Your apps need zero changes — the phpredis extension and your existing REDIS_* .env values keep working exactly as before.

How Cipi implements it

  • Installsetup.sh installs and configures Valkey (/etc/valkey/valkey.conf, service valkey-server), bound to localhost only and protected with a password.
  • Service managementcipi service … manages valkey-server (the names redis-server, redis, and valkey are still accepted as aliases). It is added to the unattended-upgrades blacklist, so Cipi manages it instead of an automatic upgrade.
  • Credentials — stored under valkey_user / valkey_password in /etc/cipi/server.json (the legacy redis_* keys are still read as a fallback). Host: 127.0.0.1, Port: 6379.
  • Password resetcipi reset valkey-password regenerates the password and restarts the service (cipi reset redis-password stays as an alias).

Migrating from Redis (4.5.6 / 4.5.7)

Existing servers are switched to Valkey automatically on cipi self-update — no app .env editing required. The migration reuses the current Redis password (recovered from server.json or /etc/redis/redis.conf), forces an RDB SAVE and snapshots dump.rdb/AOF, purges redis-server, installs valkey-server + valkey-tools on the same port with the same requirepass / bind, restores the dataset, and rewrites server.json (redis_*valkey_*) and the unattended-upgrades blacklist — so cache, sessions, and queued jobs survive the switch.

v4.5.7 corrects the package name to valkey-server (the Ubuntu 24.04 daemon package; 4.5.6 initially used valkey) and makes the migration fully self-contained and safe. It auto-enables the universe APT component when the package isn't found, runs a post-start health check (PINGPONG with the password), and rolls back to redis-server — restoring both the saved password and the dataset — if Valkey can't be installed or doesn't come up healthy. The dataset snapshot is kept until the switch is verified, then cleaned up. The migration is idempotent: servers already on Valkey skip it.

Laravel integration

Add these variables to your .env via cipi app env myapp. The variable names stay REDIS_* — that is what phpredis and Laravel's redis driver expect, and Valkey answers on the very same socket:

env
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=your-password-from-server-json
REDIS_PORT=6379

Then set the drivers for each use case:

  • CacheCACHE_STORE=redis
  • SessionSESSION_DRIVER=redis
  • QueueQUEUE_CONNECTION=redis (then cipi worker restart myapp)
  • BroadcastingBROADCAST_CONNECTION=redis

Install the phpredis PHP extension for best performance, or use predis/predis as a pure-PHP fallback. Both talk to Valkey transparently.

Self-Update

Cipi can update itself from GitHub without affecting any app, database, or configuration.

bash
$ cipi self-update --check   # check for a new version
$ cipi self-update           # update to latest

Update process

  1. Downloads the latest version from GitHub
  2. Backs up the current installation to /opt/cipi.bak.YYYYMMDDHHMMSS/
  3. Replaces CLI and lib scripts
  4. Runs any pending migration scripts in order (e.g. new Nginx directives, new packages)
  5. Updates the version file

Migration scripts live in lib/migrations/ and are named by version (e.g. 4.1.0.sh). When updating from v4.0.0 to v4.2.0, Cipi automatically runs 4.1.0.sh and 4.2.0.sh in order. Your apps, databases, and configurations are never touched.

For example, the 4.5.5 migration retro-fits existing apps with the new ll='ls -al' shell alias: it appends the alias to each app's ~/.bashrc once (only when missing, preserving ownership), so apps created before 4.5.5 get it on the next cipi self-update.

Automatic maintenance crons

Cipi schedules several root-level jobs during installation. App-level crontabs (scheduler, deploy trigger) are separate — see User crontab.

Schedule Job
Daily 02:00 cipi backup run — S3 backups for all apps
Daily 03:00 cipi backup prune --weeks=4
Daily 03:50 cipi self-update (wrapped by cipi-cron-notify)
Sun 04:10 cipi ssl renew
Daily 04:15 Panel API maintenance (cipi-api-maintain — prune jobs/metrics)
Daily 04:30 cipi api update — soft-update panel Laravel + cipi/api

Wildcard domains

Cipi does not support wildcard domains (*.myapp.com) natively. The block is twofold and architectural — not a configuration detail.

Why wildcards are not supported

1 — Domain validation rejects *
Every domain passed to cipi alias add (and cipi app create) is validated against a strict regex that requires the string to start with [a-zA-Z0-9]. The asterisk fails immediately, before nginx or certbot are ever touched.

2 — Certbot uses HTTP-01 challenge, which cannot issue wildcard certs
cipi ssl install calls certbot --nginx, which relies on the HTTP-01 (or TLS-ALPN-01) challenge — placing a verification file on disk and serving it over port 80. Let's Encrypt only issues wildcard certificates via the DNS-01 challenge, which requires programmatic access to your DNS provider's API. Cipi does not integrate with any DNS provider, so even if the validation were bypassed, certbot would refuse to issue the wildcard cert.

Recommended alternative — Multi-SAN certificate

If your subdomains are fixed and enumerable (e.g. api, admin, www, staging), the correct approach is to add each one as an explicit alias and let Cipi issue a single SAN certificate covering all of them:

bash
$ cipi alias add myapp api.myapp.com
$ cipi alias add myapp admin.myapp.com
$ cipi alias add myapp www.myapp.com
$ cipi ssl install myapp   # single cert, SAN covers all domains

Certbot's --expand flag (used internally by Cipi) adds the new SANs to the existing certificate without issuing a new one. The SAN list has no meaningful limit for typical use.

Manual wildcard certificate (outside Cipi)

If you need dynamic subdomains (e.g. <tenant>.saas.com), you can obtain a wildcard certificate manually using a DNS plugin for certbot and place it on the server. Cipi will not manage, renew, or track it — you own the lifecycle entirely.

bash
# example with the Cloudflare DNS plugin
$ pip install certbot-dns-cloudflare
$ certbot certonly --dns-cloudflare \
    --dns-cloudflare-credentials /root/.cloudflare.ini \
    -d "*.myapp.com" -d "myapp.com"

After obtaining the certificate, edit the nginx vhost for the app directly (/etc/nginx/sites-available/myapp) to reference the wildcard cert paths and add server_name *.myapp.com myapp.com;. Then reload nginx:

bash
$ nginx -t && systemctl reload nginx
Running cipi ssl install myapp after manual wildcard setup will overwrite your custom nginx SSL directives with a Let's Encrypt HTTP-01 certificate. If you manage a wildcard cert manually, avoid running cipi ssl install on that app.

Edit Nginx config

To customise the Nginx vhost for an app, edit the site config directly. After changes, test and reload Nginx.

bash
$ sudo nano /etc/nginx/sites-available/<app>
$ sudo nginx -t && sudo systemctl reload nginx

Uninstall Cipi

Cipi does not provide a built-in uninstall command. If you need to completely remove Cipi from a server, follow the steps below in order. This procedure removes every component that Cipi installs — users, services, packages, configs, and data.

This is a destructive and irreversible operation. All apps, databases, SSL certificates, and server configurations managed by Cipi will be permanently deleted. Back up everything you need before proceeding. After uninstalling, the recommended approach is to reprovision the server from scratch.

1 — Stop and remove all apps

For each app managed by Cipi, remove its system user, home directory, database, nginx vhost, PHP-FPM pool, and supervisor config.

bash
# List all app users (members of cipi-apps group)
$ grep cipi-apps /etc/group

# For EACH app user, remove everything
$ supervisorctl stop <app_user>:*
$ rm -f /etc/supervisor/conf.d/<app_user>.conf
$ rm -f /etc/nginx/sites-enabled/<app_user>
$ rm -f /etc/nginx/sites-available/<app_user>
$ rm -f /etc/php/*/fpm/pool.d/<app_user>.conf
$ rm -f /etc/sudoers.d/cipi-<app_user>
$ mysql -e "DROP DATABASE IF EXISTS <app_user>; DROP USER IF EXISTS '<app_user>'@'localhost'; DROP USER IF EXISTS '<app_user>'@'127.0.0.1';"
$ userdel -r <app_user>

2 — Remove the Cipi user and groups

bash
$ userdel -r cipi
$ groupdel cipi-ssh 2>/dev/null
$ groupdel cipi-apps 2>/dev/null

3 — Remove Cipi binaries, libraries, and data

bash
$ rm -f /usr/local/bin/cipi
$ rm -f /usr/local/bin/cipi-worker
$ rm -f /usr/local/bin/cipi-cron-notify
$ rm -f /usr/local/bin/cipi-auth-notify
$ rm -rf /opt/cipi
$ rm -rf /etc/cipi
$ rm -rf /var/log/cipi

4 — Remove Cipi API (if installed)

bash
$ systemctl stop cipi-queue 2>/dev/null
$ systemctl disable cipi-queue 2>/dev/null
$ rm -f /etc/systemd/system/cipi-queue.service
$ systemctl daemon-reload

5 — Remove Cipi cron jobs

bash
# Edit root crontab and remove all Cipi entries
$ crontab -e
# Remove lines referencing: cipi self-update, certbot renewal, cache cleanup, RAM drop

6 — Remove Cipi configuration files

bash
# Sudoers
$ rm -f /etc/sudoers.d/cipi-sudo
$ rm -f /etc/sudoers.d/cipi-api

# Logrotate
$ rm -f /etc/logrotate.d/cipi-app-logs
$ rm -f /etc/logrotate.d/cipi-http-logs
$ rm -f /etc/logrotate.d/cipi-security-logs

# Unattended upgrades
$ rm -f /etc/apt/apt.conf.d/50cipi-unattended-upgrades
$ rm -f /etc/apt/apt.conf.d/20cipi-auto-upgrades

# System profile and MOTD
$ rm -f /etc/profile.d/cipi-env.sh
$ echo "" > /etc/motd

# MariaDB custom config
$ rm -f /etc/mysql/mariadb.conf.d/99-cipi.cnf

# PHP custom config (all versions)
$ rm -f /etc/php/*/fpm/conf.d/99-cipi.ini

# Nginx default page
$ rm -f /etc/nginx/sites-available/default
$ rm -f /etc/nginx/sites-enabled/default

7 — Purge installed packages

Remove all packages that Cipi installed. Skip any package you want to keep for other purposes.

bash
$ systemctl stop nginx mariadb valkey-server fail2ban supervisor
$ systemctl stop php*-fpm

$ apt purge -y nginx* mariadb-server mariadb-client valkey-server \
    fail2ban supervisor certbot python3-certbot-nginx \
    php8.4* php8.5* nodejs

$ apt autoremove -y
$ apt autoclean

8 — Remove APT repositories

bash
$ add-apt-repository --remove ppa:ondrej/php -y
$ rm -f /etc/apt/sources.list.d/mariadb.list
$ rm -f /etc/apt/sources.list.d/nodesource.list
$ rm -f /etc/apt/keyrings/mariadb-keyring.pgp
$ apt update

9 — Remove Composer and Deployer

bash
$ rm -f /usr/local/bin/composer
$ rm -f /usr/local/bin/dep

10 — Remove swap file

bash
$ swapoff /var/swap.1
$ rm -f /var/swap.1
# Remove the swap entry from /etc/fstab
$ sed -i '/swap\.1/d' /etc/fstab

11 — Restore SSH and PAM defaults

Cipi hardens SSH (disables root login and password auth) and adds PAM hooks. If you need to restore defaults:

bash
# Restore sshd_config to allow password auth (if needed)
$ sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
$ sed -i 's/^PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config

# Remove Cipi PAM hooks
$ sed -i '/cipi-auth-notify/d' /etc/pam.d/sshd
$ sed -i '/cipi-auth-notify/d' /etc/pam.d/sudo

# Restore sysctl
$ sed -i '/vm.swappiness/d' /etc/sysctl.conf
$ sysctl -p

$ systemctl restart sshd

12 — Reset firewall

bash
$ ufw disable
$ ufw reset
After a full uninstall, the server will be stripped of its web stack and security hardening. The recommended approach is to reprovision the server from a clean OS image rather than trying to reconfigure the same machine. Use this guide primarily to clean up before a fresh start, or to selectively remove Cipi components while keeping packages you still need.