From 125eab3cd490a819dc4fb0005e451ce50ab08a92 Mon Sep 17 00:00:00 2001 From: NikoKS Date: Wed, 14 Oct 2026 19:08:59 +0700 Subject: [PATCH] added deployment file --- .dockerignore | 25 +++ .env.example | 20 +++ .gitea/workflows/build-image.yaml | 40 +++++ Dockerfile | 25 +++ README.md | 4 +- compose.yaml | 99 ++++++++++++ docker/apache.conf | 42 +++++ docker/conn.php | 16 ++ docker/health.php | 17 ++ docker/healthcheck.php | 11 ++ docker/migrate.php | 33 ++++ docker/php.ini | 16 ++ docs/deployment.md | 256 ++++++++++++++++++++++++++++++ docs/image-build.md | 101 ++++++++++++ 14 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/build-image.yaml create mode 100644 Dockerfile create mode 100644 compose.yaml create mode 100644 docker/apache.conf create mode 100644 docker/conn.php create mode 100644 docker/health.php create mode 100644 docker/healthcheck.php create mode 100644 docker/migrate.php create mode 100644 docker/php.ini create mode 100644 docs/deployment.md create mode 100644 docs/image-build.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c2e8219 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +.git +.gitignore +.agents +.aws +.codex +.local +.well-known +DB +dev +scan +node_modules +.env +.env.* +*.sql +!dashboard/migrations/*.sql +*.sql.gz +*.sql.zip +*.log +*.zip +*.pem +*.key +*.crt +includes/conn.php +koneksi.php +**/__pycache__ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9b6928f --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Copy to .env for manual Compose use, or paste into Komodo Stack Environment. +# Never commit actual passwords. Generate each independently: openssl rand -hex 32 +MYSQL_DATABASE=recruitment +MYSQL_USER=recruitment +MYSQL_PASSWORD=CHANGE_ME_APP_PASSWORD +MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD + +# HTTPS must be provided by your existing reverse proxy. +# Localhost is safe for a proxy running directly on the Docker host. +APP_BIND_IP=127.0.0.1 +APP_PORT=8086 +REC_PASS_SCORE=300 + +# phpMyAdmin: use an SSH tunnel or a restricted HTTPS reverse proxy. +# Log in manually with MYSQL_USER / MYSQL_PASSWORD (no automatic login). +PHPMYADMIN_BIND_IP=127.0.0.1 +PHPMYADMIN_PORT=8087 +PHPMYADMIN_UPLOAD_LIMIT=64M +# Set only for proxy access, e.g. https://db-admin.example.com/ +PMA_ABSOLUTE_URI= diff --git a/.gitea/workflows/build-image.yaml b/.gitea/workflows/build-image.yaml new file mode 100644 index 0000000..61dc506 --- /dev/null +++ b/.gitea/workflows/build-image.yaml @@ -0,0 +1,40 @@ +# Explicit URLs and commit pins keep action sources predictable on Gitea. +# yamllint disable rule:line-length +name: Build and publish Docker image + +'on': + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + +env: + IMAGE: gitea.cptckp.com/nurinda/recruitment-hc + +jobs: + build: + if: gitea.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - name: Checkout + uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Log in to Gitea Container Registry + uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: gitea.cptckp.com + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and publish latest + shell: bash + run: | + set -euo pipefail + docker build --tag "$IMAGE:latest" . + docker push "$IMAGE:latest" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..13401e8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM php:8.3-apache-bookworm + +RUN docker-php-ext-install mysqli \ + && a2enmod rewrite headers setenvif \ + && install -d -o www-data -g www-data -m 0700 /var/lib/php/sessions + +WORKDIR /var/www/html + +# Deliberately do not copy SQL exports, local tooling, or host credentials. +COPY *.php favicon.ico ./ +COPY assets/ ./assets/ +COPY includes/ ./includes/ +COPY dashboard/ ./dashboard/ +COPY docker/conn.php ./includes/conn.php +COPY docker/apache.conf /etc/apache2/conf-available/recruitment.conf +COPY docker/php.ini /usr/local/etc/php/conf.d/recruitment.ini +COPY docker/health.php /usr/local/share/recruitment/health.php +COPY docker/migrate.php /usr/local/share/recruitment/migrate.php +COPY docker/healthcheck.php /usr/local/bin/recruitment-healthcheck.php + +RUN a2enconf recruitment \ + && find /var/www/html -type d -exec chmod 0755 {} + \ + && find /var/www/html -type f -exec chmod 0644 {} + + +EXPOSE 80 diff --git a/README.md b/README.md index e6664a5..be7f7a0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ Panduan akun, backup, dan runtime ada di [dev/README.md](dev/README.md). Hentika ## Server -Salin `includes/conn.example.php` ke `includes/conn.php`, lalu isi konfigurasi database. Impor database recruitment dan jalankan migrasi yang diperlukan di `dashboard/migrations/`. Panduan admin: [dashboard/README.md](dashboard/README.md). +Untuk deployment Docker Compose melalui Komodo (PHP/Apache + MySQL persisten, di belakang reverse proxy HTTPS), ikuti [panduan deployment](docs/deployment.md). Konfigurasi tersedia di `compose.yaml`, `Dockerfile`, dan `.env.example`; dump SQL diimpor secara eksplisit, bukan setiap redeploy. + +Untuk deployment PHP tanpa Docker, salin `includes/conn.example.php` ke `includes/conn.php`, lalu isi konfigurasi database. Impor database recruitment dan jalankan migrasi yang diperlukan di `dashboard/migrations/`. Panduan admin: [dashboard/README.md](dashboard/README.md). - `assets/`: tampilan dan perilaku tes peserta. - `includes/`: koneksi, autentikasi peserta, dan layanan sesi tes. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f5a8146 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,99 @@ +name: ${COMPOSE_PROJECT_NAME:-recruitment-hc} + +services: + app: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + init: true + ports: + - "${APP_BIND_IP:-127.0.0.1}:${APP_PORT:-8086}:80" + environment: + MYSQL_HOST: db + MYSQL_PORT: "3306" + MYSQL_DATABASE: ${MYSQL_DATABASE:-recruitment} + MYSQL_USER: ${MYSQL_USER:-recruitment} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Set MYSQL_PASSWORD} + REC_PASS_SCORE: ${REC_PASS_SCORE:-300} + TZ: Asia/Jakarta + depends_on: + db: + condition: service_healthy + volumes: + - php_sessions:/var/lib/php/sessions + healthcheck: + test: ["CMD", "php", "/usr/local/bin/recruitment-healthcheck.php"] + interval: 30s + timeout: 5s + start_period: 30s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + db: + image: mysql:8.4 + restart: unless-stopped + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE:-recruitment} + MYSQL_USER: ${MYSQL_USER:-recruitment} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Set MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD} + TZ: Asia/Jakarta + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: + - CMD-SHELL + - >- + MYSQL_PWD="$${MYSQL_ROOT_PASSWORD}" + mysql --protocol=TCP -h 127.0.0.1 -u root + -Nse "SELECT 1" >/dev/null 2>&1 + interval: 10s + timeout: 5s + start_period: 60s + retries: 12 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + phpmyadmin: + image: phpmyadmin:5.2-apache + restart: unless-stopped + ports: + - "${PHPMYADMIN_BIND_IP:-127.0.0.1}:${PHPMYADMIN_PORT:-8087}:80" + environment: + PMA_HOST: db + PMA_PORT: "3306" + PMA_ABSOLUTE_URI: ${PMA_ABSOLUTE_URI:-} + UPLOAD_LIMIT: ${PHPMYADMIN_UPLOAD_LIMIT:-64M} + HIDE_PHP_VERSION: "true" + TZ: Asia/Jakarta + depends_on: + db: + condition: service_healthy + volumes: + - phpmyadmin_sessions:/sessions + healthcheck: + test: + - CMD-SHELL + - curl --fail --silent --max-time 3 http://127.0.0.1/ > /dev/null + interval: 30s + timeout: 5s + start_period: 30s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + mysql_data: + php_sessions: + phpmyadmin_sessions: diff --git a/docker/apache.conf b/docker/apache.conf new file mode 100644 index 0000000..45ce903 --- /dev/null +++ b/docker/apache.conf @@ -0,0 +1,42 @@ +ServerName localhost +ServerTokens Prod +ServerSignature Off +TraceEnable Off + +# Only expose this HTTP listener to a trusted reverse proxy. The proxy must +# overwrite X-Forwarded-Proto, not pass a client-supplied value through. +# PHP uses HTTPS to decide whether participant/admin cookies need Secure. +SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on + + + Options -Indexes -MultiViews + AllowOverride None + DirectoryIndex index.php + Require all granted + + +# Internal code is still usable by PHP/CLI, never directly over HTTP. + + Require all denied + + +# Also deny dotfiles and backup/config files if accidentally added later. + + Require all denied + + + Require all denied + + +Alias /healthz /usr/local/share/recruitment/health.php + + Options -Indexes + AllowOverride None + Require all denied + + Require all granted + + + +Header always set X-Content-Type-Options "nosniff" +Header always set Referrer-Policy "same-origin" diff --git a/docker/conn.php b/docker/conn.php new file mode 100644 index 0000000..17c8f3f --- /dev/null +++ b/docker/conn.php @@ -0,0 +1,16 @@ +set_charset('utf8mb4'); diff --git a/docker/health.php b/docker/health.php new file mode 100644 index 0000000..1baa783 --- /dev/null +++ b/docker/health.php @@ -0,0 +1,17 @@ +query("SELECT 1 FROM `$table` LIMIT 0"); + } + $conn->query('SELECT email, nama_de, nama_be, idkar, password_hash FROM rec_admin LIMIT 0'); + echo "ok\n"; +} catch (Throwable $error) { + http_response_code(503); + // Never expose connection details, credentials, or schema errors over HTTP. + echo "not ready\n"; +} diff --git a/docker/healthcheck.php b/docker/healthcheck.php new file mode 100644 index 0000000..5580d11 --- /dev/null +++ b/docker/healthcheck.php @@ -0,0 +1,11 @@ +query("SELECT 1 FROM `$table` LIMIT 0"); + } + + foreach (['001_admin.sql', '002_admin_identity.sql', '003_exam_attempts.sql'] as $file) { + // Only migrate the historical rec_admin schema, preserving identities. + if ($file === '002_admin_identity.sql' + && $conn->query("SHOW COLUMNS FROM rec_admin LIKE 'name'")->num_rows === 0) { + continue; + } + $sql = file_get_contents('/var/www/html/dashboard/migrations/' . $file); + $conn->multi_query($sql); + do { + $result = $conn->store_result(); + if ($result instanceof mysqli_result) { + $result->free(); + } + } while ($conn->more_results() && $conn->next_result()); + fwrite(STDOUT, "Applied: $file\n"); + } + fwrite(STDOUT, "Database ready. Existing accounts and test data were preserved.\n"); +} catch (Throwable $error) { + fwrite(STDERR, 'Migration failed: ' . $error->getMessage() . "\nImport the base database first; see docs/deployment.md.\n"); + exit(1); +} diff --git a/docker/php.ini b/docker/php.ini new file mode 100644 index 0000000..8266895 --- /dev/null +++ b/docker/php.ini @@ -0,0 +1,16 @@ +expose_php = Off +display_errors = Off +display_startup_errors = Off +log_errors = On +error_log = /proc/self/fd/2 +error_reporting = E_ALL +date.timezone = Asia/Jakarta + +session.use_strict_mode = 1 +session.use_only_cookies = 1 +session.cookie_httponly = 1 +session.cookie_samesite = Lax +session.save_path = /var/lib/php/sessions + +; Secure cookies are set by the application when Apache receives the HTTPS +; indication from the trusted proxy (see apache.conf). diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..37c717c --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,256 @@ +# 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. diff --git a/docs/image-build.md b/docs/image-build.md new file mode 100644 index 0000000..afa7873 --- /dev/null +++ b/docs/image-build.md @@ -0,0 +1,101 @@ +# Gitea Actions image build + +Gitea discovers [`.gitea/workflows/build-image.yaml`](../.gitea/workflows/build-image.yaml). +The workflow builds the root `Dockerfile` for the **Docker daemon's native +architecture** (typically `linux/amd64`) and publishes `:latest` to: + +```text +gitea.cptckp.com/nurinda/recruitment-hc +``` + +Only the PHP/Apache app is built; MySQL and phpMyAdmin retain their official +images. Database exports and host credentials are excluded by `.dockerignore`. + +## Setup + +1. Enable **Actions** for the repository and ensure Gitea's package/container + registry is enabled. +2. Register an available runner with the **`ubuntu-latest`** label. Its job + environment needs Bash, Git, Node 20 support for these actions, Docker CLI, + and access to a Docker daemon with build support. No QEMU or separate Buildx + setup is required. Use an appropriately configured runner image or host-mode + runner; a bare Ubuntu job image without these tools is insufficient. + The runner must reach GitHub (pinned action downloads), Docker Hub (base images), + and `gitea.cptckp.com` over trusted HTTPS. +3. In **Repository Settings → Actions → Secrets**, add: + + | Secret | Value | + | --- | --- | + | `REGISTRY_USERNAME` | Gitea username allowed to publish packages under `Nurinda` | + | `REGISTRY_TOKEN` | That user's personal access token with **Packages: Read & Write** | + + Use a dedicated publishing account if possible. Its account/token must have + access to the organization and its packages. The built-in `GITEA_TOKEN` is + not used for registry publishing. Never put credentials in the workflow. +4. Commit and push these files to `main`, or run **Build and publish Docker + image** manually from the Actions tab. + +Docker daemon access is highly privileged. Use a dedicated CI runner, not your +production Docker host. Only trusted changes merged/pushed to `main` are built; +pull requests do not trigger this workflow. + +## Triggers and tags + +| Event | Behavior | +| --- | --- | +| Push to `main` | Build and push `:latest` | +| Manual run on `main` | Build and push `:latest` | +| Manual run on another branch or tag | Job skipped | +| Pull request or tag push | No workflow run | + +The workflow has three steps: checkout, registry login, and `docker build` / +`docker push`. It does not publish version/commit tags or multi-platform images. +Use a runner with the same architecture as your deployment server. + +`latest` is mutable, and rebuilds can differ when upstream base images change. +Record the pushed image digest and deploy by digest for reproducibility/rollback. +Third-party actions are pinned to commit hashes with explicit GitHub URLs; review +and update those pins when upgrading actions. + +## Use the published image with Komodo + +The workflow only builds/publishes; it does **not** redeploy Komodo. The existing +`compose.yaml` still builds locally and remains usable without a registry. + +To deploy the CI image instead, add a small override file in your server-side +Stack run directory, for example `compose.image.yaml`: + +```yaml +services: + app: + image: gitea.cptckp.com/nurinda/recruitment-hc:latest +``` + +Prefer replacing `:latest` with the tested image digest (`@sha256:...` instead +of `:latest`). +Include both `compose.yaml` and `compose.image.yaml` in the Komodo Stack's Compose +file paths, in that order. Then: + +- Disable **Run build** so Komodo does not build/tag a local image over the CI image. +- Enable **Auto pull** so the image is pulled before deployment. +- Configure Komodo's Registry provider/account for `gitea.cptckp.com`; for private + packages, use a token with **Packages: Read** on an account allowed to pull them. + Keep this separate from the CI write token. +- Keep the Compose project name and volumes unchanged. Imports/migrations remain + explicit; follow [the deployment guide](deployment.md). + +Compose retains the base file's `build` declaration in this simple override. Use +`docker compose ... up -d --no-build` for manual deployments of the CI image +(after registry login and `docker compose ... pull`); do **not** use `--build`. +Komodo's Auto pull supplies the image before `up`, with Run build disabled. + +For example, after configuring registry authentication on the server: + +```sh +docker pull gitea.cptckp.com/nurinda/recruitment-hc:latest +``` + +Verify a successful workflow run and available package before deploying it. + +References: [Gitea Actions compatibility](https://docs.gitea.com/usage/actions/comparison/) +and [Gitea Container Registry](https://docs.gitea.com/usage/packages/container/).