Deploy WordPress on Cipi: custom app & GitHub auto-deploy
By Andrea Pollastri · Last updated: · free to read, no paywall
Cipi is Laravel-first, but it also hosts WordPress as a custom app: isolated Linux user, PHP-FPM pool, Nginx vhost, Git pull into htdocs. This guide walks from an empty Ubuntu VPS to a site that deploys itself on every push to main.
Why a custom app (not Laravel)
Cipi supports two app types. The default is Laravel: own system user, PHP-FPM or Octane, MariaDB, .env, Supervisor workers, crontab and zero-downtime Deployer releases under current/shared. WordPress is none of that.
A custom app (cipi app create --custom) is the right shape:
- Classic deploy into
htdocs— nocurrent/sharedsymlink swap.cipi deploy blogpulls the repo into/home/blog/htdocs. - Nginx is already WordPress-ready:
index index.html index.phpandtry_files $uri $uri/ /index.php?$args. - Git is optional. Skip the repository for SFTP-only uploads; attach one later with
cipi app edit blog --repository=…. - No database, no
.env, no cron, no queues until you add them. WordPress needs a database — you create it in the next step.
Custom deploys are not zero-downtime. Keep theme and plugin diffs small, backup the database before risky releases, and let GitHub Actions be the gate so a red check never reaches production. For Laravel zero-downtime patterns, use the CI/CD for Laravel guide.
How to structure the WordPress repo
Treat Git as the source of truth for code, not for secrets or media. A repo that survives auto-deploy looks like this:
wordpress-site/
.gitignore
.github/workflows/deploy.yml
wp-admin/
wp-includes/
wp-content/themes/your-theme/
wp-content/plugins/your-plugin/
wp-config-sample.php
index.php
…
Put this in .gitignore so a pull never overwrites the live site’s state:
wp-config.php
wp-content/uploads/
wp-content/cache/
wp-content/upgrade/
wp-content/backup-db/
.htaccess
Because custom apps pull into htdocs instead of swapping a release directory, untracked files stay on disk. That is how uploads and wp-config.php survive every deploy.
- Core in Git is the simplest agency workflow: theme, plugins and WordPress itself versioned together.
- Composer / Bedrock also works — set
--docroot=web(orpublic) when the front controller is not at the repo root. - Do not commit production secrets. Generate salts on the server; never push a live
wp-config.php.
Create the custom app
Cipi must already be installed on a fresh Ubuntu VPS:
SSH in as cipi, then sudo -s. Create the WordPress app non-interactively (PHP 8.3 is a safe match for current WordPress; hot-swap later with cipi app edit):
$ cipi app create --custom --user=blog \
--domain=blog.example.com \
--repository=git@github.com:you/wordpress-site.git \
--branch=main \
--php=8.3
That provisions:
- Linux user
blogand home/home/blog - PHP-FPM pool on 8.3 and an Nginx vhost for
blog.example.com - SSH deploy key for the repository
- Deployer config that pulls into
/home/blog/htdocs
If you saved a GitHub Personal Access Token first, Cipi also adds the deploy key and a provider webhook automatically:
$ cipi git github-token ghp_xxxxxxxxxxxxxxxxxxxx
$ cipi git status
Fine-grained tokens need Administration and Webhooks set to Read and write on the target repo; classic tokens need the repo scope. Details are in the Git auto-setup docs.
SFTP-only (no Git yet) — omit the repository and upload into ~/htdocs as the app user:
$ cipi app create --custom --user=blog --domain=blog.example.com --php=8.3
Database, DNS and SSL
Custom apps do not get a database. Create one after the app exists — MariaDB is the default and what WordPress expects:
$ cipi db create --name=blog
Cipi prints the database name, user, password and a mariadb+ssh:// URL for TablePlus or DBeaver. Copy those values; they go into wp-config.php on the server, not into Git.
Point DNS A records for blog.example.com and www.blog.example.com at the VPS, then:
$ cipi alias add blog www.blog.example.com
$ cipi www add blog
$ cipi ssl install blog
cipi ssl install issues a Let’s Encrypt certificate that covers every alias (SAN) and turns on HTTPS redirect. cipi www keeps apex and www canonical. See SSL and www redirects.
First deploy and wp-config.php
Pull the repository once, then write the config as the app user so file ownership stays correct:
$ cipi deploy blog
$ su - blog
blog@server:~$ cd ~/htdocs
blog@server:~/htdocs$ cp wp-config-sample.php wp-config.php
blog@server:~/htdocs$ nano wp-config.php
Minimum production values (use the credentials from cipi db create):
define('DB_NAME', 'blog');
define('DB_USER', 'blog');
define('DB_PASSWORD', 'the-password-cipi-printed');
define('DB_HOST', '127.0.0.1');
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', '');
define('DISALLOW_FILE_EDIT', true);
define('DISABLE_WP_CRON', true);
define('WP_MEMORY_LIMIT', '256M');
Generate new salts from api.wordpress.org/secret-key and paste them in place of the placeholders. Open https://blog.example.com and finish the WordPress installer in the browser.
wp-config.php is untracked. A later cipi deploy will not delete it. If you ever re-clone the whole home directory, recreate the file from the same database credentials (cipi db password blog if you lost them).
GitHub Actions pipeline (recommended)
The Laravel Agent webhook (https://your-app.com/cipi/webhook) is a route inside a Laravel app. It does not exist on WordPress. For auto-deploy, have GitHub SSH into the server and run cipi deploy — the same pipeline SSH deploy Cipi documents for gated Laravel releases.
Pick one trigger. If Git auto-setup already created a provider webhook, disable it (or never create one) before you add Actions. Two deploys on the same push fight over the lock file.
1. Dedicated SSH key for CI
Generate an ed25519 key on your laptop. Add the public key to the server; store the private key as a GitHub secret. Do not reuse Git deploy keys or your personal SSH key.
# on your laptop
$ ssh-keygen -t ed25519 -C "ci-deploy-blog" -f ~/.ssh/ci_deploy_blog -N ""
$ ssh-copy-id -i ~/.ssh/ci_deploy_blog.pub cipi@your-server-ip
$ cat ~/.ssh/ci_deploy_blog
In the GitHub repository: Settings → Secrets and variables → Actions. Create:
SERVER_HOST— VPS IP or hostnameSERVER_SSH_KEY— full contents of the private key
2. Workflow file
Drop this in .github/workflows/deploy.yml:
name: Deploy WordPress
on:
push:
branches: [main]
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Guard tracked secrets
run: |
if git ls-files --error-unmatch wp-config.php >/dev/null 2>&1; then
echo "wp-config.php must not be committed"
exit 1
fi
- name: PHP syntax
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: none
- name: Lint theme and plugins
run: |
find wp-content/themes wp-content/plugins -name '*.php' -print0 \
| xargs -0 -n1 php -l
deploy:
runs-on: ubuntu-latest
needs: check
steps:
- name: Deploy via Cipi
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: cipi
key: ${{ secrets.SERVER_SSH_KEY }}
script: sudo cipi deploy blog
What this buys you:
- A quality gate — a committed
wp-config.phpor a PHP parse error never reaches the VPS. - One command on the server — Cipi pulls
mainintohtdocswith the app’s PHP binary. - Manual replay —
workflow_dispatchredeploys the same commit without another push.
Protect main: no direct pushes, required status checks, short-lived branches. The same trunk-based habits as the Laravel CI/CD guide, without pretending WordPress has Pest and Pint.
Watch the deploy from the server:
$ cipi app logs blog --type=deploy
3. Optional: backup before release
WordPress schema changes are often irreversible. Snapshot MariaDB first, then deploy:
script: |
sudo cipi db backup blog
sudo cipi deploy blog
Restore with cipi db restore blog backup.sql.gz if a release goes bad. Custom apps have no release symlink to roll back to — the database dump is your undo button. See cipi db.
Simpler path: Git auto-setup
If you do not need CI gates (solo site, trusted commits), skip Actions and let Cipi wire GitHub for you:
$ cipi git github-token ghp_xxxxxxxxxxxxxxxxxxxx
$ cipi app create --custom --user=blog --domain=blog.example.com \
--repository=git@github.com:you/wordpress-site.git --branch=main --php=8.3
On app create, Cipi adds the SSH deploy key and a webhook on the repository. The summary shows auto-configured ✓. A push to main then runs the same cipi deploy pipeline without a YAML file.
If you created the app before saving a token, print the values and add them by hand:
$ cipi deploy blog --key # Settings → Deploy keys
$ cipi deploy blog --webhook # Settings → Webhooks
Do not point a WordPress site at the Laravel Agent URL /cipi/webhook. That route exists only after you composer require cipi/agent inside a Laravel app. WordPress uses the provider webhook Cipi prints, or GitHub Actions over SSH — not both.
Cron, backups and everyday ops
Replace WP-Cron with system cron
Custom apps do not get a crontab. With DISABLE_WP_CRON set, hit wp-cron.php from the app user’s crontab:
$ su - blog
blog@server:~$ crontab -e
*/5 * * * * /usr/bin/php8.3 /home/blog/htdocs/wp-cron.php >/dev/null 2>&1
Match the PHP binary to the version you passed to app create. After cipi app edit blog --php=8.4, update the crontab path.
Backups
cipi db backup blog— dump MariaDB (schedule it nightly in root’s crontab or call it from Actions).- Keep
wp-content/uploadson the server (and in off-site backups). It is not in Git. - S3-compatible off-site backups:
cipi backup.
Useful commands
| Command | What it does |
|---|---|
cipi deploy blog |
Pull main into htdocs |
cipi app logs blog --type=deploy |
Deploy history |
cipi app logs blog --type=php |
PHP-FPM errors |
cipi app edit blog --php=8.4 |
Hot-swap PHP |
cipi app edit blog --branch=staging |
Change the deploy branch |
cipi db backup blog |
Dump MariaDB |
cipi ssl install blog |
Renew Let’s Encrypt (SAN) |
Put this into practice with Cipi
Cipi is the free, open-source deploy CLI used in this guide: one command turns a fresh Ubuntu VPS into a hardened server for Laravel and custom PHP apps such as WordPress — Nginx, PHP-FPM, MariaDB, SSL and Git deploys included.
Frequently asked questions
Should I create WordPress as a Laravel app or a custom app in Cipi?
Always as a custom app. Laravel apps expect artisan, Composer, a .env file and the releases/current layout. WordPress is a classic PHP site: cipi app create --custom deploys into htdocs with Nginx already set for index.php.
Does Cipi create a database for a WordPress custom app?
No. Custom apps ship without a database, .env, cron or queue workers. After the app exists, run cipi db create --name=<app> (MariaDB by default) and put those credentials in wp-config.php on the server.
Will a Git deploy wipe my WordPress uploads?
Not if they are untracked. Custom apps pull into htdocs instead of swapping a releases symlink. Keep wp-content/uploads and wp-config.php out of Git so media and secrets survive every cipi deploy.
Can I use the Cipi Agent webhook for WordPress?
No. The /cipi/webhook route lives inside a Laravel app via the cipi/agent package. For WordPress, trigger deploys from GitHub Actions over SSH with sudo cipi deploy, or use Cipi Git auto-setup (deploy key plus provider webhook) when you do not need CI gates.
Is WordPress deploy on Cipi zero-downtime?
No. Custom apps use classic deploy into htdocs — there is no current/shared symlink swap. Keep theme and plugin diffs small, take a cipi db backup before risky releases, and use GitHub Actions so a failed check never reaches the server.