How to self-host solidtime on your own VPS without Docker
Run solidtime v0.21.0, an open-source time tracker, on your Ubuntu server with ox: PostgreSQL, a queue worker and a scheduler, from the config that ran.
To self-host solidtime you need an Ubuntu server, a fork of the solidtime repo with one ox.toml file added, and 59 variables. In our run the first deploy took about 6 minutes, and ox ran PostgreSQL, a queue worker and a scheduler beside the web app.
How we tested On Oct 17, 2026, ox deployed solidtime v0.21.0 from commit b33cf007 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, registered through the e-mail link, created a project, ran a timer on it, and saw both after a reload, and checked the app's services and its backup.
solidtime is an open-source time tracker for freelancers and agencies. You start a timer on a project or add an entry by hand, and the app keeps projects, tasks, clients, billable rates and reports, with several organizations and roles per account. It is a Laravel 12 app with a Vue front end, under the AGPL, and its data lives in PostgreSQL. Importing from Toggl or Clockify was not tried.
ox deployed solidtime v0.21.0 (commit b33cf007) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 6 minutes and a redeploy took 2 minutes.
- First deploy: 6 minutes, from adding the project until the app passed the run's first checks.
- Redeploy of the same commit: 2 minutes, until the app passed the checks again.
- Server: DigitalOcean, 8 vCPUs and 16 GB of memory (
s-8vcpu-16gb), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app. - Memory in use after the reboot: 1071 MB of 15986 MB, for the whole server: Ubuntu, the app and its staging copy, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less.
- Services ox runs for it:
postgres. - Background processes:
queue, each its own systemd unit beside the web process. - Scheduled jobs:
schedule. - Source: github.com/solidtime-io/solidtime at v0.21.0, commit
b33cf007.



