Skip to content

Self-Hosting with Docker ​

Deploy the full TT Time Tracker stack — frontend, API, worker, database, cache, and file storage — using Docker Compose.

Prerequisites ​

  • A Linux server with Docker and Docker Compose installed
  • At least 1 GB of free RAM
  • A domain name (optional but recommended for HTTPS)
  • A Google OAuth app (for user login) — create one at console.cloud.google.com
  • An OpenRouter API key (for AI invoice extraction — required to run the worker)

Step 1 — Get the source ​

bash
git clone <repository-url> tt-time-tracker
cd tt-time-tracker

Step 2 — Configure the environment ​

bash
cp .env.example .env

Edit .env and fill in every required variable:

bash
# Generate a secure secret (run this on your server)
openssl rand -base64 32

Required values to set:

VariableWhat to set
POSTGRES_PASSWORDA strong password — do not leave it as changeme
BETTER_AUTH_SECRETOutput of openssl rand -base64 32
BETTER_AUTH_URLYour API's public URL, e.g. https://api.yourdomain.com
GOOGLE_CLIENT_IDFrom your Google OAuth app
GOOGLE_CLIENT_SECRETFrom your Google OAuth app
RUSTFS_ACCESS_KEYA strong random key
RUSTFS_SECRET_KEYA strong random secret

Step 3 — Build and start the stack ​

bash
docker compose -f docker-compose.yaml up --build -d

This builds the API and worker images and starts all five services:

  • postgres on port 35432
  • redis on port 36379
  • rustfs on port 39000 (API) and 39001 (console)
  • api on port 33000
  • worker (no exposed port)

The API automatically runs database migrations on first startup.

Step 4 — Verify the deployment ​

bash
# Check all services are healthy
docker compose ps

# Tail the API logs
docker compose logs api -f

The API should log Application is running on: http://[::1]:33000.

Step 5 — Set up a reverse proxy (optional) ​

If you want to serve the frontend and API on standard ports with HTTPS, place a reverse proxy (nginx, Caddy, Traefik) in front:

Example nginx configuration:

nginx
# API
server {
    listen 443 ssl;
    server_name api.yourdomain.com;

    location / {
        proxy_pass http://localhost:33000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# Frontend (if serving the built static files)
server {
    listen 443 ssl;
    server_name app.yourdomain.com;

    location / {
        proxy_pass http://localhost:80;  # or serve dist/ directly
    }
}

Step 6 — Build and serve the frontend ​

The frontend is a static Vue SPA. It calls the API over a relative /api path, so there is no API URL to bake in at build time — your reverse proxy routes /api to the NestJS service. The only build argument is the version string:

bash
VITE_APP_VERSION=$(git describe --tags --always) pnpm build:client

The output is in services/client/dist/. Serve it with any static file server, or use the included Dockerfile which produces an nginx image:

bash
docker build \
  --build-arg VITE_APP_VERSION=$(git describe --tags --always) \
  -f services/client/Dockerfile \
  -t tt-frontend .

docker run -d -p 80:80 tt-frontend

Point your reverse proxy so the app's /api/* requests reach the API container (the compose stack wires this up for you).

Updating ​

bash
git pull
docker compose -f docker-compose.yaml up --build -d

Migrations run automatically when the API container restarts.

Data persistence ​

All persistent data is stored under ./data/ on the host:

DirectoryContents
./data/pg/PostgreSQL data
./data/redis/Redis AOF journal
./data/rustfs/data/Uploaded invoice files

Back up these directories to protect your data.

TT Time Tracker — Internal Documentation