# Deployment — release 0.20.1

This release supplies deployment configuration and local verification. Production acceptance remains open until the target host passes the checks in `acceptance.md`. No public deployment is performed by the release scripts.

## Standard topology

Use Node.js 24 within the package engine range, MySQL 8.0.34+ (prefer a reviewed MySQL 8.4 release), Redis, and one trusted HTTPS reverse proxy. The standard profile requires MySQL. For the separate MariaDB/Passenger/finite-cron profile, follow [Spaceship Supreme](spaceship-supreme.md); its live acceptance is still pending. The durable job queue and scheduler leases live in MySQL; Redis serves rate limits and caches. Web, worker and scheduler are separate long-running processes. Choose **one** process manager: systemd, Docker Compose, or PM2. Do not run duplicate managers against the same installation.

Use a dedicated runtime database principal with the DML privileges required by application tables and SELECT on schema metadata/views. Keep migration DDL and backup/restore privileges on separate operator principals. Views/triggers execute with their recorded definer; preserve that controlled database identity through migration and restore. Do not grant runtime CREATE, ALTER, DROP, GRANT or FILE. Review table-level grants in staging before narrowing further. The application itself enforces double entry; database administrators remain trusted.

## Production environment

Copy `.env.example` into a protected operator-managed file and replace all placeholders. Never include that file in an image or ZIP. The validator remains authoritative.

| Setting                              | Required deployment choice                                                                           |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `NODE_ENV`                           | `production`                                                                                         |
| `APP_ORIGIN`, `TELEGRAM_WEB_APP_URL` | Actual HTTPS domain and `/miniapp` under that same origin                                            |
| `APP_HOST`                           | `127.0.0.1` for host systemd/PM2; `0.0.0.0` only inside the Docker app container                     |
| `TRUST_PROXY`                        | `loopback` for host Nginx; `1` for the supplied Docker topology, whose port is loopback-only         |
| `DATABASE_QUEUE_LIMIT`               | Positive finite limit; default 100, tune from measurements                                           |
| `DATABASE_SSL_MODE`                  | `required` for remote MySQL, with a trusted certificate matching its hostname                        |
| `REDIS_URL`                          | Authenticated URL; `rediss:` required outside loopback                                               |
| `RATE_LIMIT_STORE`, `REDIS_ENABLED`  | `redis`, `true`                                                                                      |
| `SESSION_COOKIE_SECURE`              | `true` or validated automatic HTTPS behavior                                                         |
| `SESSION_COOKIE_SAME_SITE`           | Test `none` with Secure cookies for Telegram Web iframe clients; native WebViews may work with `lax` |
| `SHUTDOWN_TIMEOUT_MS`                | At most 30000 with the supplied 45-second process-manager stop budget                                |
| `UPLOAD_ROOT`                        | Persistent `/srv/task-app/storage` on host; `/app/storage` in Docker                                 |
| `LOG_FORMAT`                         | `json`; body logging off                                                                             |

For a private certificate authority, configure Node's `NODE_EXTRA_CA_CERTS` to a mounted trusted CA file before startup; keep certificate verification enabled. The MySQL command-line backup client uses its own `ssl-ca` option. Do not expose MySQL, Redis, Node port 3000, private storage or backup files to the public network.

Production web, worker and scheduler now verify every migration filename/checksum and all seed history before starting. Pending migrations, changed history or a partial rollback prevent startup. Run migrations/seeds explicitly; runtime never creates metadata or migrates automatically. Remote Redis without TLS/authentication and unlimited database queues now fail environment validation.

## Fresh install / upgrade

