Help:Gitops

From canasta

GitOps configuration management

Canasta supports git-based configuration management through the canasta gitops command group. Your installation's configuration files are stored in a private Git repository with encrypted secrets, providing change history, easy rollback, and optional multi-server deployments.

Overview

In the simplest case, a single server uses gitops purely for version-controlled configuration backup — every change is committed and pushed to a remote repository. No pull requests, no multi-server coordination — just canasta gitops add and canasta gitops push after making changes.

The same architecture extends to multi-server deployments. Changes are made on a source server, tested, pushed to the repo with an optional pull request for peer review, and then pulled onto production servers.

Note: Gitops manages configuration only — it does not back up databases or uploaded files. Use canasta backup separately for that.

Prerequisites

The following tools must be installed:

  • git-crypt — transparent encryption of secrets in the git repo
      • macOS: brew install git-crypt
      • Ubuntu/Debian: sudo apt install git-crypt
      • RHEL/Fedora: sudo dnf install git-crypt
  • gh (GitHub CLI) — only required when pull_requests: true is set in hosts.yaml

canasta gitops init checks for these and provides instructions if any are missing.

What gets tracked

The git repository contains:

  • Configuration filesconfig/ directory (Caddyfile customizations, PHP settings)
  • Environment templateenv.template with {{placeholders}} for host-specific values
  • Wiki farm templatewikis.yaml.template with {{wiki_url_<id>}} placeholders for host-specific wiki URLs
  • Per-host variableshosts/{name}/vars.yaml with secrets and host-specific values (encrypted by git-crypt)
  • Host inventoryhosts.yaml defining all servers and their roles
  • Extensions and skins — tracked as git submodules pinned to specific versions, or as regular files for custom extensions without their own repository
  • Custom filescustom/ directory for Dockerfiles, scripts, or other deployment files
  • Orchestrator overridesdocker-compose.override.yml (if present)
  • Public assetspublic_assets/ directory (logos, favicons)

What is NOT tracked (gitignored)

  • .env — generated from env.template + vars at deploy time
  • config/wikis.yaml — generated from wikis.yaml.template + vars at deploy time
  • config/admin-password_* — generated from vars at deploy time
  • docker-compose.yml — managed by the Canasta CLI
  • config/Caddyfile — auto-generated from wikis.yaml on restart
  • config/backup/ — database dumps created by canasta backup
  • images/ — uploaded files (covered by canasta backup)

Repository structure

The tree below shows the full directory layout of a gitops-managed installation. Most files and directories are part of every Canasta installation. Files marked are created by gitops init and only exist when gitops is in use. Files marked are not pushed to the gitops repository — they are either generated locally from templates, managed by the CLI, or excluded because they contain host-specific data.

canasta-config/
├── .gitattributes †
├── .gitignore †
├── .env ‡                      # rendered from env.template + vars
├── custom-keys.yaml †          # host-specific .env keys (optional)
├── env.template †              # .env template with {{placeholders}}
├── wikis.yaml.template †       # wikis.yaml template (wiki farms only)
├── hosts.yaml †                # host inventory and settings
├── hosts/ †                    # per-host variables
│   └── myserver/ †
│       └── vars.yaml †         # encrypted by git-crypt
├── config/
│   ├── wikis.yaml ‡            # rendered from template + vars
│   ├── admin-password_* ‡      # rendered from vars
│   ├── Caddyfile ‡             # generated on restart
│   ├── backup/ ‡               # database dumps
│   ├── Caddyfile.site
│   ├── Caddyfile.global
│   └── settings/
│       ├── global/
│       │   └── *.php
│       └── wikis/
│           └── {wiki-id}/
│               └── *.php
├── custom/                     # user files (Dockerfiles, extra configs, scripts)
├── extensions/                 # git submodules (or regular files for custom extensions)
├── skins/                      # git submodules (or regular files for custom skins)
├── public_assets/
├── images/ ‡                   # uploaded files
├── docker-compose.yml ‡        # managed by CLI
└── docker-compose.override.yml # if used

Initial setup

