280 lines
14 KiB
Markdown
280 lines
14 KiB
Markdown
# Server deployment: Komodo + Docker Compose
|
||
|
||
This stack runs PHP 8.3/Apache, MySQL 8.4 LTS, and phpMyAdmin 5.2. No Node build,
|
||
registry, or separate Komodo Build resource is required. The app image is built
|
||
on your Docker host. 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.
|
||
|
||
To build/publish the app in Gitea Actions instead of on the server, see
|
||
[the image build guide](image-build.md), including registry secrets and Komodo
|
||
settings for pulling the published image. Local builds remain the default below.
|
||
|
||
## 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`) | **Enabled** — rebuilds PHP code on redeploy |
|
||
| Auto pull (`auto_pull`) | **Disabled** — app is built locally |
|
||
| 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.
|
||
|
||
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/build/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, pull the new commit in Komodo and redeploy with **Run build**
|
||
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.
|
||
docker compose --env-file .env -p recruitment-hc config --quiet
|
||
docker compose --env-file .env -p recruitment-hc up -d --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.
|