Skip to content
← Back to projects

Cipi

Easy Laravel Deployments

Cipi logo

#Cipi

Easy Laravel Deployments
One command installs a complete production stack. One command deploys your app.
No panel, no bloat — so you can focus on what you love: building your application.

Website · Docs · Report a bug


#What is Cipi?

Cipi turns any Ubuntu VPS into a multi-app PHP hosting platform — Laravel by default (PHP-FPM or optional Octane/FrankenPHP), with full isolation, zero-downtime deploys, SSL, queue workers, and S3 backups — all managed from a single CLI. Use --custom for simple sites: classic deploy (no releases/shared), configurable docroot and Nginx (try_files, entry point), no DB or cron.

No web panel. No bloat. No sleepless nights fighting Nginx configs or PHP-FPM pools.
Just SSH and the cipi command.

wget -O - https://cipi.sh/setup.sh | bash

Run this on the server, over SSH — not on your own machine — as a user with sudo.

Works on DigitalOcean, AWS EC2, Hetzner, Vultr, Linode, OVH, Google Cloud, Scaleway, and more.


#From zero to production in 3 steps

1. Install Cipi on a fresh Ubuntu 24.04 or 26.04 VPS (~10 minutes). SSH into the server first and run it there, as a user with sudo:

wget -O - https://cipi.sh/setup.sh | bash

The installer asks for the public half of an SSH key (~/.ssh/id_ed25519.pub on your own machine; ssh-keygen -t ed25519 creates one if you have none — on Windows, from PowerShell or WSL). It ends by printing a root password: keep it in a password manager. Root is reachable only by logging in as the cipi user with that key and then running su root.

2. Create your app (Laravel by default, or cipi app create --custom for a simple deploy):

cipi app create
# username, domain, git repo, branch, PHP version
# → Laravel: user, DB, Nginx, workers, cron, webhook
# → Laravel Octane: cipi app create --octane
# → Custom: user, Nginx, PHP-FPM; Git optional (empty = SFTP-only to ~/htdocs)

3. Deploy and go live:

cipi deploy myapp
cipi ssl install myapp

That's it. Your Laravel app is live.


#Stack

Every app gets a fully isolated environment. Laravel (default): zero-downtime deploy, DB, workers, cron, webhook — optionally Octane (FrankenPHP) instead of PHP-FPM. --custom: classic deploy into htdocs, configurable docroot only; Nginx uses index index.html index.php, try_files $uri $uri/ /index.php?$args, error_page 404 /404.html. No DB, no .env, no cron, no workers.