Gitops works with any existing Canasta installation — single wiki or wiki farm. You don't need to reinstall or recreate anything. The init command examines your running installation and builds the gitops repository around it.

All gitops commands accept -i <id> to specify the installation by its Canasta ID. If omitted, the command uses the current working directory.

1. Verify your installation

Make sure your installation is working and that .env, config/, and any extensions/skins are in their final state. Gitops will snapshot the current configuration as its starting point.

If you have a wiki farm, ensure config/wikis.yaml and all per-wiki settings under config/settings/ are in place. Admin passwords (config/admin-password_*) are automatically captured into the encrypted per-host vars.

If you don't have an installation yet:

canasta create -i mywiki -w main -n wiki.example.com

2. Check for custom secrets

Before initializing gitops, review your .env file for any secrets or host-specific values beyond the built-in set. Gitops automatically extracts the following into encrypted per-host variables:

  • Database and MediaWiki secrets: MYSQL_PASSWORD, WIKI_DB_PASSWORD, MW_SECRET_KEY
  • Backup credentials: RESTIC_REPOSITORY, RESTIC_PASSWORD, and any key starting with AWS_, AZURE_, B2_, GOOGLE_, OS_, ST_, or RCLONE_
  • Host-specific values: MW_SITE_SERVER, MW_SITE_FQDN, HTTP_PORT, HTTPS_PORT

Any .env key not in this list is committed as a literal value in env.template, which is not encrypted. If you have additional secrets (e.g., getenv('MY_API_KEY') in PHP settings, SMTP credentials), create a custom-keys.yaml file in the installation directory before running init:

keys:
  - MY_API_KEY
  - SMTP_PASSWORD

Then set their values:

canasta config set -i mywiki MY_API_KEY=... SMTP_PASSWORD=...

If you don't have any custom secrets beyond the built-in set, you can skip this step.

3. Initialize gitops

canasta gitops init -i mywiki -n myserver --repo git@github.com:yourorg/mywiki-config.git --key /path/to/gitops-key

To require pull requests for all changes instead of pushing directly to main:

canasta gitops init -i mywiki -n myserver --repo git@github.com:yourorg/mywiki-config.git --key /path/to/gitops-key --pull-requests

The remote repository must be empty (no commits, no README). Create an empty repository on GitHub/GitLab first, then pass its URL with --repo. The --key flag specifies where to export the git-crypt symmetric key.

This bootstraps a new gitops repository from the existing installation:

  1. Initializes a git repo in the installation directory
  2. Sets up .gitignore and .gitattributes
  3. Initializes git-crypt and exports the symmetric key
  4. Creates env.template by extracting the current .env and replacing host-specific values with {{placeholders}}
  5. Creates wikis.yaml.template by extracting wiki URLs from config/wikis.yaml and replacing them with {{wiki_url_<id>}} placeholders
  6. Creates hosts.yaml with this server as the first entry
  7. Creates hosts/myserver/vars.yaml with the actual values extracted from .env, wiki URLs, and admin password files
  8. Converts user-installed extensions and skins to git submodules
  9. Makes an initial commit
  10. Pushes to the remote

Store the exported git-crypt key securely — it is needed to unlock the repo on other servers and must never be committed to the repo.

Repairing a broken init

If canasta gitops init fails partway through (e.g., due to push errors or missing credentials), extensions may be left in a broken state — committed as regular directories instead of proper submodules. Running init again is blocked because the .git directory already exists.

Use --repair to fix submodule registration without re-initializing:

canasta gitops init --repair -i mywiki

This reads .gitmodules, detects extensions that were committed as regular trees instead of submodules, and re-registers them properly. After repairing, push the fix with canasta gitops push.

Environment template and variables

The env.template is the single source of truth for what configuration exists. Host-specific values (secrets, domain names, ports) are replaced with {{placeholders}}:

MW_SITE_SERVER={{mw_site_server}}
MW_SITE_FQDN={{mw_site_fqdn}}
MYSQL_PASSWORD={{mysql_password}}
MW_SECRET_KEY={{mw_secret_key}}
MW_DB_NAME=mediawiki
MW_SITE_NAME=My Wiki

Each host's vars.yaml supplies the actual values:

# hosts/myserver/vars.yaml
mw_site_fqdn: wiki.example.com
mysql_password: "my-db-pass"
mw_secret_key: "abc123..."
admin_password_main: "my-admin-pass"

At deploy time, canasta gitops pull renders the template with the host's vars to produce .env, and writes config/admin-password_* files from the corresponding vars.

Warning: The .env file is regenerated from env.template + vars.yaml on every pull. Manual edits to .env will be overwritten. To change a host-specific value, update hosts/{name}/vars.yaml. To change the structure of the .env (add or remove keys), edit env.template.

Wiki URL template

For wiki farms, config/wikis.yaml contains per-wiki URLs that differ between hosts (e.g., production.example.com vs localhost). The wikis.yaml.template works the same way as env.template — wiki URLs are replaced with {{wiki_url_<id>}} placeholders:

# wikis.yaml.template
wikis:
- id: main
  url: "<nowiki>{{</nowiki>wiki_url_main<nowiki>}}</nowiki>"
  name: Main Wiki
- id: docs
  url: "<nowiki>{{</nowiki>wiki_url_docs<nowiki>}}</nowiki>"
  name: Documentation

Each host's vars.yaml supplies the actual URLs:

# hosts/production/vars.yaml (in addition to .env vars)
wiki_url_main: production.example.com
wiki_url_docs: production.example.com/docs
# hosts/devbox/vars.yaml
wiki_url_main: localhost
wiki_url_docs: localhost/docs

At deploy time, canasta gitops pull renders wikis.yaml.template with the host's vars to produce config/wikis.yaml. This prevents local development from accidentally overwriting production URLs when pushing configuration changes.

Warning: Like .env, config/wikis.yaml is regenerated from wikis.yaml.template + vars.yaml on every pull. To change a wiki's URL for a specific host, update hosts/{name}/vars.yaml. To add or remove wikis, edit wikis.yaml.template.

Built-in placeholder keys

The following .env keys are automatically converted to placeholders:

Secrets (differ per host): MYSQL_PASSWORD, WIKI_DB_PASSWORD, MW_SECRET_KEY, RESTIC_REPOSITORY, RESTIC_PASSWORD

Backup backend credentials (auto-detected by prefix): Any key starting with AWS_, AZURE_, B2_, GOOGLE_, OS_, ST_, or RCLONE_

Host-specific values: MW_SITE_SERVER, MW_SITE_FQDN, HTTPS_PORT, HTTP_PORT

Additional keys can be added via custom-keys.yaml.

Host inventory

The hosts.yaml file defines the deployment targets:

Single-server (simplest case):

canasta_id: mywiki
hosts:
  myserver:
    role: both

When there is only one host, the role defaults to both and can be omitted.

Multi-server with pull requests:

canasta_id: mywiki
pull_requests: true
hosts:
  staging:
    role: source
  production:
    role: sink

Roles

Each host has a role that controls the direction of git flow:

Role Can push Can pull Use case
source Yes No Staging, dev — where changes originate
sink No Yes Production — receives config from the repo
both Yes Yes Single server, or dual-purpose server

Roles act as a safety guardrail — a sink host will refuse to push, preventing accidental commits of local drift on production.

Pull requests setting

The pull_requests setting controls how canasta gitops push behaves:

  • false (default) — commits push directly to main. Good for single-server setups or small teams.
  • true — push creates a branch and opens a pull request for review. Requires the gh CLI.

Common operations

Changing a setting

  1. Edit the settings file
  2. Test the change
  3. Stage and push:
canasta gitops add -i mywiki config/settings/global/MySettings.php
canasta gitops push -i mywiki -m "Enable VisualEditor by default"

Removing a file

To remove a tracked file from the repository:

canasta gitops rm -i mywiki config/settings/global/OldSettings.php
canasta gitops push -i mywiki -m "Remove obsolete settings file"

If pull requests are enabled, review and merge the PR. Then on sink hosts:

canasta gitops pull -i mywiki

Checking status

canasta gitops status -i mywiki

Shows the current host, role, commit info, uncommitted changes, and ahead/behind remote status.

Previewing changes before pulling

canasta gitops diff -i mywiki

Fetches without applying and shows what files would change.

Extensions and skins

Extensions and skins that have their own git repositories are tracked as git submodules, pinning each one to an exact commit. This ensures every server runs the same version and makes updates explicit and reviewable.

How init handles extensions and skins

During canasta gitops init, the CLI scans the extensions/ and skins/ directories. Each subdirectory that contains a .git folder is automatically converted to a git submodule using its origin remote URL. The original directory is removed and re-added via git submodule add.

Subdirectories that are not git repositories — such as custom extensions you wrote yourself or copied in without cloning — are left as regular files and committed directly to the repo. They are tracked like any other file, not as submodules. See Custom extensions below.

If a directory has a .git folder but no origin remote configured, it is skipped with a warning.

Adding a new extension or skin

To add a publicly available extension:

git submodule add https://github.com/wikimedia/mediawiki-extensions-Cite.git extensions/Cite

Then enable it in the appropriate settings file (e.g., config/settings/global/extensions.php), test, stage, and push:

canasta gitops add -i mywiki extensions/Cite config/settings/global/extensions.php
canasta gitops push -i mywiki -m "Add Cite extension"

On sink hosts after pulling, run canasta restart -i mywiki and then canasta maintenance update -i mywiki to run any database migrations.

Updating an extension or skin

cd extensions/MyExtension
git fetch && git checkout v2.0.0
cd ../..
canasta gitops add -i mywiki extensions/MyExtension
canasta gitops push -i mywiki -m "Update MyExtension to v2.0.0"

The submodule reference in the parent repo now points to the new commit. On sink hosts after pulling, run canasta maintenance update if the extension has schema changes.

Removing an extension or skin

Remove the submodule and its configuration:

git submodule deinit -f extensions/MyExtension
git rm -f extensions/MyExtension

Remove the wfLoadExtension or wfLoadSkin call from the settings file, then push.

Submodule initialization on new servers

When a new server joins the gitops repo via canasta gitops join, or when an existing server runs canasta gitops pull, the CLI runs git submodule update --init --recursive. This clones all submodule repositories and checks out the exact commits recorded in the repo.

This is necessary because git does not automatically clone submodules when cloning or pulling a repository — the submodule directories would otherwise be empty.

Converting a cloned extension to a submodule

If you cloned an extension directly with git clone rather than adding it as a submodule, you need to convert it before gitops can track it properly. If you haven't run canasta gitops init yet, the init command handles this automatically. If gitops is already initialized:

# Note the remote URL and current commit
cd extensions/MyExtension
git remote get-url origin
git rev-parse HEAD
cd ../..

# Remove the cloned directory and re-add as a submodule
rm -rf extensions/MyExtension
git submodule add https://github.com/org/MyExtension.git extensions/MyExtension

# Check out the same commit you were on
cd extensions/MyExtension
git checkout <commit-hash>
cd ../..

canasta gitops add -i mywiki extensions/MyExtension .gitmodules
canasta gitops push -i mywiki -m "Convert MyExtension to submodule"

Custom extensions without a git repo

Some extensions are custom code that doesn't live in a separate git repository — for example, a small extension you wrote specifically for your wiki. These are committed directly to the gitops repo as regular files, not submodules.

Since they are regular files in the repo, they are automatically synced to all servers on canasta gitops pull without any submodule commands. However, they cannot be independently versioned or pinned to a specific commit the way submodules can.

Adding a server

Backup and gitops are complementary systems. Backup (canasta backup) captures everything — databases, uploaded files, and configuration. Gitops tracks configuration only — settings files, extensions, skins, environment template, and wiki farm structure. When adding a server (whether a production replica or a local dev copy), you need both: the backup provides the database and files, while gitops gives you the shared configuration repo.

To add a new server to an existing managed wiki farm:

1. Back up the existing wiki farm

On the existing server:

canasta backup create -i mywiki

2. Create and restore on the new server