What you need
An Ubuntu LTS server. The run used the size in the facts above and measured the memory there.
A GitHub account to fork
solidtime-io/solidtime, and an ox account, which you open by signing in with GitHub.An SMTP server. solidtime mails a link to confirm each new account, and its pages stay locked until it is clicked. The run used a test mail catcher, so real SMTP was not tested.
The steps
Open the ox dashboard, press Add a server, and run the command it shows as root on your Ubuntu server. It installs the ox agent and Caddy.
Fork solidtime-io/solidtime and check out commit b33cf007, the run's commit, which solidtime calls v0.21.0. Add the ox.toml shown below at the repo's root, then commit and push. Write solidtime.env on your laptop with one KEY=value line for each key in the variables list below. APP_KEY is Laravel's encryption key: base64: followed by 32 random bytes in base64, and openssl rand -base64 32 makes the second half. With the ox CLI signed in, run:
ox new <you>/solidtime
ox review solidtime --from-file solidtime.env --waitThe review saves your values and starts the first deploy, and --wait follows the run to its end. The console has the same steps, in the quickstart.
When the deploy is green, open the address ox shows. solidtime has no default login, so go to /register, fill in the form, and open the link it mails. The test agent did that with an example.com address, read the link in the mail catcher, created a project, and ran a timer named "Homepage wireframes" on it. After a reload, both were listed.
The run used the project's preview address, so a domain of your own was not part of it. The domains page covers adding one.
The ox.toml, line by line
# solidtime v0.21.0 (Laravel 12, Inertia and Vue, PostgreSQL), as its
# operator would write it: ox detects the Composer install, the Vite build,
# FrankenPHP and artisan migrate.
[app]
# The cheap route: it checks neither the database nor the cache.
health = "/health-check/up"
[build]
# solidtime's composer.json pins php 8.3.* and its lock's phpspreadsheet
# wants php below 8.5, the only PHP ox serves (M-6a; v0.21.0 is its latest
# release). The operator accepts the risk ox check names: Composer ignores
# the php version, the rest of the install is ox's detected one.
install = "composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-req=php && npm ci"
# Passport signs API tokens with a key pair. The stock Docker image takes it
# from PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY, which an operator makes
# with `self-host:generate-keys`; here passport:keys writes the files to
# storage/ once, and storage is kept.
migrate = "php artisan migrate --force && { test -f storage/oauth-private.key || php artisan passport:keys --no-interaction; }"
[services]
postgres = {}
[workers]
# QUEUE_CONNECTION=database: mails, exports and imports run here.
queue = "php artisan queue:work --tries=3 --max-time=3600"
[cron]
# Laravel's scheduler: reminder mails, the database consistency check.
schedule = { schedule = "* * * * *", run = "php artisan schedule:run --no-interaction" }
[storage]
# Laravel writes storage/ (logs, the file cache, the Passport keys, uploads).
keep = ["storage"]ox already recognizes a Laravel app: it serves public with FrankenPHP and runs vite build from package.json. The file adds what solidtime needs on top.
healthis solidtime's own/health-check/up. It answers a small JSON success and checks neither the database nor the cache, and solidtime skips its trusted-host check on that path. ox moves traffic to a new release only once the path answers, and waits up to two minutes.installis the install ox would detect, plus--ignore-platform-req=phpandnpm ci. solidtime'scomposer.jsonrequires PHP8.3.*and a locked package allows only PHP below 8.5, while ox serves 8.5 and no other version.ox checkstops on that until the flag is in the line. The run's checks passed on PHP 8.5 this way, and PHP 8.3 was not tested.migratebuilds the schema in PostgreSQL, then makes the Passport key pair ifstorage/oauth-private.keyis missing. Passport signs API tokens with it, andstorageis kept, so the keys are made once. API tokens were not tested.postgres = {}asks for a PostgreSQL database for this project.The
queueworker runsqueue:workon the database queue. Two jobs recalculate the tracked time of a project and of a task after entries change, and invitation, timer reminder and token expiry mail is queued too.--tries=3tries a failing job three times, and--max-time=3600ends the worker after an hour so ox starts it again. The test agent saw the project job logged as done. The sign-up link is sent by the web process.The
schedulejob runsphp artisan schedule:runevery minute, and Laravel decides what is due. Every ten minutes solidtime looks for timers left running and tokens near expiry, and twice a day it checks for a new version and sends usage counts to solidtime's servers. A consistency check is off unless its variable turns it on. The test agent saw the scheduler tick every minute.keep = ["storage"]names the folder Laravel writes: logs, the file cache, the Passport keys and, on the local disk, uploads and exports. A PHP release is read-only while it runs, so ox linksstorageto the project's data folder, which survives deploys.
APP_NAME
APP_ENV
APP_KEY
APP_DEBUG
APP_URL
APP_FORCE_HTTPS
TRUSTED_PROXIES
APP_ENABLE_REGISTRATION
SUPER_ADMINS
PAGINATION_PER_PAGE_DEFAULT
LOG_CHANNEL
LOG_DEPRECATIONS_CHANNEL
LOG_LEVEL
DB_CONNECTION
DB_HOST
DB_PORT
DB_DATABASE
DB_USERNAME
DB_PASSWORD
DB_TEST_HOST
DB_TEST_PORT
DB_TEST_DATABASE
DB_TEST_USERNAME
DB_TEST_PASSWORD
BROADCAST_DRIVER
CACHE_DRIVER
QUEUE_CONNECTION
SESSION_DRIVER
SESSION_LIFETIME
MAIL_MAILER
MAIL_HOST
MAIL_PORT
MAIL_USERNAME
MAIL_PASSWORD
MAIL_ENCRYPTION
MAIL_FROM_ADDRESS
MAIL_FROM_NAME
MAIL_REPLY_TO_ADDRESS
MAIL_REPLY_TO_NAME
FILESYSTEM_DISK
PUBLIC_FILESYSTEM_DISK
S3_ACCESS_KEY_ID
S3_SECRET_ACCESS_KEY
S3_REGION
S3_BUCKET
S3_URL
S3_ENDPOINT
S3_USE_PATH_STYLE_ENDPOINT
PASSPORT_PERSONAL_ACCESS_CLIENT_ID
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET
AUDITING_ENABLED
TELESCOPE_ENABLED
GOTENBERG_URL
OCTANE_SERVER
NGINX_HOST_NAME
NETWORK_NAME
FORWARD_DB_PORT
VITE_HOST_NAME
VITE_APP_NAMEFifty-eight of the 59 keys are in solidtime's .env.example, and the other is TRUSTED_PROXIES. APP_ENV is production, APP_DEBUG is false, and APP_URL is ${PUBLIC_URL}, a reference ox fills with the project's address (see variables). solidtime refuses requests whose host is not that address or a subdomain of it, unless TRUSTED_HOSTS lists it. TRUSTED_PROXIES is 127.0.0.1, so Laravel believes the X-Forwarded-Proto header from ox's proxy and builds https links.
APP_ENABLE_REGISTRATION is on, and the example also lists off and invite-only. While it is on, anyone who can reach the address can register, and changing it was not tested. SUPER_ADMINS lists emails that get the admin pages once confirmed, which the run did not test.
DB_CONNECTION is pgsql and the other DB_ keys are references to the postgres service, such as ${postgres.host} (config lists them), and DB_TEST_* are empty. CACHE_DRIVER is file, and SESSION_DRIVER and QUEUE_CONNECTION are database, so there is no Redis. LOG_CHANNEL is stderr, so logs reach the journal that ox logs reads. The MAIL_* keys point at the mail catcher.
The example sends files to an S3 bucket. The run used the local and public disks instead, left the S3 credentials and bucket empty, and did not test S3. GOTENBERG_URL is empty: PDF export needs a Gotenberg server, the run had none, and PDF export was not tested. The Passport client keys and the dev-only keys are empty.
PostgreSQL and the units
solidtime needs PostgreSQL and no other service. ox runs one PostgreSQL for the server and gives this project its own database and role. Browse it in the Services tab or with ox explore solidtime postgres. A staging copy ran beside production, so the memory in the facts covers both.
Updates and backups
To move to a newer release, merge it into your fork and push, or run ox deploy solidtime --wait. ox builds it beside the running one, runs migrate, and switches once the health path answers. It dumps PostgreSQL before the migrate, and the run's redeploy left such a dump. A newer solidtime was not tested.
Backups need no setup. The run's backups held a PostgreSQL dump and an archive of the kept storage folder, taken daily and again by Back up now. List them with ox backups solidtime. They hold no variables, so keep solidtime.env. A restore was not tested. The backups page covers your own S3 bucket.
Frequently asked questions
Do I need Docker to self-host solidtime?
No. The solidtime repo has a production Dockerfile under docker/prod, but the app is a plain Laravel project. ox runs Composer and npm on your server, serves the app with FrankenPHP, and keeps PostgreSQL, the queue worker and the scheduler as system services.
How do I create the first solidtime account?
Leave APP_ENABLE_REGISTRATION on, open /register on the deployed address, and fill in your name, email, password, its confirmation and the terms box. solidtime then mails a link, and its pages stay locked until you click it, so SMTP must work. The run used a mail catcher, so real mail was not tested.
How much memory does solidtime use?
After the run's reboot the whole server used the memory shown in the facts. That counts solidtime and its staging copy, PostgreSQL, Caddy and the ox agent. ox takes any server from 1 GB up and tells you when memory is why an app stopped.
What is TRUSTED_PROXIES for in solidtime?
It names the proxies Laravel believes when they say a request came over https. ox's Caddy calls solidtime from 127.0.0.1, so the run set TRUSTED_PROXIES=127.0.0.1. Left empty, Laravel trusts no proxy and builds http links, and a browser refuses to load http scripts and styles on an https page.
Does self-hosted solidtime send data to its makers?
By default its scheduler runs two jobs twice a day. One checks for a new version. The other sends counts, such as users, projects and time entries, with the app's URL and version, to app.solidtime.io. config/scheduling.php has a variable for each. The run set neither, and switching them off was not tested.
New to running apps on your own server? How ox self-hosts your apps shows what ox sets up on your server.
