Deployment guide

Self-Host Kimai on a VPS: Open-Source Time Tracking

Deploy on a VPS Cloud →

Tutorial

Self-Host Kimai on a VPS: Open-Source Time Tracking

Self-hosting14 min read14 steps

Kimai is an MIT-licensed open-source time tracker that can replace Toggl Track, Clockify or Harvest for the long haul: timers, timesheets, customers, projects, activities, hourly rates, budgets, reports and invoicing. This guide goes well beyond installation. It covers the architecture of the Docker stack, an annotated `docker-compose.yml` for manual deployments, HTTPS behind a reverse proxy, creating accounts from the command line, the data model and roles, invoicing, the REST API, plugins, backups, upgrades and the errors you are most likely to meet. By the end you will be able to run Kimai in production for a whole team, not just get it to start.

Contents· Why Self-Host Kimai?1/21
  1. 01Why Self-Host Kimai?
  2. 02Architecture: What Runs on the VPS
  3. 03The building blocks
  4. 04Deploy Kimai on ServOrbit
  5. 05Manual Installation with Docker Compose
  6. 06Environment variables worth knowing
  7. 07Put Kimai Behind a Domain with HTTPS
  8. 08Create Users from the Command Line
  9. 09The Data Model: Customers, Projects, Activities
  10. 10Rates and budgets
  11. 11Roles, Teams and Permissions
  12. 12Invoicing from Timesheets
  13. 13Set Up Your First Invoice Template
  14. 14VAT and mandatory mentions
  15. 15REST API and Integrations
  16. 16Extend Kimai with Plugins
  17. 17Backup and Restore
  18. 18Upgrades without surprises
  19. 19Common Troubleshooting
  20. 20Kimai vs Toggl Track, Clockify and Harvest
  21. 21Sizing Your VPS

Why Self-Host Kimai?

A time tracker holds sensitive data: who works for which customer, for how many hours, at what rate. On a SaaS platform that information lives with the vendor, and the bill grows with every seat you add. Hosting Kimai on your own VPS keeps the database within reach, lets you add users without licence fees, and puts you in charge of when upgrades happen.

  • MIT licence — no licence fees and no user cap imposed by the vendor.
  • Built-in invoicing — invoices are generated straight from timesheets, no external module needed.
  • Documented REST API — connect your internal tools (ticketing, HR, dashboards) to your own data.
  • Data sovereignty — hours, rates and customer records stay in your MySQL database.
  • Lightweight stack — one application container plus a MySQL database, comfortable on an entry-level VPS.

Architecture: What Runs on the VPS

Kimai is a PHP application built on the Symfony framework. Version 2 relies on MySQL or MariaDB; SQLite is no longer supported. The official kimai/kimai2 image comes in two flavours: apache (bundled web server, listening on port 8001) and fpm (meant to sit behind your own Nginx). The ServOrbit template uses kimai/kimai2:apache-latest with MySQL 8.0, so the whole stack is just two containers.

The building blocks

  • kimai container — Apache and PHP, the Symfony application in /opt/kimai, port 8001 published on 127.0.0.1 only.
  • db container — MySQL 8.0 with a mysqladmin ping healthcheck, so Kimai only starts once the database is ready.
  • kimai_db volume — the MySQL data files, in other words every timesheet you record.
  • kimai_data volume — /opt/kimai/var/data, files uploaded through the application (invoice templates, documents).
  • kimai_public volume — /opt/kimai/public/avatars, user avatars.
  • Reverse proxy — Nginx, Caddy or Traefik terminates HTTPS and forwards requests to local port 8001.

Deploy Kimai on ServOrbit

The ServOrbit Marketplace provisions the full stack on Ubuntu 24.04: Docker, MySQL 8.0, the Kimai container, the three volumes and a generated administrator password. You do not need a domain name to get started.

  1. Start the deployment

    In your client area: Marketplace → ERP & Business Management → Kimai → Deploy. ServOrbit creates the VPS, installs Docker and starts the stack (MySQL 8 + Apache/PHP) automatically.

  2. Collect your credentials

    Wait about 60 seconds. The app card shows your credentials: login [email protected] and a generated password.

  3. Connect without a domain

    Open an SSH tunnel: ssh -L 8080:127.0.0.1:8001 root@<ip>, then browse to http://localhost:8080. Add a domain from the dashboard whenever you are ready.

  4. Create a customer and a project

    Create your first customer, then a project linked to it, and at least one activity. Set an hourly rate or a budget.

  5. Log your first hours

    Click ▶ to start the timer and pick the project and activity: Kimai records your time as you work.

