WordPress · custom apps · GitHub Actions

Deploy WordPress on Cipi: custom app & GitHub auto-deploy

By · 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.

In this guide
  1. Why a custom app (not Laravel)
  2. How to structure the WordPress repo
  3. Create the custom app
  4. Database, DNS and SSL
  5. First deploy and wp-config.php
  6. GitHub Actions pipeline (recommended)
  7. Simpler path: Git auto-setup
  8. Cron, backups and everyday ops
  9. FAQ

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:

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.

Create the custom app

Cipi must already be installed on a fresh Ubuntu VPS:

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

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:

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:

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:

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

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.

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

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.

Keep reading