Files
Recruitment-HC/docs/deployment.md
T
NikoKS 930ab377ce
Build and publish Docker image / build (push) Successful in 9s
change desc
2026-10-15 08:16:42 +07:00

15 KiB
Raw Blame History

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 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 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, set DB_INIT_SQL_PATH to the tracked repository dump or a separately transferred private file, 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.

2. Automatic database initialization

The initial dump DB/recruitment_local_2026-10-14.sql is tracked in this repository. The recruitment-hc Komodo Stack and .env.example use a read-only mount from the repository checkout, so no separate upload is required:

DB_INIT_SQL_PATH=./DB/recruitment_local_2026-10-14.sql

The relative path resolves on the Docker host within the checked-out Compose project, not on your local computer. Ensure the deployed commit includes the dump. It contains personal data and existing accounts, not just schema: keep repository access restricted, review/sanitize production exports, and remove unwanted demo/test access before opening the site. The SQL is excluded from the app image.

To use a different trusted export containing in_daper, in_soal, and in_jawab, transfer it privately to a path outside the checkout/document root, for example /srv/recruitment-private/initial.sql, and change DB_INIT_SQL_PATH to that absolute path. It must exist on the Docker host, not merely in a container or on your computer.

The file is mounted read-only into db-init and must exist on every deployment. 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.

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:

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:

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:

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:

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

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.

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:

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.