Files
agent 779743fdb0
CI / tests-and-images (pull_request) Successful in 2m18s
Runner smoke test / runner-smoke (pull_request) Successful in 1s
fix: validate release tags by Gitea run ID
2026-10-26 10:56:59 +03:00
..

Production deployment

Compose runs Caddy, the Search API, and a private OpenSearch node. Only Caddy publishes ports 80 and 443; backend and OpenSearch have no host ports. The OpenSearch security plugin is disabled to match the current API client, so keep it on the private network.

Host access and configuration

The production environment file is /etc/akyldash/production.env; the review secret is /etc/akyldash/review_secret. Both must stay outside the checkout, root-owned, and mode 0600. DATA_ROOT must be an absolute path. The codex account is intentionally outside the sudo and docker groups because access to the Docker socket grants root-equivalent control. Run host operations with the narrowly required sudo command; do not use docker as codex directly.

After reviewing the host audit and confirming these files do not already exist, the owner can create them from the approved checkout with sudo bash infra/deploy/prepare-production-config.sh. The script refuses to overwrite existing configuration, installs the tracked env example with mode 0600, and generates a random review secret with mode 0600. It does not start Compose or create volumes. Review and set DATA_ROOT in the protected env file before deployment.

Set DATA_ROOT to the approved data directory and ensure the normalized corpus is at $DATA_ROOT/minjust-normalized/. The first host setup is documented in docs/operations/DEPLOYMENT.md; the root bootstrap installs packages and creates directories, but does not deploy the application or alter SSH, firewall, user groups, or sudoers. The restricted akyldash-compose helper allows only root-owned Compose config and status; service start/stop remains an owner-run root operation.

The repository Gitea Actions runner is an isolated CI worker described in docs/operations/CI_RUNNER.md. It has no production Docker socket, production env files, or application checkout. CI does not deploy or change production; all deployment remains owner-run.

First deployment and upgrades

Run commands from the root-owned checkout at /opt/akyldash/current. Render and review Compose before changing services:

cd /opt/akyldash/current
sudo docker compose --env-file /etc/akyldash/production.env -f deploy/production/compose.yaml config --quiet

On a clean first deployment, infra/backup/archive.py create reports that no backup is required when all persistent mounts are empty and no Compose containers exist. Continue without a backup in that case. If any persistent data exists, create and verify a backup before changing schema, images, or starting a new application version. This also applies when containers are absent, including an initial installation where the normalized corpus was preloaded under DATA_ROOT; create the archive before the first up. When named volumes do not exist, pass --initial-deployment; this is accepted only without Compose containers and when DATA_ROOT contains only minjust-normalized/. During upgrades, missing volumes are an error because they can indicate lost production data. The archive tool also refuses ambiguous storage state, running containers, and incomplete Compose service containers. See docs/operations/BACKUP.md for the exact commands. Do not remove an older usable backup to make room; retain the pre-change archive until the new deployment and a newer verified backup are confirmed.

For an initial clean installation, start the stack only after config review:

sudo docker compose --env-file /etc/akyldash/production.env -f deploy/production/compose.yaml up -d --build
sudo docker compose --env-file /etc/akyldash/production.env -f deploy/production/compose.yaml ps
curl -fsS https://akyldash.kg/review >/dev/null

Before this step, confirm DNS and have the owner review firewall rules for TCP 80 and 443. Caddy manages the origin certificate; confirm HTTPS before setting Cloudflare to Full (strict). On the initial start, create the admin account interactively:

sudo docker compose --env-file /etc/akyldash/production.env -f deploy/production/compose.yaml exec backend python -m search.api --create-admin

The account database is $DATA_ROOT/search-users.sqlite3 and is included in the data backup. Admin password reset is also interactive:

sudo docker compose --env-file /etc/akyldash/production.env -f deploy/production/compose.yaml exec backend python -m search.api --reset-admin-password

After the initial OpenSearch index is loaded and validated, the admin account is created, and /review works over HTTPS, create and verify the required post-setup baseline backup before treating deployment as complete. It captures the initialized index, account database, and Caddy certificate data. Follow the exact stop/create/verify/start sequence in docs/operations/BACKUP.md.

Backup, restore, and rollback

Backups are cold and require downtime. Stop services, create the archive, then verify both its checksum and contents before proceeding. Do not run an upgrade or schema change until verification succeeds. Keep archives root-only under /var/backups/akyldash and copy them to separately managed encrypted storage.

Restore is a staged operation on a marked replacement host; it does not overwrite production. For application-only rollback, use the guarded helper with a verified pre-release backup and explicit data-compatibility review. Follow the procedures in BACKUP.md, ROLLBACK.md, and RECOVERY.md.

Automated production deployment is not enabled. Deployments remain owner-run; record the release and verify service health before considering it complete.

Image release and owner-run cutover

PR CI still builds both production images with Kaniko --no-push. The separate Release production images workflow is manual and runs only when dispatched from protected main. It requires repository secrets named REGISTRY_USERNAME and REGISTRY_PASSWORD; without them it fails before any build. Configure those secrets only after the owner confirms a dedicated Gitea account/token with write access to the two repository packages and no unneeded administration rights. Do not reuse a runner or production host credential. The workflow uses the Gitea host and fixed package namespace git.vakanaut.ru/admin/akyldash/{backend,opensearch}, pushes unique commit/run-ID tags, captures registry digests, validates the release descriptor, and uploads the JSON plus SHA-256 sidecar as a Gitea Actions artifact using a full-SHA-pinned Gitea-compatible v3 action. PR CI also uploads a tiny synthetic marker with one-day retention to verify the runner's runtime artifact path without registry credentials. The runner cache stays disabled; artifact compatibility is based on the runtime API supported by act_runner 0.2.13/Gitea 1.25.5 and will be empirically checked by PR CI. Registry and artifact retention must preserve release digests and descriptors for the full rollback window; the owner must confirm both policies.

The schema-v3 descriptor records source repository/ref/commit, workflow run ID, pinned Kaniko builder and both image digests. It is an unsigned trace record, not a cryptographic attestation; deployment relies on retrieving it from the authenticated Gitea run artifact and verifying its checksum and repository allowlist. Do not treat the sidecar alone as proof of authenticity.

For a future owner-approved cutover, use the root-owned checkout and run infra/deploy/deploy_release.py as root with the downloaded manifest, its matching verified cold backup, and infra/deploy/release-policy.json, plus owner-verified non-empty Russian and Kyrgyz canary queries, for example --canary-ru '<known Russian query>' --canary-ky '<known Kyrgyz query>'. The helper checks both queries against the public Search API through local HTTPS after Compose reports healthy, and requires at least one result with a document code before recording the new release state. Select queries only after corpus/index canaries have been verified; do not use generic terms that can match unrelated test data. The helper requires the backup image references and image IDs to match the live stack, serializes deploy and rollback with /run/akyldash-deploy.lock, atomically switches both protected env image refs, and always runs Compose with --no-build. It records recovery state before changing references. If ref replacement reports an error after an atomic rename, recovery rereads the actual file and restores the prior pair before restarting; an unexpected or unverifiable pair stops recovery and leaves a secret-free journal. A failed cutover refuses a subsequent release until an owner resolves the journal. Rollback accepts a digest-pinned previous release only when that backup matches the separately recorded previous refs and image IDs. Legacy tag rollback remains supported. Neither the helper nor registry secrets are installed on the VPS by this PR; deployment remains an owner-only operation and must wait for host/data/backup review. The exact sanitized evidence required from the owner is listed in DEPLOYMENT_OWNER_PACKET.md.