# 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. 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. Deploy the Stack. **The app may initially be unhealthy** until you explicitly import the base database and run migrations below. Do not enable a deploy `--wait` flag on this first launch: it cannot become ready before that setup. 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. Import the database once On the Docker server, change to the Stack's checked-out **run directory**. The commands below assume the project name `recruitment-hc` and Komodo's default `.env` file; if you choose another project name/env path, adjust **every** command accordingly. Never run commands against a different project by accident. Wait for `db` to be healthy: ```sh docker compose --env-file .env -p recruitment-hc ps ``` Obtain a trusted recruitment SQL 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`, with restricted permissions. The repository currently also tracks an export under `DB/`; it may contain personal data and existing accounts. Keep the repository private, review/sanitize any export used in production, and remove unwanted demo/test access before opening the site. Neither `DB/` nor any export is included in the image or auto-imported. **Only import into a new, empty database.** Dumps can contain `DROP TABLE` and existing accounts/passwords. Check first; this should return **no table names**: ```sh docker compose --env-file .env -p recruitment-hc exec -T db sh -c \ 'MYSQL_PWD="$MYSQL_PASSWORD" mysql -u "$MYSQL_USER" "$MYSQL_DATABASE" -e "SHOW TABLES"' ``` If it is empty, import and apply the migrations: ```sh docker compose --env-file .env -p recruitment-hc exec -T db sh -c \ 'MYSQL_PWD="$MYSQL_PASSWORD" mysql -u "$MYSQL_USER" "$MYSQL_DATABASE"' \ < /srv/recruitment-private/initial.sql docker compose --env-file .env -p recruitment-hc exec -T app \ php /usr/local/share/recruitment/migrate.php ``` The migration command requires the three base tables, creates the admin and exam protection tables, and handles the historical admin identity schema when needed. It can be rerun without resetting users or test progress. Migrations are explicit, not run automatically on every container restart. Back up before applying them to an existing database. Future migrations must be added to the migration runner as part of their release. 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 imported using the CLI command in section 2. Browser imports can also overwrite data: only import into a verified empty database, then run the migration command. No database initialization is automated. ## 4. Verify ```sh docker compose --env-file .env -p recruitment-hc ps 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 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 "$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. Restore using the import command in section 2 **only after selecting a new empty database or explicitly approving replacement of the target data**; migrate after restoring if needed. To update, back up, pull the new commit in Komodo and redeploy with **Run build** enabled. The database volume is reused; imports are never repeated automatically. Apply migration changes explicitly when a release requires them. 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 and replace both password placeholders before proceeding. docker compose --env-file .env -p recruitment-hc config --quiet docker compose --env-file .env -p recruitment-hc up -d --build ``` Then follow sections 2–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.