added migration
Build and publish Docker image / build (push) Failing after 41s

This commit is contained in:
NikoKS committed 2026-10-15 00:05:11 +07:00
1 parent 125eab3cd4
commit 045d3a8fd3
8 files changed
+375 -65

No files matched your search

+5
View File
@@ -5,6 +5,11 @@ MYSQL_USER=recruitment
MYSQL_PASSWORD=CHANGE_ME_APP_PASSWORD
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
# Read-only SQL mount for automatic first-time setup; never included in the image.
# On a server, use an absolute private path OUTSIDE the checkout/document root.
# The file must exist on the Docker host, even when the database is initialized.
DB_INIT_SQL_PATH=./DB/recruitment_local_2026-10-14.sql
# 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
+1 -1
View File
@@ -10,7 +10,7 @@ Panduan akun, backup, dan runtime ada di [dev/README.md](dev/README.md). Hentika
## Server
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 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`. Set `DB_INIT_SQL_PATH` ke dump SQL tepercaya di host Docker; startup otomatis mengimpor hanya ke database kosong dan menjalankan migrasi sebelum aplikasi dimulai. Database yang sudah diinisialisasi tidak diimpor ulang.
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).
+39 -4
View File
@@ -2,14 +2,14 @@ name: ${COMPOSE_PROJECT_NAME:-recruitment-hc}
services:
app:
build:
build: &app-build
context: .
dockerfile: Dockerfile
restart: unless-stopped
init: true
ports:
- "${APP_BIND_IP:-127.0.0.1}:${APP_PORT:-8086}:80"
environment:
environment: &app-environment
MYSQL_HOST: db
MYSQL_PORT: "3306"
MYSQL_DATABASE: ${MYSQL_DATABASE:-recruitment}
@@ -18,8 +18,8 @@ services:
REC_PASS_SCORE: ${REC_PASS_SCORE:-300}
TZ: Asia/Jakarta
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
volumes:
- php_sessions:/var/lib/php/sessions
healthcheck:
@@ -62,6 +62,41 @@ services:
max-size: "10m"
max-file: "3"
# One-shot services: never run the destructive dump as an app entrypoint.
db-init:
image: mysql:8.4
restart: "no"
entrypoint: ["sh", "/bootstrap/init-db.sh"]
environment: *app-environment
depends_on:
db:
condition: service_healthy
volumes:
- type: bind
source: ./docker/init-db.sh
target: /bootstrap/init-db.sh
read_only: true
bind:
create_host_path: false
- type: bind
source: ${DB_INIT_SQL_PATH:-./DB/recruitment_local_2026-10-14.sql}
target: /bootstrap/initial.sql
read_only: true
bind:
create_host_path: false
migrate:
build: *app-build
restart: "no"
command: ["php", "/usr/local/share/recruitment/migrate.php"]
environment: *app-environment
depends_on:
db-init:
condition: service_completed_successfully
# This service exits after migration; it is not an Apache server.
healthcheck:
disable: true
phpmyadmin:
image: phpmyadmin:5.2-apache
restart: unless-stopped
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""Exercise destructive-import guards without connecting to any real database."""
import json
import os
import secrets
import subprocess
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).resolve().parents[1] / "docker/init-db.sh"
BASE = ["in_daper", "in_soal", "in_jawab"]
ALL = BASE + ["rec_admin", "rec_exam_attempt", "rec_exam_event", "rec_candidate_login_guard"]
FAKE_MYSQL = r'''#!/usr/bin/env python3
import json
import os
import re
import sys
from pathlib import Path
path = Path(os.environ["FAKE_MYSQL_STATE"])
state = json.loads(path.read_text())
args = sys.argv[1:]
query = args[args.index("-e") + 1] if "-e" in args else None
if query is None:
sys.stdin.read()
state["imports"] = state.get("imports", 0) + 1
state["tables"] += state.get("import_tables", [])
path.write_text(json.dumps(state))
if state.get("fail_import"):
print("Synthetic import failure", file=sys.stderr)
sys.exit(1)
elif "information_schema.tables" in query:
tables = state["tables"]
if "table_name = 'rec_bootstrap_state'" in query:
print(int("rec_bootstrap_state" in tables))
elif "table_name IN" in query:
names = re.findall(r"'([^']+)'", query.split("table_name IN", 1)[1])
print(sum(name in tables for name in names))
else:
print(len(tables))
elif query.startswith("SELECT status"):
print(state.get("status", ""))
elif query.startswith("CREATE TABLE rec_bootstrap_state"):
if "rec_bootstrap_state" in state["tables"]:
sys.exit(1)
state["tables"].append("rec_bootstrap_state")
state["status"] = "importing"
path.write_text(json.dumps(state))
elif query.startswith("UPDATE rec_bootstrap_state"):
state["status"] = "imported"
path.write_text(json.dumps(state))
else:
print("Unexpected query: " + query, file=sys.stderr)
sys.exit(1)
'''
class InitializationTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
client = self.root / "mysql"
client.write_text(FAKE_MYSQL)
client.chmod(0o700)
self.state_path = self.root / "state.json"
self.dump = self.root / "initial.sql"
self.dump.write_text("-- Synthetic SQL; never loads project personal data.\n")
self.env = dict(os.environ, PATH=f"{self.root}:{os.environ['PATH']}",
MYSQL_HOST="test-db", MYSQL_USER="test-user",
MYSQL_DATABASE="test-db", MYSQL_PASSWORD=secrets.token_hex(16),
FAKE_MYSQL_STATE=str(self.state_path), DB_INIT_SQL_FILE=str(self.dump))
def state(self):
try:
return json.loads(self.state_path.read_text())
except (OSError, ValueError) as error:
self.fail(f"Invalid mock database state: {error}")
def configure(self, tables=None, **options):
self.state_path.write_text(json.dumps(dict(tables=tables or [], import_tables=ALL, **options)))
def run_init(self):
return subprocess.run(["sh", str(SCRIPT)], env=self.env, capture_output=True, text=True)
def test_empty_database_imports_once(self):
self.configure()
first = self.run_init()
self.assertEqual(first.returncode, 0, first.stderr)
self.assertEqual(self.state()["status"], "imported")
second = self.run_init()
self.assertEqual(second.returncode, 0, second.stderr)
self.assertEqual(self.state()["imports"], 1)
def test_existing_complete_database_skips_import(self):
self.configure(ALL)
result = self.run_init()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("imports", self.state())
self.assertEqual(self.state()["tables"], ALL)
def test_partial_database_refuses_import(self):
self.configure(BASE)
self.assertNotEqual(self.run_init().returncode, 0)
self.assertNotIn("imports", self.state())
def test_unrelated_database_refuses_import(self):
self.configure(["unrelated_table"])
self.assertNotEqual(self.run_init().returncode, 0)
self.assertNotIn("imports", self.state())
def test_failed_import_blocks_automatic_retry(self):
self.configure(fail_import=True)
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["status"], "importing")
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["imports"], 1)
def test_missing_dump_leaves_database_untouched(self):
self.configure()
self.dump.unlink()
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["tables"], [])
def test_empty_dump_leaves_database_untouched(self):
self.configure()
self.dump.write_text("")
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["tables"], [])
def test_dump_cannot_replace_the_safety_marker(self):
self.configure()
self.dump.write_text("DROP TABLE IF EXISTS `REC_BOOTSTRAP_STATE`;\n")
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["tables"], [])
self.assertNotIn("imports", self.state())
def test_dump_missing_base_tables_is_not_marked_imported(self):
self.configure()
state = self.state()
state["import_tables"] = ["in_daper"]
self.state_path.write_text(json.dumps(state))
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["status"], "importing")
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["imports"], 1)
def test_imported_marker_with_missing_tables_is_refused(self):
self.configure(["rec_bootstrap_state", "in_daper"], status="imported")
self.assertNotEqual(self.run_init().returncode, 0)
self.assertNotIn("imports", self.state())
def test_marker_with_missing_status_is_refused(self):
self.configure(["rec_bootstrap_state"])
self.assertNotEqual(self.run_init().returncode, 0)
self.assertNotIn("imports", self.state())
def test_missing_password_is_refused_before_access(self):
self.configure()
del self.env["MYSQL_PASSWORD"]
self.assertNotEqual(self.run_init().returncode, 0)
self.assertEqual(self.state()["tables"], [])
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
#!/bin/sh
# Runs in the MySQL client container, never in the application's web root.
set -eu
: "${MYSQL_HOST:?Missing MYSQL_HOST}"
: "${MYSQL_DATABASE:?Missing MYSQL_DATABASE}"
: "${MYSQL_USER:?Missing MYSQL_USER}"
: "${MYSQL_PASSWORD:?Missing MYSQL_PASSWORD}"
export MYSQL_PWD="$MYSQL_PASSWORD"
mysql_run() {
mysql --protocol=TCP --host="$MYSQL_HOST" --port="${MYSQL_PORT:-3306}" \
--user="$MYSQL_USER" --database="$MYSQL_DATABASE" \
--batch --skip-column-names "$@"
}
fail() {
printf '%s\n' "Database initialization refused: $*" >&2
exit 1
}
require_tables() {
count=$(mysql_run -e "SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'
AND table_name IN ($1)")
[ "$count" = "$2" ] || fail "Required recruitment tables are missing. Review the database manually; no automatic reimport will be attempted."
}
marker=$(mysql_run -e "SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'rec_bootstrap_state'")
if [ "$marker" = 1 ]; then
status=$(mysql_run -e "SELECT status FROM rec_bootstrap_state WHERE id = 1")
[ "$status" = imported ] || fail "A previous import did not finish. SQL imports are not transactional; inspect or restore this database before retrying."
require_tables "'in_daper', 'in_soal', 'in_jawab'" 3
printf '%s\n' 'Initial SQL was already imported; preserving existing data.'
exit 0
fi
tables=$(mysql_run -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()")
if [ "$tables" != 0 ]; then
# Adopt a previously initialized deployment, but reject a bare/partial schema.
require_tables "'in_daper', 'in_soal', 'in_jawab', 'rec_admin', 'rec_exam_attempt', 'rec_exam_event', 'rec_candidate_login_guard'" 7
printf '%s\n' 'Existing recruitment database detected; skipping SQL import.'
exit 0
fi
sql_file=${DB_INIT_SQL_FILE:-/bootstrap/initial.sql}
[ -f "$sql_file" ] && [ -r "$sql_file" ] && [ -s "$sql_file" ] \
|| fail "The initial SQL file is missing, unreadable, or empty. Set DB_INIT_SQL_PATH to a trusted dump on the Docker host."
# The dump must not replace our safety marker with a previously imported one.
# Exclude this setup-only table when creating backups (see deployment guide).
if grep -iFq 'rec_bootstrap_state' "$sql_file"; then
fail "The dump references the reserved rec_bootstrap_state table. Re-export without that table before importing."
fi
# Persist the in-progress marker BEFORE any DROP/CREATE statements in the dump.
# Do not use IF NOT EXISTS: concurrent first imports must not both proceed.
mysql_run -e "CREATE TABLE rec_bootstrap_state (
id TINYINT UNSIGNED NOT NULL PRIMARY KEY,
status VARCHAR(20) NOT NULL
) ENGINE=InnoDB;
INSERT INTO rec_bootstrap_state (id, status) VALUES (1, 'importing');"
printf '%s\n' 'Importing initial SQL into the empty database...'
mysql_run < "$sql_file"
require_tables "'in_daper', 'in_soal', 'in_jawab'" 3
mysql_run -e "UPDATE rec_bootstrap_state SET status = 'imported' WHERE id = 1"
printf '%s\n' 'Initial SQL import complete.'
+6 -1
View File
@@ -26,8 +26,13 @@ try {
} while ($conn->more_results() && $conn->next_result());
fwrite(STDOUT, "Applied: $file\n");
}
// Fail the one-shot service before starting Apache if setup is incomplete.
foreach (['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');
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");
fwrite(STDERR, 'Migration failed: ' . $error->getMessage() . "\nReview setup logs and docs/deployment.md before retrying.\n");
exit(1);
}
+77 -54
View File
@@ -3,7 +3,8 @@
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.
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
@@ -32,9 +33,10 @@ 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.
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
@@ -42,52 +44,70 @@ 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
```
## 2. Automatic database initialization
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.
`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`):
**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"'
```dotenv
DB_INIT_SQL_PATH=/srv/recruitment-private/initial.sql
```
If it is empty, import and apply the migrations:
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 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
docker compose --env-file .env -p recruitment-hc ps -a
docker compose --env-file .env -p recruitment-hc logs db-init migrate
```
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.
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**:
@@ -169,17 +189,17 @@ 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.
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
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 phpmyadmin
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
@@ -211,19 +231,20 @@ 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 "$MYSQL_DATABASE"' \
'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.
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 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; imports are never repeated automatically.
Apply migration changes explicitly when a release requires them. Rolling back an
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.
@@ -246,11 +267,13 @@ From the repository root on the server:
```sh
cp .env.example .env
chmod 600 .env
# Edit .env and replace both password placeholders before proceeding.
# 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
```
Then follow sections 2–5. Docker uses its own environment-backed
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.
+9 -5
View File
@@ -8,8 +8,8 @@ architecture** (typically `linux/amd64`) and publishes `:latest` to:
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`.
Only the PHP/Apache image is built (used by both `app` and `migrate`); MySQL,
`db-init`, and phpMyAdmin retain their official images. Database exports and host credentials are excluded by `.dockerignore`.
## Setup
@@ -69,6 +69,8 @@ Stack run directory, for example `compose.image.yaml`:
services:
app:
image: gitea.cptckp.com/nurinda/recruitment-hc:latest
migrate:
image: gitea.cptckp.com/nurinda/recruitment-hc:latest
```
Prefer replacing `:latest` with the tested image digest (`@sha256:...` instead
@@ -81,10 +83,12 @@ file paths, in that order. Then:
- 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).
- Use the same tested image/digest for `app` and `migrate`.
- Keep the Compose project name and volumes unchanged. Set `DB_INIT_SQL_PATH` to
a trusted dump on the Docker host. Initialization/migrations run automatically;
follow [the deployment guide](deployment.md).
Compose retains the base file's `build` declaration in this simple override. Use
Compose retains both services' base `build` declarations 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.