1. Back up the database, uploaded files, release source and required encryption keys. Verify a restore rehearsal before a live upgrade.
2. Drain all traffic and stop web, worker and scheduler. Maintenance UI alone is insufficient: some routes intentionally remain available.
3. Install source into a new release directory. Run `npm ci --include=optional`, `npm run check`, and the live acceptance target in an isolated environment. Build native dependencies on the deployment OS/architecture.
4. With the protected migration environment, run `npm run migrate:dry-run`, then `npm run migrate`. Existing 0.19.1 installations already have migration 023; **0.20.0 adds no migration**. Fresh installs also need `npm run seed` and `npm run admin:create` using the documented password/TOTP bootstrap. Remove bootstrap credentials immediately afterward.
5. With runtime production credentials, run `npm run release:check`. It checks configuration, database version/schema/seeds/clock and Redis connectivity. It does not certify load, UI, provider delivery or backup recovery.
6. Start web, verify `/health/ready`, then start worker/scheduler. Complete `acceptance.md`, including two independent admin reviewers.
7. Configure the existing secure Telegram webhook only after verifying the intended bot and domain, following `TELEGRAM-AUTH-AND-BOT.md`. This is an operator action with external effects; no deployment script calls it automatically.
8. Reopen traffic gradually and monitor latency, database pool pressure, failed deliveries and worker iteration failures.

The environment loader reads a project `.env` or inherited process variables. With an external protected file on Linux, CLI examples can use `node --env-file=/etc/task-app/app.env bin/release-check.js`. Use the same mechanism for migration CLI with its distinct principal. Do not `source` an untrusted environment file as shell code.

## systemd and Nginx

Create an unprivileged `taskapp` account and directories `/srv/task-app/releases`, `/srv/task-app/storage`, `/etc/task-app`. Point `/srv/task-app/current` at the reviewed release. Keep `/etc/task-app/app.env` restricted to the service account/operator. Make only storage writable. Verify `/usr/bin/node` is the required Node 24 binary before installing the supplied `infrastructure/systemd/*.service` units.

Install `infrastructure/nginx/task-proxy.conf` as `/etc/nginx/snippets/task-proxy.conf`, and the server template in Nginx's enabled HTTP configuration. Replace the example domain and certificate paths, run `nginx -t`, then reload. Forwarding headers are overwritten at the trusted proxy. Test that spoofed incoming X-Forwarded-For cannot change the application's client IP. Header/body/time/connection limits apply at the proxy, while application route rate limits remain in Redis.

The template does not cache authenticated responses or serve storage directly. Request access logs omit query strings and headers. Restrict and rotate Nginx error logs, which can still contain URLs. systemd writes structured logs to journald; configure bounded retention and alerts on the host.

## Docker

The production Compose file contains only the app tier and expects separately managed TLS MySQL/Redis. It does not provision production database accounts, certificates or secrets. Review and pin an immutable base-image digest for the final build; the default Node 24 tag is a starting point and has not been image-scanned here.

```sh
docker build -f infrastructure/docker/Dockerfile --target runtime -t task-earning:0.20.0 .
```

Set `APP_IMAGE` to the reviewed image/digest and `APP_ENV_FILE` to the absolute protected runtime file before using `infrastructure/docker/docker-compose.yml`. Use a separate one-shot container with the migration environment for migrations. With a fresh database, run migrate then seed before starting web. Initialize/persist the named storage volume with ownership UID/GID 1000. The image runs unprivileged, with read-only source, dropped capabilities and limited memory/PIDs. The host Nginx connects to `127.0.0.1:3000`; the published port must stay loopback-only. Match stop budgets if changing shutdown configuration.

Worker/scheduler use process exit and operational monitoring; their web healthcheck is disabled. Compose `service_healthy` gates initial startup, not ongoing recovery or process health. Monitor worker iteration failures and outbox age separately. See [Docker startup ordering](https://docs.docker.com/compose/how-tos/startup-order/) for those semantics.

## PM2 / cPanel

`ecosystem.config.cjs` is an alternative host process manager. Install/review PM2 separately, start all three processes from the release root and provide protected environment configuration. Set up PM2 startup persistence and bounded log rotation on the host. PM2 is not added as an application dependency.

For cPanel use `infrastructure/cpanel/deployment-checklist.md`. A hosting plan without compatible MySQL, Redis, Node/native modules and persistent worker/scheduler processes cannot host this architecture reliably.
