Protocol uses a few files to know what to do with your project. This guide explains what they are, where they live, and how they work together.
There are four things that configure Protocol:
protocol.json— Lives in your project. Tells Protocol what Docker image to use, how to deploy, and where your config repo is.- The config repo — A separate git repo next to your project. Holds your
.envfiles, nginx configs, cron schedules — anything that changes between environments. ~/.protocol/.node/key— Lives on each machine. The encryption key for your secrets.~/.protocol/.node/nodes/— On slave nodes only. Stores per-node deployment settings that persist across blue-green directory swaps.
For most projects, you only need the first three. Node config is created automatically when you set up a slave node via protocol init.
This is your project's identity card. Created by protocol init, committed to git, shared across all machines.
Here's what a typical one looks like:
{
"name": "myapp",
"project_type": "php82",
"deployment": {
"strategy": "release",
"pointer": "github_variable",
"pointer_name": "PROTOCOL_ACTIVE_RELEASE",
"secrets": "encrypted"
},
"docker": {
"image": "registry/myapp:latest",
"container_name": "myapp-web"
},
"git": {
"remote": "git@github.com:org/myapp.git",
"remotename": "origin",
"branch": "master"
},
"configuration": {
"local": "../myapp-config",
"remote": "git@github.com:org/myapp-config.git"
},
"bluegreen": {
"enabled": false,
"auto_promote": false,
"health_checks": []
}
}No credentials go in this file. It's committed to git. Docker passwords, API tokens, and encryption keys are handled through environment variables or the encrypted config repo.
| Setting | What it means |
|---|---|
name |
Project identifier, used for naming and lookups |
project_type |
PHP version for the initializer: php81, php82, or php82ffmpeg |
| Setting | What it means |
|---|---|
strategy: "release" |
Use versioned git tags. Nodes watch a GitHub variable for the active version. Recommended. |
strategy: "branch" |
Follow the tip of a git branch. Good for local dev. No rollback. |
pointer: "github_variable" |
How the active release version is stored. Currently only github_variable is supported. |
pointer_name |
The GitHub repository variable that stores the active release version. Default: PROTOCOL_ACTIVE_RELEASE |
secrets: "encrypted" |
.env files are encrypted in git and decrypted on arrival. Recommended for production. |
secrets: "file" |
.env files are used as-is. Fine for local dev. |
| Setting | What it means |
|---|---|
image |
The Docker image to pull/push |
container_name |
Which container to target for docker:exec and docker:logs |
local |
Path to your Dockerfile source (for docker:build) |
| Setting | What it means |
|---|---|
remote |
Your project's git remote URL |
remotename |
Name of the git remote (default: origin) |
branch |
The branch to track (branch mode only) |
| Setting | What it means |
|---|---|
local |
Where the config repo lives relative to your project (default: ../myapp-config) |
remote |
Git remote URL for the config repo |
environments |
List of environment names (branches) available |
These settings control zero-downtime shadow deployments. See Shadow Deployment for the full guide.
| Setting | What it means |
|---|---|
bluegreen.enabled |
Enable shadow deployment mode (true/false) |
bluegreen.auto_promote |
Automatically promote the shadow to production after health checks pass |
bluegreen.health_checks |
Array of health check definitions (see below) |
Note: releases_dir and git_remote are stored in NodeConfig under the release.* namespace, not in protocol.json. See NodeConfig (Runtime State) above.
Health check format:
{
"bluegreen": {
"health_checks": [
{"type": "http", "path": "/health", "expect_status": 200},
{"type": "command", "command": "curl -s localhost:8080/ping", "expect_exit": 0}
]
}
}This is where the real magic happens. Your project has a sibling directory — a completely separate git repo — that stores everything specific to an environment.
/opt/
├── myapp/ ← your code (one git repo)
│ ├── protocol.json
│ ├── src/
│ ├── nginx.conf → ../myapp-config/nginx.conf (symlink!)
│ └── docker-compose.yml
│
└── myapp-config/ ← your configs (separate git repo)
├── .env.enc ← encrypted secrets
├── nginx.conf ← server config
├── php.ini ← PHP settings
└── cron.d/ ← cron schedules
Because your production .env and your dev .env are totally different files. Your production nginx config listens on port 443 with SSL. Your dev one listens on port 80 with no SSL.
If you put these in your main repo, you'd need conditionals, templating, or a directory full of variations. With Protocol, each environment is just a branch in the config repo.
myapp-config/
└── .git/
└── refs/heads/
├── production ← production .env, nginx.conf, etc.
├── staging ← staging .env, nginx.conf, etc.
└── localhost-sarah ← Sarah's laptop .env, nginx.conf, etc.
When you run protocol config:env production, Protocol knows to check out the production branch of your config repo. When you run protocol start, it symlinks those files into your project.
| Type of file | Where it lives | Examples |
|---|---|---|
| Application secrets | Config repo (encrypted) | .env, database passwords, API keys |
| Server configuration | Config repo (plaintext) | nginx.conf, php.ini, cron schedules |
| Docker configuration | App repo | docker-compose.yml, Dockerfile |
| Project metadata | App repo | protocol.json |
| Runtime state | NodeConfig + per-release .protocol/deployment.json |
Active version, ports, container names |
# Create the config repo (the wizard handles everything)
protocol config:init
# Move files from your project to the config repo
protocol config:mv .env
protocol config:mv nginx.conf
# Encrypt your secrets
protocol config:init # → choose "Encrypt secrets"
# Push to remote
protocol config:saveprotocol config:switch stagingThis saves any changes, removes the old symlinks, switches to the staging branch, and creates new symlinks. Your app instantly has staging configs.
For symlinks to work inside Docker, the config repo needs to be mounted as a volume:
services:
web:
volumes:
- '.:/var/www/html:rw'
- '../myapp-config/:/var/www/myapp-config:rw'This way, the relative symlinks resolve correctly inside the container.
Runtime state is stored in NodeConfig (~/.protocol/.node/nodes/<project>.json) rather than in the project directory. This ensures state persists across blue-green directory swaps and isn't accidentally committed to git.
NodeConfig uses two namespaces for release and bluegreen state:
release.* — Shared release tracking (used by both release and bluegreen strategies):
| Key | Type | Description |
|---|---|---|
release.releases_dir |
string | Absolute path to the releases directory |
release.git_remote |
string | Git URL to clone releases from |
release.active |
string | Currently serving version |
release.previous |
string | Previous version (rollback target) |
release.target |
string | Version being built or promoted |
release.versions |
array | All known version tags |
bluegreen.* — Bluegreen-specific settings:
| Key | Type | Description |
|---|---|---|
bluegreen.shadow_version |
string | Version currently building in the shadow |
bluegreen.auto_promote |
boolean | Auto-promote after successful build |
bluegreen.health_checks |
array | Health check definitions |
bluegreen.promoted_at |
string | Timestamp of last promotion |
You never edit NodeConfig directly. Protocol manages it. The release.previous version is how protocol deploy:rollback knows where to go back to.
Each release directory stores its own runtime state in <release_dir>/<version>/.protocol/deployment.json:
{
"port": 80,
"status": "serving",
"container_name": "myapp-v1_2_0",
"deployed_at": "2024-01-15T10:30:01Z",
"watcher_pid": 12345
}This file is auto-generated during shadow:build and updated during promotions and rollbacks.
Each machine has several things set at the Protocol level (not per-project):
Set once per machine:
protocol config:env productionStored in Protocol's own config at config/config.php. This tells Protocol which config repo branch to use.
Common patterns:
production— live serversstaging— pre-productionlocalhost-sarah— Sarah's laptopci— CI/CD pipeline
Set once per machine:
protocol secrets:setup "your-64-char-hex-key"Stored at ~/.protocol/.node/key with 0600 permissions. This is the key that decrypts your .env.enc files. Same key on every machine.
When a server is set up as a slave/deployment node via protocol init, its configuration is stored in ~/.protocol/.node/nodes/<project>.json. This is separate from the project's protocol.json so that blue-green deployments can swap directories without losing track of settings.
{
"name": "myapp",
"node_type": "slave",
"environment": "production",
"repo_dir": "/opt/myapp",
"git": {
"remote": "git@github.com:org/myapp.git"
},
"deployment": {
"strategy": "release",
"pointer": "github_variable",
"pointer_name": "PROTOCOL_ACTIVE_RELEASE"
},
"release.releases_dir": "/opt/myapp-releases",
"release.git_remote": "git@github.com:org/myapp.git",
"release.active": "v1.2.0",
"release.previous": "v1.1.0",
"release.target": null,
"release.versions": ["v1.1.0", "v1.2.0"],
"bluegreen.enabled": true,
"bluegreen.shadow_version": null,
"bluegreen.auto_promote": true,
"bluegreen.health_checks": [
{"type": "http", "path": "/health", "expect_status": 200}
],
"bluegreen.promoted_at": "2026-03-10T21:00:00+00:00"
}Node config files are created with 0600 permissions and the directory with 0700. They persist across deploys and directory swaps.
Here's what happens when protocol start runs on a production node:
- Reads
protocol.json→ knows the Docker image, deploy strategy, and config repo URL - Reads
config/config.php→ knows this machine isproduction - Checks out the
productionbranch of the config repo - Sees
.env.enc→ reads~/.protocol/.node/key→ decrypts to.env - Symlinks
nginx.conf,php.ini, etc. into the project - Starts Docker with the decrypted secrets
- Starts watching for new releases
Every machine runs the same code but gets different configs because each one checks out a different branch. That's the whole trick.