added deployment file

This commit is contained in:
NikoKS committed 2026-10-14 19:08:59 +07:00
1 parent 061d5fd8a4
commit 125eab3cd4
14 files changed
+704 -1

No files matched your search

+25
View File
@@ -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__
+20
View File
@@ -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=
+40
View File
@@ -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"
+25
View File
@@ -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
+3 -1
View File
@@ -10,7 +10,9 @@ Panduan akun, backup, dan runtime ada di [dev/README.md](dev/README.md). Hentika
## Server ## 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. - `assets/`: tampilan dan perilaku tes peserta.
- `includes/`: koneksi, autentikasi peserta, dan layanan sesi tes. - `includes/`: koneksi, autentikasi peserta, dan layanan sesi tes.
+99
View File
@@ -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:
+42
View File
@@ -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
<Directory /var/www/html>
Options -Indexes -MultiViews
AllowOverride None
DirectoryIndex index.php
Require all granted
</Directory>
# Internal code is still usable by PHP/CLI, never directly over HTTP.
<DirectoryMatch "^/var/www/html/(?:includes|DB|dev|docker|dashboard/(?:tools|migrations))(?:/|$)">
Require all denied
</DirectoryMatch>
# Also deny dotfiles and backup/config files if accidentally added later.
<LocationMatch "(?:^|/)\.">
Require all denied
</LocationMatch>
<FilesMatch "(?i)\.(?:sql(?:\.gz|\.zip)?|ini|env|toml|ya?ml|log|bak)$">
Require all denied
</FilesMatch>
Alias /healthz /usr/local/share/recruitment/health.php
<Directory /usr/local/share/recruitment>
Options -Indexes
AllowOverride None
Require all denied
<Files health.php>
Require all granted
</Files>
</Directory>
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "same-origin"
+16
View File
@@ -0,0 +1,16 @@
<?php
// Docker-only replacement for includes/conn.php; local/server config is untouched.
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
foreach (['MYSQL_HOST', 'MYSQL_DATABASE', 'MYSQL_USER', 'MYSQL_PASSWORD'] as $key) {
if (getenv($key) === false || getenv($key) === '') {
throw new RuntimeException('Missing database environment variable: ' . $key);
}
}
$conn = new mysqli(
getenv('MYSQL_HOST'),
getenv('MYSQL_USER'),
getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
(int) (getenv('MYSQL_PORT') ?: 3306)
);
$conn->set_charset('utf8mb4');
+17
View File
@@ -0,0 +1,17 @@
<?php
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
try {
require '/var/www/html/includes/conn.php';
// A database connection alone is insufficient: the initial import and
// migrations must also be present before this instance is ready for users.
foreach (['in_daper', 'in_soal', 'in_jawab', 'rec_admin', 'rec_exam_attempt', 'rec_exam_event', 'rec_candidate_login_guard'] as $table) {
$conn->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";
}
+11
View File
@@ -0,0 +1,11 @@
<?php
// Check Apache + PHP + database readiness without requiring curl in the image.
$socket = @fsockopen('127.0.0.1', 80, $errno, $error, 3);
if (!$socket) {
exit(1);
}
stream_set_timeout($socket, 3);
fwrite($socket, "GET /healthz HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n");
$status = fgets($socket);
fclose($socket);
exit(is_string($status) && preg_match('/^HTTP\/\d\.\d 200\b/', $status) ? 0 : 1);
+33
View File
@@ -0,0 +1,33 @@
<?php
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
try {
require '/var/www/html/includes/conn.php';
foreach (['in_daper', 'in_soal', 'in_jawab'] as $table) {
$conn->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);
}
+16
View File
@@ -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).
+256
View File
@@ -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.
+101
View File
@@ -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/).