On first login, open your profile to replace [email protected] with a real address and change the generated password. Then keep that super-admin account for administration only and create named accounts for day-to-day work.

Manual Installation with Docker Compose

If you would rather install Kimai yourself on an existing VPS, here is a docker-compose.yml equivalent to the template, with one extra volume for plugins. Put it in a dedicated directory such as /opt/kimai.

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: kimai
      MYSQL_USER: kimai
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - kimai_db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
      interval: 10s
      retries: 12

  kimai:
    image: kimai/kimai2:apache-latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:8001:8001"
    environment:
      DATABASE_URL: "mysql://kimai:${DB_PASSWORD}@db/kimai?charset=utf8mb4&serverVersion=8.0.34"
      APP_SECRET: ${APP_SECRET}
      ADMINMAIL: [email protected]
      ADMINPASS: ${ADMIN_PASSWORD}
      TRUSTED_HOSTS: "localhost,127.0.0.1,kimai.your-domain.com"
    volumes:
      - kimai_data:/opt/kimai/var/data
      - kimai_plugins:/opt/kimai/var/plugins
      - kimai_public:/opt/kimai/public/avatars

volumes:
  kimai_db:
  kimai_data:
  kimai_plugins:
  kimai_public:

Next, generate the secrets into a .env file, which Compose reads automatically, and start the stack. On first boot the container creates the database schema and the administrator account defined by ADMINMAIL and ADMINPASS.

cat > .env <<EOF
DB_PASSWORD=$(openssl rand -hex 16)
DB_ROOT_PASSWORD=$(openssl rand -hex 16)
APP_SECRET=$(openssl rand -hex 32)
ADMIN_PASSWORD=$(openssl rand -hex 12)
EOF
chmod 600 .env
docker compose up -d
docker compose logs -f kimai

Environment variables worth knowing

  • DATABASE_URL — the Doctrine connection string: user, password, host db, database name, charset=utf8mb4 and serverVersion. Because the password sits inside a URL, avoid @, / or : in it; a hexadecimal secret sidesteps the problem.
  • APP_SECRET — the Symfony key used to sign sessions and tokens. Use a long random value, generate it once and leave it alone: changing it logs everyone out.
  • ADMINMAIL and ADMINPASS — the super-admin account created on first boot. Once the instance is running, manage accounts from the UI or the console.
  • TRUSTED_HOSTS — a comma-separated list of accepted host names. It must include your public domain, otherwise Symfony rejects the requests.
  • MAILER_URL — the mail transport. The ServOrbit template sets it to null://localhost, so no email is sent. Point it at an SMTP server to enable password resets and notifications.