canasta create -i mywiki -w main -n production.example.com
canasta backup restore -i mywiki -b /path/to/backup.tar.gz

This ensures the new server has all wikis and their databases. The restore gives the new server its own generated passwords (in .env and config/admin-password_*), which are then captured as that host's vars during the join step below.

3. Set any custom environment variables

If the repo has a custom-keys.yaml, set the values on the new server:

canasta config set -i mywiki MY_API_KEY=...

4. Join the gitops repo

canasta gitops join -i mywiki -n production --repo git@github.com:yourorg/mywiki-config.git --key /path/to/gitops-key

This clones the repo, unlocks git-crypt, adds the host to hosts.yaml, extracts host-specific values (including wiki URLs) into vars.yaml, renders .env and config/wikis.yaml from templates, updates submodules, and pushes the new host entry back to the repo.

Setting up a local dev copy

The steps above also apply when setting up a local development copy of a production wiki. The key differences are:

  1. Restore from a remote backup instead of copying a file. Add the production server's backup credentials (RESTIC_REPOSITORY, RESTIC_PASSWORD, and any AWS_*/AZURE_*/B2_* keys) to your local instance's .env, then:
    canasta backup list -i mywiki
    canasta backup restore -i mywiki -s <snapshot-id>
  1. Edit config/wikis.yaml before joining to replace production URLs with local addresses (e.g., localhost). The join command captures these URLs into your host's vars.yaml, so they must be correct before you run it.
  1. Use --role source (or --role both) so you can push changes back to the repo:
    canasta gitops join -i mywiki -n devbox --role source \
      --repo git@github.com:yourorg/mywiki-config.git --key /path/to/gitops-key

After making and testing changes locally, push them to the repo and pull on the production server:

# Local
canasta gitops add -i mywiki config/settings/global/MyChange.php
canasta gitops push -i mywiki -m "Description of change"

# Production
canasta gitops pull -i mywiki

Removing a server

On a source host:

  1. Remove the host entry from hosts.yaml
  2. Optionally remove the hosts/{name}/ directory
  3. Push: canasta gitops push -i mywiki -m "Remove production-2"

The installation on the removed server continues to function — it simply is no longer managed through gitops.

What needs a restart?

Change Restart needed?
PHP settings files No — takes effect on next request
wikis.yaml Yes — Caddyfile must be regenerated
Caddyfile.site / Caddyfile.global Yes — Caddy reloads on restart
docker-compose.override.yml Yes
.env changes Yes
New extension/skin Yes — run canasta maintenance update
Extension version update (no schema change) No
Extension version update (with schema change) Run canasta maintenance update

canasta gitops pull and canasta gitops diff automatically report whether a restart or maintenance update is needed.

Secret management

Secrets are encrypted transparently using git-crypt. Files under hosts/ are configured in .gitattributes to be encrypted on push and decrypted on pull:

hosts/** filter=git-crypt diff=git-crypt

On servers with the key, vars files are readable as plain text. On GitHub or for users without the key, they appear as encrypted blobs.

Key management

Symmetric key (recommended for small teams):

A single key file is generated during canasta gitops init. Distribute it securely (e.g., via scp or a secrets manager) to each server that needs access. After unlocking, store the key outside the repo (e.g., /etc/canasta/gitops-key).

GPG-based (for larger teams):

Each team member and server has its own GPG key. Access is granted per-identity with git-crypt add-gpg-user. No shared key file needed, and access can be revoked individually (though re-keying is required).

Concern Symmetric key GPG-based
Setup complexity Low — one key file Higher — GPG keys for every user/server
Key distribution Must securely copy the key file No shared secret
Revoking access Change key and redistribute Remove identity and re-key
Best for Small teams, few servers Larger teams, frequent access changes

Workflow diagrams

Single server or small team (pull requests disabled):

[server] → edit & test → canasta gitops add → canasta gitops push → [git repo]

Multi-server with review (pull requests enabled):

[source] → edit & test → canasta gitops add → canasta gitops push → [PR] → review & merge → canasta gitops pull → [sink hosts]

Further reading