Production Environment¶
- Status:
CURRENT RUNBOOK - Owns: approved production environment configuration and read-only inspection procedure
- Does not own: deployment authorization, current Production state proof or application secrets
- Update when: approved service, environment, credential-installation or inspection procedure changes
This document captures the production runtime environment contract for CTC. Keep application code in Git and keep production secrets outside the Git worktree.
PHP-FPM Environment Variables¶
Production runtime variables are managed in:
/etc/ctc/prod.env
php-fpm loads that file through the systemd drop-in:
/etc/systemd/system/php-fpm.service.d/override.conf
The drop-in should use:
[Service]
EnvironmentFile=/etc/ctc/prod.env
Restart=always
RestartSec=5
Do not add new per-key Environment= entries to the php-fpm systemd drop-in.
/etc/php-fpm.d/www.conf should keep:
clear_env = no
With clear_env = no, PHP can read variables loaded by systemd through getenv() without per-key env[KEY] = $KEY pass-through lines.
Updating Production Variables¶
Edit only the external env file:
sudo vi /etc/ctc/prod.env
Use quoted values to avoid parsing issues:
DB_HOST="db.cwto1o3arnd0.us-west-2.rds.amazonaws.com"
DB_NAME="prod"
DB_PORT="3306"
DB_USER="ctc"
DB_PASS="..."
TELEGRAM_TOKEN="..."
CTC_LOG_ROOT="/var/log/ctc"
GMAIL_CREDENTIALS_PATH="/etc/ctc/gmail/credentials.json"
GMAIL_ADDON_TOKEN="<private value in .agents/rules/DEV_ENV.md>"
After editing, validate and restart:
sudo systemctl daemon-reload
sudo php-fpm -t
sudo systemctl restart php-fpm
PHP-FPM does not reload process environment variables from the file until the service is restarted.
Permissions¶
Keep the env file root-owned and readable only by root:
sudo chown root:root /etc/ctc/prod.env
sudo chmod 600 /etc/ctc/prod.env
Do not commit production .env files or secrets into the repository.
Git Autopull¶
Production may periodically sync the worktree with:
git fetch --prune origin
git reset --hard origin/master
Because /etc/ctc/prod.env is outside /var/www/html, it is not affected by git reset --hard.
Runtime-generated files, uploads, logs, and production-only config should also stay outside Git-managed code paths unless the app explicitly owns them.
Gmail OAuth Credentials¶
Gmail OAuth client credentials must live outside the Git worktree. The app reads GMAIL_CREDENTIALS_PATH, then GOOGLE_APPLICATION_CREDENTIALS, then checks the external default:
/etc/ctc/gmail/credentials.json
Preferred installation from the local workstation:
./scripts/install-gmail-credentials.sh /absolute/path/to/rotated-google-oauth-client.json ctc
The source JSON must be the newly rotated OAuth client JSON from Google Cloud. Do not use a file from this repository or from /var/www/html/config/credentials.json.
The helper validates the OAuth client shape before and after install. The JSON must include web.client_id, web.client_secret, web.auth_uri, web.token_uri, and web.redirect_uris containing https://ctclogistic.com/public/gmail/callback.php. Override the expected callback only when deploying a different host:
EXPECTED_GMAIL_REDIRECT_URI="https://example.com/public/gmail/callback.php" \
./scripts/install-gmail-credentials.sh /absolute/path/to/google-oauth-client.json ctc
Manual production setup, if the helper script is not available:
sudo install -d -m 0750 -o root -g apache /etc/ctc
sudo install -d -m 0750 -o root -g apache /etc/ctc/gmail
sudo install -m 0640 -o root -g apache /path/to/google-oauth-client.json /etc/ctc/gmail/credentials.json
sudo -u apache test -r /etc/ctc/gmail/credentials.json
sudo -u apache php -r '$p="/etc/ctc/gmail/credentials.json"; $expected="https://ctclogistic.com/public/gmail/callback.php"; $json=file_get_contents($p); $data=is_string($json)?json_decode($json,true):null; $web=is_array($data)&&isset($data["web"])&&is_array($data["web"])?$data["web"]:null; $missing=array(); foreach(array("client_id","client_secret","auth_uri","token_uri") as $key){ if(!is_array($web)||!isset($web[$key])||!is_string($web[$key])||trim($web[$key])===""){ $missing[]="web.".$key; }} if(!is_array($web)||!isset($web["redirect_uris"])||!is_array($web["redirect_uris"])||count($web["redirect_uris"])===0){ $missing[]="web.redirect_uris"; } elseif(!in_array($expected,$web["redirect_uris"],true)){ $missing[]="web.redirect_uris expected callback"; } if(is_array($web)&&isset($web["client_id"])&&is_string($web["client_id"])&&substr($web["client_id"],-27)!==".apps.googleusercontent.com"){ $missing[]="web.client_id Google OAuth suffix"; } if(count($missing)>0){ fwrite(STDERR,"Invalid Gmail OAuth credential JSON: ".implode(", ",$missing).PHP_EOL); exit(1); }'
sudo vi /etc/ctc/prod.env
sudo chown root:root /etc/ctc/prod.env
sudo chmod 600 /etc/ctc/prod.env
sudo php-fpm -t
sudo systemctl restart php-fpm
The env file should contain:
GMAIL_CREDENTIALS_PATH="/etc/ctc/gmail/credentials.json"
GMAIL_ADDON_TOKEN="<private value in .agents/rules/DEV_ENV.md>"
Gmail Workspace Add-on Gateway¶
The Gmail add-on calls the public appointment gateway:
/public/gmail/addon.php
Configure a dedicated shared secret for Apps Script. Do not reuse the Gmail controller or operation token:
GMAIL_ADDON_TOKEN="<private value in .agents/rules/DEV_ENV.md>"
GMAIL_ADDON_SYSTEM_BASE_URL="https://ctclogistic.com"
GMAIL_ADDON_REVIEW_PATH="/transload-email-approval"
Set the matching Apps Script properties on the Gmail Workspace Add-on project:
GMAIL_ADDON_TOKEN=<same shared secret>
GMAIL_ADDON_API_URL=https://ctclogistic.com/public/gmail/addon.php
GMAIL_ADDON_TOKEN is used only for the Gmail Workspace Add-on gateway at /public/gmail/addon.php. Store the same value in the Apps Script project's script properties. GMAIL_ADDON_SYSTEM_BASE_URL and GMAIL_ADDON_REVIEW_PATH are optional when the defaults are correct. The Apps Script request must send Authorization: Bearer <GMAIL_ADDON_TOKEN> or X-CTC-Gmail-Addon-Token.
Do not restore config/credentials.json in the repository. A production deploy may run git reset --hard origin/master, which intentionally removes repo-local credentials.
If a repo-local config/credentials.json ever existed in production or Git history, treat that OAuth client secret as exposed. Rotate the Google OAuth client secret first, then install the new JSON at /etc/ctc/gmail/credentials.json. Do not copy the exposed file from /var/www/html/config/credentials.json into /etc/ctc/gmail/credentials.json.
Runtime Logs¶
JSONL runtime logs are written through JsonlLogger.
- Production should set
CTC_LOG_ROOT="/var/log/ctc"in/etc/ctc/prod.env. - The log root must be writable by the PHP-FPM runtime user.
- If
CTC_LOG_ROOTis not set, the app falls back tostorage/logsunder the web root. - Do not point runtime logs at
media/logs;mediamay be backed by EFS or upload storage and is not the durable log contract.
Expected production setup:
sudo install -d -m 0775 -o apache -g apache /var/log/ctc
sudo systemctl restart php-fpm
SSH, SFTP, And DB Access¶
Local operator SSH config should provide this alias:
ssh ctc
sftp ctc
Expected SSH target:
Host ctc
HostName ec2-34-220-102-225.us-west-2.compute.amazonaws.com
Port 2222
User ec2-user
IdentityFile ~/.ssh/ctc250616.pem
Quick SSH verification:
ssh ctc 'hostname && whoami && pwd'
Expected verified state on 2026-05-12:
- SSH user:
ec2-user - Remote home:
/home/ec2-user - Web root:
/var/www/html - Production env file:
/etc/ctc/prod.env
SFTP verification:
printf 'pwd\nbye\n' | sftp -b - ctc
Expected remote working directory:
/home/ec2-user
For production MySQL read checks from the local workstation, use the canonical
SSH path below. Do not assume the optional ctc-db helper is installed; check
with command -v ctc-db before using it. A missing helper is not a production
DB access failure.
ssh ctc 'sudo -n bash -s' <<'REMOTE'
set -euo pipefail
set -a
source /etc/ctc/prod.env
set +a
MYSQL_PWD="$DB_PASS" mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" --batch "$DB_NAME" \
-e 'SELECT @@hostname AS db_host, @@port AS db_port, DATABASE() AS db_name, CURRENT_USER() AS db_user, VERSION() AS version;'
REMOTE
/etc/ctc/prod.env is root-owned by design, so normal-user read failure is
expected. The query must run through ssh ctc and sudo -n; do not use
ssh hub or the repository-local .env, which targets the local DB mirror.
Always run and inspect the identity query before a ticket/data query.
Read-only DB smoke test:
Use the same canonical path with both read-only queries:
ssh ctc 'sudo -n bash -s' <<'REMOTE'
set -euo pipefail
set -a
source /etc/ctc/prod.env
set +a
MYSQL_PWD="$DB_PASS" mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" --batch "$DB_NAME" <<'SQL'
SELECT @@hostname AS db_host, @@port AS db_port, DATABASE() AS db_name, CURRENT_USER() AS db_user, VERSION() AS version;
SELECT COUNT(*) AS table_count FROM information_schema.tables WHERE table_schema = DATABASE();
SQL
REMOTE
Verified result on 2026-05-12:
db user version
prod ctc@% 8.0.45
table_count
59
Do not run production writes through this path unless the user explicitly confirms the exact write operation.
Verification¶
Use these checks after an environment change:
sudo stat -c "%U:%G %a %n" /etc/ctc/prod.env
sudo stat -c "%U:%G %a %n" /etc/ctc/gmail/credentials.json
sudo systemctl cat php-fpm
sudo grep -n "^[[:space:]]*clear_env\|^[[:space:]]*env\[" /etc/php-fpm.d/www.conf
curl -I http://localhost/login.php
Expected state:
/etc/ctc/prod.envisroot:rootand mode600./etc/ctc/gmail/credentials.jsonis outside/var/www/html, readable by the PHP-FPM runtime group, and not committed to Git.php-fpmusesEnvironmentFile=/etc/ctc/prod.env.www.confhasclear_env = no.www.confdoes not need per-keyenv[KEY] = $KEYlines.login.phpreturns HTTP 200 locally.
Deployment Scope¶
CTC deployment is triggered by the repository's master push. Do not run a
deployed-SHA confirmation, autopull.timer inspection, or service preflight as
part of a normal deployment. Follow the repository deployment rules and run
only the minimum post-deploy health check they require.
---¶
Source:
docs/runbooks/PRODUCTION_ENVIRONMENT_RUNBOOK.md— ctc repository · This page is an English translation of the source document. Identifiers, file paths and code symbols are unchanged.