Files
Recruitment-HC/docs/deployment.md
T
NikoKS 26307ed370
Build and publish Docker image / build (push) Successful in 44s
change image
2026-10-15 07:57:08 +07:00

291 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Server deployment: Komodo + Docker Compose
This stack runs PHP 8.3/Apache, MySQL 8.4 LTS, and phpMyAdmin 5.2. Gitea Actions
builds and publishes `gitea.cptckp.com/nurinda/recruitment-hc:latest`; deployment
only pulls and runs images. No local build or separate Komodo Build resource is
required. `compose.yaml` is the only Compose file. MySQL has no published port.
Database, application sessions, and phpMyAdmin sessions use persistent named
volumes. Two one-shot services import an initial SQL dump safely and apply
migrations before Apache starts.
See [the image build guide](image-build.md) for CI registry secrets, runner
requirements, and registry pull credentials. Wait for a successful image build
before deploying.
## 1. Configure the Komodo Stack
Commit/push the deployment files, then create a **Stack** in Komodo:
| Setting | Value |
| --- | --- |
| Server | Your server running Komodo Periphery and Docker Compose |
| Source | This Git repository (configure a Git account if private) |
| Branch | The branch containing these files |
| Run directory | `.` (repository root) |
| Compose file paths | `compose.yaml` |
| Project name | `recruitment-hc` |
| Run build (`run_build`) | **Disabled** — Gitea Actions builds the image |
| Auto pull (`auto_pull`) | **Enabled** — pull the published image before deployment |
| Extra args | `--no-build` — image-only deployment |
| Ignore services | `db-init`, `migrate` — ignore exited setup services for status checks |
| Destroy before deploy | **Disabled** |
| Automatic image updates | **Disabled** — upgrade deliberately after backup |
Paste the settings from [`.env.example`](../.env.example) into the Stack's
**Environment** field, replacing both password placeholders with separate random
secrets. Generate each password with `openssl rand -hex 32`. Use Komodo secrets
if available; do not commit the populated environment to Git. Leave the environment
file path at `.env`: Komodo writes it and passes it to Compose.
For a private package, configure Komodo's Registry provider/account for
`gitea.cptckp.com` with a token allowed to read the image. Keep these credentials
separate from the CI write token.
Before deploying, transfer your trusted initial SQL dump to the Docker host and
set `DB_INIT_SQL_PATH` as described below. Deploy the Stack: startup waits for
MySQL, runs `db-init`, runs `migrate`, then starts the app. The one-shot services
should finish with exit code 0; they do not remain running.
For a files-on-host Stack instead, put the repository on the server, select
**Files on host**, set Run directory to its absolute path, and use the same
Compose/image/environment settings.
Official reference: [Komodo Compose Stacks](https://komo.do/docs/deploy/compose).
## 2. Automatic database initialization
Obtain a trusted recruitment SQL export containing `in_daper`, `in_soal`, and
`in_jawab`. Transfer it privately to the **Docker host**, outside the
checkout/document root, for example `/srv/recruitment-private/initial.sql`.
Restrict access to the file and set this in the Stack Environment (or `.env`):
```dotenv
DB_INIT_SQL_PATH=/srv/recruitment-private/initial.sql
```
For local Docker use, the default is
`./DB/recruitment_local_2026-10-14.sql`. That dump contains personal data and
existing accounts, not just schema. Review/sanitize production exports and remove
unwanted demo/test access before opening the site. Dumps are not shipped in the
app image or fetched from Git. The file is mounted read-only into `db-init`;
it must exist on the Docker host even on later deployments. A missing path fails
startup instead of silently creating a directory. Dumps must not reference the
reserved `rec_bootstrap_state` safety table; exclude it from exports/backups using
the command in section 5. This prevents a restored marker from masking a failed
import.
Normal deployment handles setup automatically:
1. `db` becomes healthy.
2. `db-init` imports the dump **only if the database contains no tables or views**.
A persistent `rec_bootstrap_state` table records `importing` before the import
and `imported` only after success and validation of the three base tables.
3. On subsequent deployments, an `imported` marker and the base tables skip the
dump entirely. A pre-existing database without a marker is accepted only if
all seven recruitment tables are present. Partial/unexpected databases are
refused, not overwritten. This check cannot validate the contents of a dump
imported outside this setup; review such databases yourself before deployment.
4. `migrate` applies the idempotent admin/exam migrations and validates readiness.
5. `app` starts only after both one-shot services exit successfully.
On the Docker server, change to the Stack's checked-out **run directory**. The
commands below assume project name `recruitment-hc` and `.env`; adjust every
command if your Stack uses different settings.
```sh
docker compose --env-file .env -p recruitment-hc ps -a
docker compose --env-file .env -p recruitment-hc logs db-init migrate
```
The import guard has a database-free regression suite:
`python3 dev/test-db-init.py`.
**Failed import:** SQL DDL is not transactional. An `importing` marker deliberately
blocks retries, including when only some tables were restored. Inspect the logs
and affected database, then restore a trusted backup or use a separately verified
empty database/volume. Do not delete the marker or run a dump over partial data
blindly. Never delete the production volume merely to retry setup.
Migrations can be rerun without resetting users or test progress. They run during
Compose deployment, not on an individual Apache container restart. Back up before
deploying changes against an existing database. Add future migrations to the
migration runner as part of their release. If a migration fails, fix the cause and
rerun setup without reimporting:
```sh
docker compose --env-file .env -p recruitment-hc run --rm migrate
docker compose --env-file .env -p recruitment-hc up -d
```
If you need a new admin account, run this from an **interactive SSH terminal**:
```sh
docker compose --env-file .env -p recruitment-hc exec app \
php dashboard/tools/create-admin.php
```
Use a unique email and a password of at least 12 characters. There is no built-in
Docker admin account. Imported accounts remain intact; check who has access.
## 3. Connect your existing HTTPS reverse proxy
Default upstream: **`http://127.0.0.1:8086`**. `APP_PORT` can change the host port.
This default assumes the reverse proxy runs on the Docker host itself.
Your reverse proxy must:
- Terminate HTTPS and redirect public HTTP to HTTPS.
- Preserve the original `Host` and **overwrite** `X-Forwarded-Proto` with `https`
for HTTPS requests (do not forward an untrusted client header).
- Forward the original client address for proxy logging/rate limiting.
- Disable response caching for the application and add appropriate login rate
limiting, particularly for `/login.php` and `/dashboard/login.php`.
Apache maps `X-Forwarded-Proto: https` to PHP's HTTPS flag, so both participant and
admin cookies receive `Secure`. The old cPanel handler/domain redirect in the
root `.htaccess` is intentionally not used in Docker. Container Apache rules deny
internal PHP, CLI tools, migrations, dotfiles, and SQL/config/backup downloads.
Example location inside an **existing Nginx HTTPS server block**:
```nginx
location / {
proxy_pass http://127.0.0.1:8086;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
```
If your proxy is itself in a Docker container, its `127.0.0.1` is **not** the host.
To keep the host-port approach, set `APP_BIND_IP` to a private host interface
reachable from that proxy, then use that host address/port as upstream. Restrict
access to the proxy with firewall rules appropriate to Docker's published-port
rules. Do not expose this trusted-header HTTP listener directly to the Internet;
`0.0.0.0` without those restrictions is not a safe default.
### Browser database access with phpMyAdmin
phpMyAdmin is a separate service, bound to **`127.0.0.1:8087`** by default.
It connects only to this stack's `db` service; automatic login and arbitrary
server selection are not enabled. Log in with **`MYSQL_USER` / `MYSQL_PASSWORD`**
from your Komodo environment, not your recruitment dashboard account. Select the
database named by `MYSQL_DATABASE`. Use the database-scoped user, not root.
For remote browser access without publishing another public endpoint, run this
on your own computer and keep the SSH connection open:
```sh
ssh -N -L 8087:127.0.0.1:8087 your-user@your-server
```
Then open **http://127.0.0.1:8087/** in your local browser. Adjust the right-hand
port if `PHPMYADMIN_PORT` differs; change the left-hand port if it is busy locally.
Leave `PMA_ABSOLUTE_URI` empty for this tunnel workflow.
Alternatively, configure a **separate, restricted HTTPS hostname** on your
existing reverse proxy, with upstream `http://127.0.0.1:8087`, and set
`PMA_ABSOLUTE_URI=https://db-admin.example.com/` (replace with your actual URL,
including the trailing slash). Follow the proxy/header guidance above and protect
this hostname with a VPN, IP allowlist, or additional proxy authentication. Disable
proxy caching. Keep it separate from the public recruitment site. If your proxy
runs in Docker, use a private reachable host address and set
`PHPMYADMIN_BIND_IP` accordingly, with firewall restrictions as described above.
Never expose phpMyAdmin directly on an unrestricted public HTTP port.
Add these new settings from `.env.example` to the Komodo Stack Environment and
redeploy. `PHPMYADMIN_PORT` defaults to `8087`; `PHPMYADMIN_UPLOAD_LIMIT` defaults
to `64M`. For browser imports, adjust your reverse proxy's upload size/timeouts as
needed; large dumps are better handled by the setup service in section 2.
Browser imports can also overwrite data. Prefer the automatic `db-init` service
for first-time setup; do not import another dump over an initialized database.
## 4. Verify
```sh
docker compose --env-file .env -p recruitment-hc ps -a
curl --fail http://127.0.0.1:8086/healthz
curl --fail http://127.0.0.1:8087/ > /dev/null
docker compose --env-file .env -p recruitment-hc logs --tail=100 app db db-init migrate phpmyadmin
```
Adjust the curl address if you changed the bind IP or port. `/healthz` returns
`200 ok` only when Apache/PHP, the DB connection, base tables, and required
migration tables/admin identity columns are ready; otherwise it returns a generic
`503 not ready` without database details. Docker health checks use this endpoint.
phpMyAdmin has its own HTTP health check; it does not depend on application
migrations being complete. Its login screen is available once MySQL is healthy.
An unhealthy container does not automatically restart; investigate logs/schema.
Then open the public HTTPS URL:
- Participants: `/`
- Admin: `/dashboard/login.php`
Verify both session cookies have `Secure`, `HttpOnly`, and `SameSite=Lax` in browser
devtools. Verify `/includes/conn.php`, `/dashboard/tools/create-admin.php`,
`/dashboard/migrations/001_admin.sql`, and `/.env` are denied. Run a full test with
a disposable participant before accepting real participants.
## 5. Backup, redeploy, and recovery
Keep backups outside Git and the document root. Take regular backups and test
restoring them into an isolated stack. Pause user writes during backups involving
legacy non-InnoDB tables; `--single-transaction` only guarantees a consistent
snapshot for InnoDB. Include any active test data when planning maintenance.
```sh
umask 077
mkdir -p /srv/recruitment-private/backups
docker compose --env-file .env -p recruitment-hc exec -T db sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysqldump -u root --single-transaction --routines --triggers --no-tablespaces --ignore-table="$MYSQL_DATABASE.rec_bootstrap_state" "$MYSQL_DATABASE"' \
> "/srv/recruitment-private/backups/recruitment-$(date +%Y%m%d-%H%M%S).sql"
```
Check the command's exit status and backup contents before relying on the file.
SQL backups contain personal data and password hashes; restrict and protect them.
To restore into a new empty stack, set `DB_INIT_SQL_PATH` to the trusted backup
and deploy; initialization and migrations run automatically. Restoring over an
existing database requires a separate, explicitly approved recovery procedure;
normal startup never replaces its tables.
To update, back up, wait for Gitea Actions to publish the new image, then pull the
new commit in Komodo and redeploy with **Run build disabled** and **Auto pull
enabled**. The database volume is reused; successful imports are never repeated.
The migration service runs before the updated app starts. Rolling back an
image does not undo schema changes; use a tested database restore if necessary.
Image tags track compatible patch releases; consider pinning tested image digests
in production for reproducible upgrades.
Keep the Compose project name stable: it determines the volume names. Renaming the
Stack/project can create a new empty database instead of attaching the old volume.
**Never use `docker compose down -v` or delete `mysql_data` for a normal redeploy.**
The persistent `php_sessions` volume preserves sessions across app recreations;
run only one app instance with this file-based session setup.
MySQL image environment variables initialize users/passwords only on the first
start of an empty volume. Changing `.env` later does **not** rotate database
passwords: update the actual MySQL accounts and Compose environment together.
The app receives only its database-scoped user, never the root password.
## Manual Compose deployment (without Komodo)
From the repository root on the server:
```sh
cp .env.example .env
chmod 600 .env
# Edit .env: replace both passwords and set DB_INIT_SQL_PATH to your trusted dump.
# The SQL file must already exist on this Docker host.
# For a private image, first run: docker login gitea.cptckp.com
docker compose --env-file .env -p recruitment-hc config --quiet
docker compose --env-file .env -p recruitment-hc pull
docker compose --env-file .env -p recruitment-hc up -d --no-build
```
Setup is automatic. Inspect `db-init`/`migrate` logs, create an admin as needed
(section 2), then follow sections 3–5. Docker uses its own environment-backed
`includes/conn.php` baked into the image; it never reads or overwrites the host's
private `includes/conn.php`. Standalone PHP/local preview behavior is unchanged.