Component Details
Web server Nginx reverse proxy with per-app virtual hosts — PHP-FPM or Octane (proxy_pass), optimized for Laravel
PHP & Composer Selectable per app — PHP 7.4 to 8.5, hot-swappable
Runtime PHP-FPM pools by default; optional Laravel Octane (FrankenPHP) per app (--octane)
Database MariaDB (default) + optional PostgreSQL; dedicated DB and user per Laravel app
Search Optional Meilisearch for Laravel Scout — native binary, loopback only, per-app scoped key + index prefix
Queue workers Supervisor with per-app pools — queue:work or Horizon; optional Reverb for WebSockets (wss:// + credentials + fd limits)
Deployments Deployer — Laravel: atomic symlink, 5 releases, rollback, optional Node build; Custom: clone into htdocs
SSL Let's Encrypt via Certbot — HTTP-01 by default; optional DNS-01 (Cloudflare) + wildcards
Security Fail2ban + UFW, optional CrowdSec (firewall bouncer), optional Cloudflare Zero Trust (cipi zt: tunnel, Access, origin dark), and nightly integrity/upload scan, per-app Linux user + PHP-FPM/Octane + SSH key
Healthchecks HTTP probes every 5 minutes, plus a post-deploy check with optional automatic rollback of a broken release
Backups Backup profiles: what, how often, where, how long — S3/S3-compatible/local, client-side encryption
Frontends Node apps: SPA and static sites served by nginx, SSR (Next, Nuxt, SvelteKit, Astro, Remix) with blue/green deploys, per-app Node version
Configuration cipi ini for php.ini; optional per-project cipi.yml for aliases, www, basic auth, redirects and proxies, search, databases, workers, Reverb, limits, deploy recipe, forced HTTPS, required .env names, scheduled commands, backups and post-deploy steps

#Features

#🔒 Security & Isolation by Design

Each app runs under its own Linux user with an isolated filesystem, PHP-FPM pool (or Octane process), and database. A compromise in one app cannot touch the others. Configs are encrypted at rest with AES-256 (Vault). GDPR-compliant log rotation included. Per-app resource limits (cipi app limits) cap FPM children, memory, Octane workers, and queue processes. cipi app fix-permissions restores that layout if a deploy or a zip-as-root left the home unreadable.

Optional, off until you turn them on — setup.sh / self-update never install them:

cipi crowdsec enable    # IP reputation → firewall bouncer (not a WAF)
                        # includes a TLS rescue URL: one GET allowlists your IP
cipi zt enable          # Cloudflare Zero Trust: tunnel + real_ip, ports stay open
                        # then hostname / Access / lock http / lock ssh as you choose
cipi scan enable        # nightly: release integrity + ClamAV on uploads

Ubuntu security updates land daily via unattended-upgrades. PHP patch releases are applied every Sunday by cipi php upgrade. Nginx, MariaDB, PostgreSQL and Valkey are left alone until you ask — restarting a database at 04:00 is not a surprise Cipi will create:

cipi nginx upgrade                 # nginx.org mainline, config test, reload
cipi db upgrade                    # MariaDB, and PostgreSQL if installed
cipi db upgrade pgsql --yes
cipi service upgrade valkey

Patch-level only (apt --only-upgrade of what is already installed). Cipi configs are kept. --yes skips the prompt.

#⚡ Zero-Downtime Deploys

Deployer clones your repo, runs composer install, links storage, runs migrations, and swaps the symlink atomically. Optional Node build on deploy (cipi app edit --node-build=…). Roll back to any of the last 5 releases instantly. Opt-in pre-deploy DB snapshot (cipi deploy --snapshot).

Every deploy is recorded in a root-owned, hash-chained audit ledger (/var/log/cipi/deploys.jsonl, also sent to syslog), whatever started it: cipi deploy, the Git webhook, cipi/agent, the panel, or dep deploy run by hand. Each record has the release, the commit, and who started the deploy, read by root from the process chain: origin, login user and SSH address. cipi deploy <app> --audit shows it.

#💾 Backups That Match How You Actually Work

Backups are driven by profiles: each one decides what it takes (application files, databases, or both), which apps and databases it covers, how often it runs, where it lands and how long it is kept. A 30-minute database-only copy kept on disk sits happily next to a nightly full copy encrypted to S3.

cipi backup profile add hourly-db --scope=db \
    --databases='shop,tenant_*' --exclude-tables='*.jobs,*.telescope_*' \
    --every=30m --keep=48 --dest=local

cipi backup profile add nightly --scope=all \
    --cron='0 2 * * *' --keep-days=14 --dest=s3 --encrypt

Databases are discovered from the engine, so tenant databases an app creates at runtime are backed up too. The schedule is written to root's crontab for you. cipi backup verify checks the newest run actually opens, and an overdue profile raises an alert — a backup that quietly stopped running is worse than none, because it still looks configured.

#📄 cipi.yml — Configuration That Travels With the Code

An app can carry a cipi.yml in its repository describing the state it expects: domain aliases, the www redirect, HTTP basic auth, redirects and prefix proxies, Meilisearch for Scout, PHP version, settings and limits, its extra databases, its queue workers, its healthcheck, its deploy recipe options (keep_releases, the artisan hooks, the pre-deploy snapshot), the forced HTTPS redirect, the .env variables it requires (names only, never values), its scheduled commands (crons), its backup strategy, and post-deploy steps (deploy.post). cipi yml plan shows exactly what would change and cipi yml apply applies it.

Server reconciliation (aliases, PHP, workers, databases, …) is opt-in: cipi yml auto <app> on makes every successful deploy run cipi yml apply (from cipi deploy or the Git webhook). A release without the file is a no-op.

deploy.post is different — it runs after every successful deploy as soon as the section is in the committed file, with no auto switch. Steps use allowlisted runners only (artisan, npm, composer, php, node, …); no shell, no pipes. After Deployer, optional cipi yml apply, then deploy.post, then the post-deploy healthcheck.

deploy:
  post:
    - artisan cache:clear
    - npm run build
  # post_on_failure: abort   # default warn — log + email, release stays live
app:
  www: to-root                 # www → apex
  basic_auth:
    users: [admin]             # keeps the password set on the server
  limits:
    memory_limit: 512M         # cipi app limits, same bounds as the CLI
redirects:
  - from: /blog/
    to: "https://blog.example.com/"
proxies:
  - prefix: /api/
    upstream: "http://127.0.0.1:3000"
    strip_prefix: true
ssl:
  force_https: true            # needs a certificate; only ever turned on here
env:
  required: [STRIPE_KEY]       # names the .env must carry — plan blocks if missing
crons:
  - every: 30m                 # same allowlisted runners as deploy.post
    run: artisan queue:prune-batches
deploy:
  keep_releases: 3             # cipi app deploy-config, from the repository
  migrate: false
  snapshot: true               # DB snapshot before each deploy
cipi yml generate myapp > cipi.yml   # start from the server
cipi yml example myapp > cipi.yml    # or a commented template
cipi yml plan myapp                  # server diff + "After deploy" steps
cipi yml post-deploy myapp           # run deploy.post now (test)

It can only configure an app that already exists, its databases must be named <app> or <app>_*, its backup profiles <app> or <app>-*, and its healthcheck URL one of the app's own domains — so a commit can never reach beyond its own app. Passwords never go in the file: a basic auth user listed by name keeps the password already on the server, and only bcrypt or SHA-512 crypt hashes are accepted. Proxies declared in the file cannot publish Cipi's own local services (databases, Valkey, another app's Octane/Reverb) or link-local and cloud metadata addresses, even through a hostname that resolves to one. crons entries go through the same allowlisted runners as deploy.post — never a free-form shell line — and only replace the crontab entries the file manages, tagged # cipi-yml. app.limits values outside the CLI's bounds block the plan instead of being clamped, and ssl.force_https can only ever be turned on from the file.

#⚛️ Node Frontends: SPA, Static and SSR

React, Vue, Svelte, Next.js, Nuxt, SvelteKit, Astro and Remix get the same flow as Laravel: git push, releases, rollback, webhooks, SSL, basic auth, redirects, audit ledger.

cipi app create --user=shop --domain=shop.com --repository=git@github.com:acme/shop.git --framework=next
cipi app create --user=docs --domain=docs.com --repository=git@github.com:acme/docs.git --node=static --output=dist
cipi app create --user=dash --domain=dash.com --repository=git@github.com:acme/dash.git --framework=vite
  • spa: built on deploy and served by nginx. Unknown paths fall back to index.html, fingerprinted assets are cached as immutable, HTML is revalidated.
  • static: pre-rendered output (Astro, nuxt generate, Next output: 'export') with real 404s.
  • ssr: a Node server on localhost behind nginx, with blue/green deploys. The new release starts on the idle slot and must answer on its health path before nginx switches to it; the old process stops only after that. A release that never comes up fails the deploy and the old one keeps serving. cipi node restart <app> uses the same switch.

Dependencies are installed exactly as locked (npm ci, pnpm, yarn or bun, detected from the lockfile), and the build runs with NODE_ENV=production. shared/.env is linked into every release, so it works for build-time variables (VITE_*, NEXT_PUBLIC_*) and runtime secrets alike. Presets (--framework=next|nuxt|sveltekit|astro|remix|vite) fill in the mode, build, start command and output folder; --build, --start, --output and --health-path override them.

Node per app, from nodejs.org. --node-version=22|24 installs the official build into /opt/cipi/node/<major>, verified against the release's SHA-256 list, with corepack for pnpm and yarn. Apps on different majors run side by side. cipi node upgrade --restart moves every app to the latest patch with no downtime.

Laravel apps too. cipi node default 24 makes that major the server-wide Node for Laravel asset builds (--node-build), cipi app run npm and cipi.yml post-deploy steps, overriding the system Node from NodeSource without uninstalling it (cipi node default system goes back). One app that needs something else keeps its own: cipi app edit legacy --node-version=22.

The same settings can live in the repository as a node: section of cipi.yml (framework, mode, version, build, start, output, health_path). With cipi yml auto <app> on, each deploy reads them from the commit being deployed, before the build, so a change ships together with the code that needs it.

Git webhooks work without cipi/agent: Cipi verifies GitHub/Bitbucket HMAC signatures and GitLab/Azure tokens itself, in a PHP pool that can do nothing but queue the app's own deploy. The start command runs as an argument list, never through a shell. .env is read as data, not sourced, and the server binds to 127.0.0.1 on a port Cipi assigns.

#🚀 Laravel Octane (FrankenPHP)

Serve Laravel via Octane instead of PHP-FPM — same server, side by side with classic apps:

cipi app create --octane
cipi app convert myapp --to=octane   # or --to=fpm

Nginx proxies to a localhost Octane port; Supervisor runs ${app}-octane. Requires laravel/octane in the app repo (starts after the first successful deploy).

#📡 Reverb, Horizon & Scheduler

First-class Laravel extras, all CLI-managed:

cipi app reverb enable myapp
cipi worker horizon enable myapp
cipi schedule on myapp

Enabling Reverb allocates a localhost port, writes a Supervisor program, and proxies the two paths the Pusher protocol uses — /app/{key} for the WebSocket and /apps/{id}/… for the broadcast API — through Nginx on the app's own domain. It also generates REVERB_APP_ID/KEY/SECRET and the VITE_REVERB_* copies the frontend build reads, picks ws:// or wss:// from whether the app has a certificate (and moves to wss:// by itself when one is installed later), and raises the file-descriptor limits that otherwise cap a WebSocket server at about a thousand concurrent clients.

An app with its own /app or /apps route cannot use Reverb behind the same domain — those paths belong to the protocol. Reverb can also be declared in cipi.yml as workers.reverb: true.

Horizon is mutually exclusive with queue:work workers. Scheduler toggles the crontab schedule:run entry.

#🔎 Meilisearch for Laravel Scout (optional)

A self-hosted alternative to Algolia, off by default and never installed by setup.sh or cipi self-update:

cipi search install          # binary + systemd unit on 127.0.0.1:7700
cipi search enable myapp     # scoped key + Scout settings in the app .env

Meilisearch is a single static Rust binary, so it installs natively like the rest of the stack — no Docker in the loop. enable writes SCOUT_DRIVER, MEILISEARCH_HOST, MEILISEARCH_KEY and SCOUT_PREFIX into shared/.env; then composer require laravel/scout meilisearch/meilisearch-php and php artisan scout:import "App\Models\Post" in the app.

One instance, real isolation. Meilisearch has no per-tenant databases, so Cipi gives every app an API key scoped to the index pattern <app>-* — with only the actions Scout calls, never key management — and writes SCOUT_PREFIX itself rather than leaving it to you. Cipi usernames contain no hyphens, so blog- can never overlap blogs-: an app that ignores the prefix gets a 403 rather than its neighbour's documents. The master key stays in the vault and in a root-only EnvironmentFile, never in an app .env and never on a command line.

cipi search key rotate <app> reissues one key; --master rotates the master key and rewrites every app's .env in the same run (Meilisearch derives each key from the master, so they all change at once). cipi search upgrade swaps the binary and does the in-place store upgrade, rolling the binary back if the engine refuses — and only dropping the indexes if you say so, since scout:import rebuilds them. Indexes are derived data, so they are deliberately not part of cipi backup.

For a small dataset you may not need any of this: Scout's database driver, or Postgres full-text, needs no extra service.

#📦 Optional host tools

Some projects need a binary on the host — an image optimiser, ffmpeg, pdftotext. cipi package installs them from Ubuntu's own repositories, from a closed allowlist:

cipi package list
cipi package install image-optimizers   # or ffmpeg, imagemagick, poppler-utils
cipi package remove ffmpeg

The allowlist is the feature: without it this would be a root apt shell with extra steps. An entry has to be a stateless binary from an Ubuntu repo — no daemon, no port, no credentials, no state that outlives the process — with a real Laravel package behind it. Anything else is a service, and goes through cipi search, cipi db install or the container branch. Chromium is the instructive rejection: on Ubuntu 24.04 chromium isn't a deb at all and chromium-browser is a transitional package that depends on snapd, so "just apt-install it" would quietly add a self-updating daemon.

install shows what apt actually intends to pull — package count and disk — and asks before running. remove purges only what is present, then shows which orphaned dependencies autoremove would take before touching them.

Already in the base stack, so not in the list: the Imagick PHP extension, Ghostscript and fonts-dejavu-core (Recommends of php-imagick), and Node 22 (cipi node default changes it).

#🔗 Webhook Auto-Deploy

Native GitHub, GitLab, Bitbucket, Azure DevOps, Cursor Origin, and AWS CodeCommit integration — deploy keys (and webhooks where the forge has them) configured automatically. HMAC signature verification. Or plug in any custom Git provider. If a token expires and keys/webhooks drift, cipi git refresh re-registers them on every app (--rotate-keys / --rotate-secret to mint new material).

#📦 App Types

Laravel (default) — zero-downtime deploy with releases, shared storage, workers, scheduler, webhook; add --octane for FrankenPHP. --custom — for simple sites (e.g. WordPress, static+PHP): classic deploy into htdocs (no current/shared), choose docroot only (e.g. /, www, dist). Nginx: index index.html index.php, try_files $uri $uri/ /index.php?$args, error_page 404 /404.html. No DB, no .env, no cron, no workers, no webhook — just Nginx, PHP-FPM, and deploy key.

Clone an app for staging with cipi app clone <src> --domain=….

#🌐 Aliases, www & SSL

Add multiple domains or subdomains to any app. A domain can be a wildcard (*.example.com), as the app's primary domain or as an alias — multi-tenant apps get one vhost for every tenant. Manage www/apex aliases and canonical redirects with cipi www. A single SAN certificate covers all of them — HTTP-01 by default, or DNS-01 via Cloudflare for wildcards (cipi ssl install --dns=cloudflare --wildcard). Auto-renew handles the rest.

#↪️ Redirects & prefix proxies

Redirects and reverse proxies are written into the app's nginx vhost, so they cost nothing at runtime and never reach PHP. Every change runs nginx -t and is reverted if nginx refuses it.

cipi redirect set old-shop --to=https://shop.example.com   # whole app, path kept, 301
cipi redirect disable old-shop                             # serve it again, target kept
cipi redirect add shop /blog/ https://blog.example.com/ --308
cipi redirect add shop /promo /sale --302
cipi proxy add shop /api/ http://10.0.0.5:8080 --strip-prefix   # /api/users → :8080/users
cipi proxy add shop /events/ http://127.0.0.1:3000 --no-buffering

A proxy only takes its prefix; the rest of the app is served as usual. WebSockets pass through, X-Forwarded-* headers are set, and basic auth covers the prefix when it is on.

#❤️ HTTP Healthchecks

Point Cipi at an HTTP endpoint; it probes every 5 minutes and alerts after consecutive failures:

cipi health set myapp --url=https://example.com/up --expect=200

#📟 System Monitor & Chat Alerts

cipi health watches your apps; cipi monitor watches the server itself. Every 5 minutes it checks disk usage, SSL certificate expiry, system services (nginx, MariaDB, PHP-FPM, …), queue workers and Horizon, HTTP 5xx spikes in the access logs, read-only filesystems, and load average. Alerts fire on state changes only — one message when something breaks, one when it recovers, and a quiet reminder every 4 hours while it stays broken. No dashboards, no metrics storage: just a message when it matters.

cipi monitor                      # run all checks now
cipi monitor set disk --warn=80 --crit=90

Alerts reach you where you actually look. Email works out of the box (cipi smtp configure); chat channels take one command and apply to every Cipi notification — deploys, backups, scans, logins, monitor alerts:

cipi notifications channel add slack ops --url=https://hooks.slack.com/services/…
cipi notifications channel add discord ops --url=https://discord.com/api/webhooks/…
cipi notifications channel add telegram ops --token=<bot-token> --chat-id=<id>
cipi notifications channel add ntfy phone --url=https://ntfy.sh/my-topic --priority=high

#📋 Compliance Evidence (ISO 27001 / SOC 2)

Cipi is self-hosted software, so it cannot be certified itself: ISO 27001 certifies an organisation, SOC 2 attests a service. What an agency or SaaS under audit needs is evidence that its servers meet the controls. cipi compliance report collects it in one read-only pass: SSH hardening, firewall, fail2ban, OS patches, kernel network parameters, TLS protocols and certificates, local accounts and sudo, SSH key fingerprints, API tokens (expiry, last use, scope), GUI 2FA, config encryption at rest, the deploy audit ledger (every deploy with commit, origin and operator, hash chain verified, cross-checked against Deployer's own release log), backup freshness, log forwarding, monitoring and alert delivery, NTP, and malware scanning. Each control is mapped to ISO/IEC 27001:2022 Annex A and SOC 2 criteria.

cipi compliance                   # pass / warn / fail per control, exit 1 on fail
cipi compliance report --days=90  # report.md + report.json + raw evidence + SHA256SUMS, as a .tar.gz

The bundle has no secrets (no token hashes, no key material, no vault content) and is written root-only under /var/log/cipi/compliance/. Give the archive and its SHA-256 to your auditor. Organisational controls such as policies, risk assessment and incident response are still yours to cover.

#🤖 AI Agent Ready (MCP)

Cipi ships with a built-in MCP server. Laravel first: install the cipi-agent Laravel package, point your AI client at the endpoint, and deploy, rollback, query logs, and run Artisan commands via natural language — no SSH required.

Works with Claude, Cursor, VS Code, OpenAI, Gemini, and more.

composer require cipi/agent

MCP tools exposed: health, app_info, deploy, logs, db_query, artisan.

#🔌 REST API (optional)

When you need to manage apps programmatically or integrate with external pipelines, enable the optional API layer with a single command. Bearer tokens, granular permissions, OpenAPI spec available, interactive Swagger docs.

#🖥️ Web GUI (optional)

Multi-server control panel for operators who prefer a browser over SSH. Register N Cipi servers with API tokens, switch between them from any page, and manage apps, databases, deploys, SSL, aliases, and logs with Livewire UI and async job overlays. Install with cipi gui <domain> — requires cipi api on each managed server. Session login with optional Google Authenticator 2FA.

GitHub

#🔁 Sync Between Servers

Move entire stacks or single apps between Cipi servers — for migration, failover, or disaster recovery. Archives are encrypted in transit.

#🖥️ CLI Client

A standalone Go binary that talks to the Cipi REST API from your local machine — no SSH required. Manage apps, databases, SSL, aliases, and deployments from any terminal. Pre-built binaries for Linux and macOS (amd64/arm64).

Docs · GitHub

#🛒 WHMCS Module

An official provisioning module that bridges the WHMCS lifecycle to the Cipi REST API — automate app creation, deletion, SSL certificates, deployments, and package changes for your hosting customers. Self-contained drop-in, no Composer dependencies.

Docs · GitHub


#Who uses Cipi?

  • Solo developers — ship Laravel first, without the DevOps overhead
  • Agencies — one VPS, many isolated client projects (Laravel first), onboard a new client in minutes
  • Startups & SaaS — atomic deploys, instant rollbacks, grow without changing your workflow
  • Datacenters & automation pipelines — every Cipi command is a plain shell call, wire it into Ansible or any provisioning script

#Requirements

  • Ubuntu 24.04 LTS or 26.04 LTS (no other releases)
  • Root access
  • Ports 22, 80, 443 open

#Documentation

Full docs at: cipi.sh/docs


#Contributing

Cipi is open source and MIT licensed. Issues, PRs, and feedback are welcome on GitHub.


Made with ❤️ by Andrea Pollastri

New version available.