Help:Gitops
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 backupseparately 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
- macOS:
- gh (GitHub CLI) — only required when
pull_requests: trueis set inhosts.yaml
canasta gitops init checks for these and provides instructions if any are missing.
What gets tracked
The git repository contains:
- Configuration files —
config/directory (Caddyfile customizations, PHP settings) - Environment template —
env.templatewith{{placeholders}}for host-specific values - Wiki farm template —
wikis.yaml.templatewith{{wiki_url_<id>}}placeholders for host-specific wiki URLs - Per-host variables —
hosts/{name}/vars.yamlwith secrets and host-specific values (encrypted by git-crypt) - Host inventory —
hosts.yamldefining 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 files —
custom/directory for Dockerfiles, scripts, or other deployment files - Orchestrator overrides —
docker-compose.override.yml(if present) - Public assets —
public_assets/directory (logos, favicons)
What is NOT tracked (gitignored)
.env— generated fromenv.template+ vars at deploy timeconfig/wikis.yaml— generated fromwikis.yaml.template+ vars at deploy timeconfig/admin-password_*— generated from vars at deploy timedocker-compose.yml— managed by the Canasta CLIconfig/Caddyfile— auto-generated from wikis.yaml on restartconfig/backup/— database dumps created bycanasta backupimages/— uploaded files (covered bycanasta 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 withAWS_,AZURE_,B2_,GOOGLE_,OS_,ST_, orRCLONE_ - 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:
- Initializes a git repo in the installation directory
- Sets up
.gitignoreand.gitattributes - Initializes git-crypt and exports the symmetric key
- Creates
env.templateby extracting the current.envand replacing host-specific values with{{placeholders}} - Creates
wikis.yaml.templateby extracting wiki URLs fromconfig/wikis.yamland replacing them with{{wiki_url_<id>}}placeholders - Creates
hosts.yamlwith this server as the first entry - Creates
hosts/myserver/vars.yamlwith the actual values extracted from.env, wiki URLs, and admin password files - Converts user-installed extensions and skins to git submodules
- Makes an initial commit
- 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
.envfile is regenerated fromenv.template+vars.yamlon every pull. Manual edits to.envwill be overwritten. To change a host-specific value, updatehosts/{name}/vars.yaml. To change the structure of the.env(add or remove keys), editenv.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.yamlis regenerated fromwikis.yaml.template+vars.yamlon every pull. To change a wiki's URL for a specific host, updatehosts/{name}/vars.yaml. To add or remove wikis, editwikis.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 tomain. Good for single-server setups or small teams.true— push creates a branch and opens a pull request for review. Requires theghCLI.
Common operations
Changing a setting
- Edit the settings file
- Test the change
- 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:
- Restore from a remote backup instead of copying a file. Add the production server's backup credentials (
RESTIC_REPOSITORY,RESTIC_PASSWORD, and anyAWS_*/AZURE_*/B2_*keys) to your local instance's.env, then:
canasta backup list -i mywiki
canasta backup restore -i mywiki -s <snapshot-id>
- Edit
config/wikis.yamlbefore joining to replace production URLs with local addresses (e.g.,localhost). The join command captures these URLs into your host'svars.yaml, so they must be correct before you run it.
- 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:
- Remove the host entry from
hosts.yaml - Optionally remove the
hosts/{name}/directory - 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
- CLI Reference — full list of subcommands, flags, and options
- git-crypt documentation — details on encryption and key management
- Backup and restore — for backing up databases and uploaded files