- TypeScript 89.5%
- Shell 10.2%
- Makefile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Emailed submissions@start9.com on 2026-09-11 with a link to this repo at 2.66.0:2, disclosing the three known gaps: SMTP unexercised against a real provider, translations unreviewed, and aarch64 declared but never built. Notes what happens next, in particular that the Start9-Community fork becomes the upstream and packageRepo will need updating again. Claude-Session: https://claude.ai/code/session_01YGYGTA2MvmXxbguz9HbqaW Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| .github/workflows | ||
| assets | ||
| scripts | ||
| startos | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| icon.png | ||
| instructions.md | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| TODO.md | ||
| tsconfig.json | ||
| UPDATING.md | ||
Kimai on StartOS
Upstream docs: https://www.kimai.org/documentation/
Everything not listed in this document should behave the same as upstream Kimai. If a feature, setting, or behavior is not mentioned here, the upstream documentation is accurate and fully applicable.
Kimai is a self-hosted time-tracking application for freelancers, agencies, and companies. This package runs the official Apache image alongside a MySQL sidecar, provisions the initial super-admin, and wires Kimai's mailer into StartOS's SMTP settings.
Table of Contents
- Image and Container Runtime
- Volume and Data Layout
- Installation and First-Run Flow
- Configuration Management
- Network Access and Interfaces
- Actions (StartOS UI)
- Backups and Restore
- Health Checks
- Dependencies
- Limitations and Differences
- What Is Unchanged from Upstream
- Contributing
- Quick Reference for AI Consumers
Image and Container Runtime
| Kimai image | Upstream kimai/kimai2, unmodified |
| MySQL image | Upstream mysql, unmodified |
| Architectures | x86_64, aarch64 |
| Entrypoint | Both images use sdk.useEntrypoint() |
The Kimai image is published in two flavors under different tags. This package pins the Apache flavor, which bundles its own web server listening on port 8001. The latest/fpm tags are PHP-FPM only and would require an additional nginx sidecar; they are deliberately not used.
Kimai's entrypoint waits for the database, runs kimai:install (schema creation and migrations, idempotent), then execs Apache in the foreground. That behavior is unchanged — this package only supplies its environment.
Volume and Data Layout
| Volume | Mount point | Contents |
|---|---|---|
main |
/opt/kimai/var |
Invoices, exports, invoice/export templates, plugins, the generated app secret, the admin-credentials marker, logs |
mysql |
/var/lib/mysql |
MySQL data directory |
startos |
— | store.json: generated secrets and SMTP settings |
The upstream Docker Compose example mounts only var/data and var/plugins. This package mounts the whole /opt/kimai/var directory instead, which is what the image itself declares as a VOLUME. The narrower mount silently discards generated invoices, exports, and custom templates on restart.
Both subcontainers share a network namespace, so Kimai reaches MySQL over 127.0.0.1.
Installation and First-Run Flow
- On install, the package generates a MySQL root password and Symfony
APP_SECRETintostore.json. - A critical task is raised pointing at the Set Admin Password action. Kimai ships with no accounts, so this must be run before anyone can sign in.
- On first start, MySQL initializes its data directory and Kimai's entrypoint builds the schema. This is the slow part — several minutes is normal.
- Once Kimai is healthy, an
apply-admin-credentialsoneshot runskimai:user:create, falling back tokimai:user:password, to provision or update theadminsuper-admin. It runs only when the credentials have changed since the last successful apply.
Upstream's ADMINPASS/ADMINMAIL variables are deliberately not used. They only feed kimai:user:create, which fails once the account exists — enough to set a password, never to rotate one. The oneshot covers both cases with a single code path.
The oneshot is guarded rather than unconditional. It hashes the credentials it is about to apply and records that hash in var/data/.startos-admin-applied; on a later start where the hash still matches, it exits without touching Kimai. Applying on every start would be destructive whenever the database already holds an admin this package did not create — after importing another instance's data, or restoring a backup taken elsewhere — because it would silently reset that account's password on the next restart. The marker lives on the main volume, so a backup and its restore carry it alongside the store.json that holds the password, and the two stay in agreement.
If the admin account is ever deleted from inside Kimai, run Set Admin Password again: a new password produces a new hash, which makes the oneshot re-create the account on the next start.
Installs upgrading from 2.66.0:1 or earlier have no marker yet, so the first start after the upgrade applies the stored password once more and writes the marker. Every start after that leaves the account alone. This is deliberate: seeding the marker from a version migration would mean writing a credential hash from a runtime this package cannot test against, and a migration that throws blocks the upgrade outright.
The admin account is created with the non-routable address admin@kimai.local. Kimai requires an email-shaped value; nothing is ever sent to it. Change it inside Kimai if you want password-reset emails to reach you.
Configuration Management
| StartOS-Managed | Upstream-Managed |
|---|---|
DATABASE_URL, APP_SECRET, APP_ENV, TRUSTED_PROXIES, MAILER_URL, MAILER_FROM |
Everything inside Kimai's own admin UI: users, teams, customers, projects, activities, rates, invoice and export templates, plugins |
TRUSTED_PROXIES is set to the private address ranges. StartOS terminates TLS at the edge and forwards plain HTTP with X-Forwarded-Proto: https; Symfony ignores those headers unless the sender is a trusted proxy, and if it ignores them Kimai emits absolute http:// URLs that the browser blocks as mixed content.
TRUSTED_HOSTS is intentionally left unset (Symfony's default: allow any host). StartOS serves each service on several addresses at once — .local, LAN IP, and any address the user adds — and pinning a single hostname would break every other one.
Network Access and Interfaces
| Interface | Internal port | Protocol | Purpose |
|---|---|---|---|
Web Interface (ui) |
8001 | HTTP | Kimai's web UI and its REST API (under /api) |
The interface is bound with protocol: 'http', so StartOS adds X-Forwarded-* headers and terminates TLS itself. Which addresses it is reachable on is the user's choice, made from the service's Interfaces tab.
Actions (StartOS UI)
Set Admin Password (set-admin-password)
- Purpose: Generates a random 22-character password for the
adminaccount, stores it, and returns it once. Covers both the initial credential and later rotation. - Visibility: Enabled
- Availability: Any status
- Inputs: None
- Outputs: Username and password (password masked and copyable)
Writing the password to store.json re-runs setupMain, which restarts the daemons and re-runs the oneshot that pushes the new password into Kimai.
Configure SMTP (configure-smtp)
- Purpose: Sets Kimai's mailer. Supports disabled, StartOS system SMTP, and custom providers.
- Visibility: Enabled
- Availability: Any status
- Inputs: The SDK's standard SMTP input spec
- Outputs: None
The stored selection is rendered into a Symfony Mailer DSN for MAILER_URL — smtps:// for implicit TLS, smtp:// for STARTTLS, null://null when disabled. Credentials are percent-encoded.
Backups and Restore
The database is backed up as a logical dump (sdk.Backups.withMysqlDump), not as a copy of the MySQL data directory — a raw datadir is only restorable by the exact server version that wrote it, while a dump survives an upstream MySQL bump.
Also included:
main— invoices, exports, custom templates, pluginsstartos—store.json, which holds the database password the restore needs to load the dump back in
The mysql volume itself is not copied; it is rebuilt from the dump on restore.
The restored datadir has a different account layout
A restore does not produce the same MySQL state as a fresh install, and the difference is load-bearing.
On a fresh install the image's entrypoint initializes the datadir and creates root@%, honouring MYSQL_ROOT_HOST (default %). On a restore, sdk.Backups.withMysqlDump builds the datadir itself and loads the dump into it, so the entrypoint finds a populated datadir and skips user setup entirely. The only account that exists is root@localhost, which is reachable over the unix socket but not over TCP.
Kimai connects over TCP to 127.0.0.1. Left alone, a restore therefore comes up with the data fully intact and the service permanently stuck, failing with ERROR 1130 (HY000): Host '127.0.0.1' is not allowed to connect to this MySQL server — visible only by attaching to the container.
Two things in main.ts handle this:
- The
mysqldaemon's readiness check connects over the socket. A TCP check could never pass on a restored datadir, and sincekimaiis gated on that check, the whole service would hang. - The
ensure-db-accessoneshot runs after the database is ready and before Kimai starts, creatingroot@%if it is missing. It is a no-op on a fresh install and a repair on a restored one, and thekimaidaemon lists it inrequires.
This was found by testing an actual restore, and the fix was verified by a second one; neither is theoretical. The underlying asymmetry arguably belongs in the SDK, but the package cannot depend on that.
Important
A restore reinstalls the package version recorded in the backup, not the version currently installed. Testing any packaging fix through a restore therefore requires a backup taken after that fix is installed — restoring an older backup replays the older code and reproduces the old behaviour.
Health Checks
| Check | Displayed as | Behavior |
|---|---|---|
mysql daemon |
Database | Runs SELECT 1 over the unix socket, not TCP — see Backups and Restore for why. Reports loading while initializing, distinguishing a brand-new datadir from a restart. |
kimai daemon |
Web Interface | checkPortListening on the UI port, with a 5-minute grace period. Apache only binds after migrations finish, so a shorter grace period would flash red during legitimate work. |
email |
Reports disabled with a pointer to the Configure SMTP action when no mailer is set, success otherwise. |
Dependencies
None.
Limitations and Differences
DEFAULT_URIcannot be set. The image's Apache vhost passes a fixed list of environment variables through to PHP (.docker/000-default.conf), andDEFAULT_URIis not on it. It only affects absolute URLs built outside a web request (some CLI-generated links); URLs generated while serving a request use the real request context and are correct.TRUSTED_HOSTSis not set, by design — see Configuration Management. Host-header validation is therefore not enforced at the application layer.- The database password must stay alphanumeric. Kimai's entrypoint recovers connection details from
DATABASE_URLwithawk -F '[/:@]', so a password containing/,:,@, or?breaks the database wait loop rather than failing loudly. Generation is constrained accordingly. - The admin account uses a placeholder email address (
admin@kimai.local) that cannot receive mail. - The database password appears in the service logs. The image's entrypoint runs under
bash -x, so every command it executes is traced — including theawkcalls that parseDATABASE_URL. Upstream suppresses tracing aroundAPP_SECRETbut not around the database credentials. The account is loopback-bound and internal to the service, so the exposure is to anyone who can already read the service's logs, but be aware of it before sharing a log dump. - LDAP and SAML are not configured by this package, though the image ships support for them.
What Is Unchanged from Upstream
Time tracking, timesheets and quick-entry, customers/projects/activities, teams and permissions, hourly and fixed rates, budgets, invoicing and invoice templates, exports (CSV, XLSX, PDF, DOCX), reporting, the REST API, two-factor authentication, plugins, and Kimai's own localization all behave exactly as the upstream documentation describes.
Contributing
See AGENTS.md.
Quick Reference for AI Consumers
package_id: kimai
architectures: [x86_64, aarch64]
volumes:
main: /opt/kimai/var
mysql: /var/lib/mysql
startos: store.json
ports:
ui: 8001
dependencies: none
startos_managed_env_vars:
- APP_ENV
- APP_SECRET
- DATABASE_URL
- TRUSTED_PROXIES
- MAILER_URL
- MAILER_FROM
actions:
- set-admin-password
- configure-smtp