Put Kimai Behind a Domain with HTTPS

  1. Point the DNS

    Create an A record for kimai.your-domain.com pointing at the VPS public IP, and check propagation with dig +short kimai.your-domain.com.

  2. Configure the reverse proxy

    Port 8001 only listens on 127.0.0.1, so only the local proxy can reach it. A sample Nginx block:

    server {
        listen 80;
        server_name kimai.your-domain.com;
    
        location / {
            proxy_pass http://127.0.0.1:8001;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
  3. Get the certificate

    With certbot and its Nginx plugin, the Let's Encrypt certificate is issued and the server block is extended for port 443:

    apt install -y certbot python3-certbot-nginx
    certbot --nginx -d kimai.your-domain.com
  4. Tell Kimai about the domain

    Add kimai.your-domain.com to TRUSTED_HOSTS, then run docker compose up -d to recreate the container. If generated links still use http://, read the Kimai documentation on reverse proxies: X-Forwarded-* headers are only honoured when they come from a trusted proxy.

  5. Close what you do not need

    Open only ports 22, 80 and 443 in the firewall. MySQL publishes no port and should never have one.

Create Users from the Command Line

The web UI is enough for daily account management, but the Kimai Symfony console is invaluable for scripted onboarding or for regaining access. The kimai:user:create command takes a username, an email address and a role, then prompts for the password:

docker compose exec kimai /opt/kimai/bin/console kimai:user:create alice [email protected] ROLE_TEAMLEAD

The same console offers kimai:reload to rebuild the cache after a configuration change, and kimai:update to run database migrations on installations outside Docker. Run bin/console list kimai inside the container to see the commands available in your version.

The Data Model: Customers, Projects, Activities

Kimai organises time on three levels. A customer carries the currency, the time zone and the billing details. A project belongs to a customer and can have start and end dates, an order number and a budget. An activity describes the kind of work (development, meeting, support); it can be attached to a single project or be global, available everywhere. Each timesheet entry ties together a user, a project, an activity, a time range and, if needed, a description and tags. Getting this hierarchy right from the start saves painful clean-ups later: one customer per billed entity, one project per engagement or contract, and global activities for cross-cutting work.

Rates and budgets

  • Hourly rate — can be set on the customer, project, activity or user; the most specific level applies to each entry.
  • Fixed rate — a flat amount per entry, handy for a job billed at a set price regardless of duration.
  • Internal rate — what an hour actually costs you, kept separate from the billed price so you can measure project profitability.
  • Time or money budget — on a customer, project or activity, with a visual consumption indicator.
  • Monthly or total budget — a monthly maintenance retainer is tracked differently from a fixed-price project over its whole lifetime.
  • Overruns — dashboards show how much of the budget is used, and a configuration setting lets you allow or block entries beyond it.

Roles, Teams and Permissions

Kimai uses hierarchical roles, and the Roles and permissions screen lets you fine-tune what each role can see and change. Teams complete the picture: a customer, project or activity restricted to a team is only visible to its members, and the team lead can review and adjust the hours of their team without seeing the rest of the company. For an outside contractor, create a user account limited to a dedicated team.

  • ROLE_USER — records and reviews their own hours on the projects they can access.
  • ROLE_TEAMLEAD — sees and edits the hours of their team members and reads their reports.
  • ROLE_ADMIN — manages customers, projects, activities, invoices and users.
  • ROLE_SUPER_ADMIN — full access, including system configuration and permissions.

Invoicing from Timesheets

The invoicing module turns a selection of hours into a document. From the Invoices menu you pick a customer, a period, optionally a project, and then a template. Kimai computes the amounts from the rates, shows a preview and stores the invoice with a status (pending, paid, cancelled). Billed entries can be marked as exported, which prevents double billing and protects them from later edits.

Set Up Your First Invoice Template

  1. Create the template

    Under Invoices → Templates, enter the issuer, address, bank details, payment terms and footer text.

  2. Choose the output format

    Pick a PDF or HTML renderer for a ready-to-send document, or an office format if you finish the invoice in another tool.

  3. Define the numbering

    Configure the invoice number format (prefix, year, counter) to get a continuous sequence that matches your accounting habits.

  4. Set the tax

    Enter the applicable tax rate in the template; create one template per tax regime if you bill customers at different rates.

VAT and mandatory mentions

Kimai computes tax from the rate entered in the template, but it knows nothing about your tax situation. Legal mentions, the applicable rate and invoice retention rules depend on your country and status, so have your accountant review the template. If you keep full books in an ERP such as Dolibarr, Kimai can remain the time-capture tool and its export can feed the final invoice.

REST API and Integrations

Kimai exposes a REST API covering customers, projects, activities, timesheets and users. Its interactive documentation is served by your own instance at /api/doc, always in sync with the installed version. To authenticate a script, create an API token from your user profile and send it as a header:

curl -s -H "Authorization: Bearer YOUR_TOKEN" \
  https://kimai.your-domain.com/api/timesheets

A token inherits its user's permissions, so create a service account with the minimum role required instead of using the super-admin. The API lets you start a timer from a shortcut, feed hours into a dashboard, or sync projects with your ticketing tool.

Extend Kimai with Plugins

Kimai has a plugin store on its official website, with free and paid extensions: expenses, custom fields, timesheet approval workflows, imports from other tools. A plugin is a directory placed in var/plugins/. With Docker, mount that directory on a volume, or it disappears whenever the container is recreated — that is what kimai_plugins does in the compose file above. A typical installation:

docker compose cp PluginNameBundle kimai:/opt/kimai/var/plugins/
docker compose exec kimai /opt/kimai/bin/console kimai:reload --env=prod

Some plugins add database tables and ship their own install command; follow the plugin page and check compatibility with your Kimai version before every upgrade.

Backup and Restore

Almost everything lives in the MySQL database; the kimai_data and kimai_public volumes complete the backup. A logical dump is more portable than a hot copy of the MySQL files:

docker compose exec -T db sh -c 'exec mysqldump --single-transaction -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > kimai-$(date +%F).sql
docker run --rm -v kimai_kimai_data:/data -v "$PWD":/backup alpine tar czf /backup/kimai_data-$(date +%F).tgz -C /data .

The actual volume name is prefixed with the Compose project name, so check it with docker volume ls. To restore, start an empty stack with the same .env, then load the dump back:

docker compose exec -T db sh -c 'exec mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' < kimai-YYYY-MM-DD.sql

Schedule the dump nightly with cron, copy it off the VPS (Restic, BorgBackup or object storage) and test a restore at least once: a backup that has never been restored is only a hypothesis.

Upgrades without surprises

  • Back up first — a dump taken right before the upgrade lets you roll back if a migration fails.
  • Update the image — docker compose pull kimai && docker compose up -d kimai, then follow docker compose logs -f kimai. On startup the container applies database migrations automatically.
  • Pin a version — in production, replace apache-latest with a versioned image tag so you decide when to upgrade.
  • Read the release notes — some major versions change requirements or plugin behaviour.
  • Export regularly — a CSV or XLSX export of timesheets is a readable copy, useful for an audit or a migration.

Common Troubleshooting

Most Kimai-on-Docker incidents come down to a handful of known causes. Always start with docker compose logs kimai and docker compose ps.

  • "Untrusted Host" or a 400 error after adding a domain — the host name is missing from TRUSTED_HOSTS. Add it and recreate the container.
  • 500 error and "Permission denied" on var/ — often after a console command run as a different user from the web server. Restore ownership with docker compose exec kimai chown -R www-data:www-data /opt/kimai/var, then run kimai:reload.
  • Configuration change has no effect — the Symfony cache is serving the old version: docker compose exec kimai /opt/kimai/bin/console kimai:reload --env=prod.
  • Kimai keeps restarting on first boot — the database is not ready or DATABASE_URL is wrong. Check the db healthcheck and the credentials in .env.
  • http:// links behind the HTTPS proxy — X-Forwarded-Proto headers are not sent or not trusted; review the proxy configuration.
  • Forgotten password with no email configured — MAILER_URL points to null://: reset the password from an admin account or create a new administrator from the console.

Kimai vs Toggl Track, Clockify and Harvest

Scroll the table

CriterionKimai (self-hosted)Toggl TrackClockifyHarvest
LicenceOpen source (MIT)ProprietaryProprietaryProprietary
HostingYour VPSVendor SaaSVendor SaaSVendor SaaS
Where data livesYour MySQL databaseVendor infrastructureVendor infrastructureVendor infrastructure
Cost modelServer only, no per-user feeLimited free tier, then subscriptionFree tier, then subscriptionLimited free tier, then subscription
InvoicingBuilt-in module (templates, numbering, tax)Depends on planDepends on planBuilt into the product
APIREST, tokens, bundled docsRESTRESTREST
CustomisationSource code, plugins, configurationSettings and integrationsSettings and integrationsSettings and integrations

Sizing Your VPS

Kimai is frugal: most of the load comes from MySQL and from reports over long periods. For a freelancer or a small team, 1 vCPU and 1 to 2 GB of RAM is enough. Beyond roughly twenty active users, or if you regularly produce yearly reports and large exports, plan for 2 vCPUs and 4 GB, which leaves headroom for the MySQL InnoDB cache. Disk usage stays modest: a few gigabytes cover years of timesheets, but keep room for local dumps before they are shipped off the server.

Before inviting your team, set the ground rules under Settings: duration rounding, whether future entries are allowed, and locking of periods that have already been billed. These choices are far easier to make on an empty instance than on months of history.

Deploy Kimai on Your VPS in 60 Seconds

ServOrbit pre-configures the full stack (Apache/PHP + MySQL 8) and generates your admin credentials — ready to log your first hours.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab