No description
  • Rust 86.1%
  • HTML 9.8%
  • CSS 1.6%
  • Dockerfile 1.3%
  • JavaScript 1.2%
Find a file
2026-08-07 13:25:08 +02:00
.sqlx initial prototype 2026-08-07 13:25:08 +02:00
migrations initial prototype 2026-08-07 13:25:08 +02:00
src initial prototype 2026-08-07 13:25:08 +02:00
static initial prototype 2026-08-07 13:25:08 +02:00
templates initial prototype 2026-08-07 13:25:08 +02:00
.env.example initial prototype 2026-08-07 13:25:08 +02:00
.gitignore initial prototype 2026-08-07 13:25:08 +02:00
Cargo.lock initial prototype 2026-08-07 13:25:08 +02:00
Cargo.toml initial prototype 2026-08-07 13:25:08 +02:00
Containerfile initial prototype 2026-08-07 13:25:08 +02:00
README.md initial prototype 2026-08-07 13:25:08 +02:00

Timetracker

A small self-hosted time tracker: log in with an existing Forgejo account, track time per project with a start/stop timer or a manual "1h 30m"-style entry. Single Rust binary, SQLite for storage, meant to run as a Podman container behind a reverse proxy (Caddy) that already handles TLS.

1. Register an OAuth2 application on Forgejo

On your Forgejo instance: Settings → Applications → Manage OAuth2 Applications → create a new application.

  • Application name: timetracker (or anything)
  • Redirect URI: https://timetracker.yourdomain.example/auth/callback (use the real domain you'll expose it on; http://localhost:8080/auth/callback for local testing)

Forgejo gives you a Client ID and Client Secret — you'll need both below.

2. Build the image

podman build -t timetracker:latest -f Containerfile .

3. Configure

Copy the example env file and fill in real values:

cp .env.example .env
Variable Value
DATABASE_URL sqlite:///data/timetracker.db (path inside the container — see the volume below)
FORGEJO_BASE_URL Your Forgejo's base URL, e.g. https://git.yourdomain.example
OAUTH_CLIENT_ID From step 1
OAUTH_CLIENT_SECRET From step 1
OAUTH_REDIRECT_URL Must match the redirect URI from step 1 exactly
COOKIE_SIGNING_KEY Random secret, generate with openssl rand -base64 64 | tr -d '\n'
COOKIE_SECURE true in production (Caddy serves HTTPS)
PORT 8080 (default)
RUST_LOG info (default)

HOST isn't needed here — it defaults to 0.0.0.0, which is what the container needs.

4. Create a volume and run

The SQLite database needs to live outside the container so it survives rebuilds/updates. Everything else is stateless.

podman volume create timetracker-data

podman run -d \
  --name timetracker \
  --restart unless-stopped \
  -v timetracker-data:/data \
  --env-file .env \
  -p 127.0.0.1:8080:8080 \
  timetracker:latest

-p 127.0.0.1:8080:8080 publishes the app only to localhost, since Caddy (running on the same host) is what should be reachable from outside. If Caddy itself runs in a container, use a shared Podman network instead — see the note at the end.

Check it's up:

curl -i http://127.0.0.1:8080/healthz

5. Point Caddy at it

Add to your existing Caddyfile:

timetracker.yourdomain.example {
    reverse_proxy 127.0.0.1:8080
}

Reload Caddy (caddy reload or however your setup does it). Caddy handles TLS/certificates as it already does for your other sites — the app only ever speaks plain HTTP.

Updating

podman build -t timetracker:latest -f Containerfile .
podman stop timetracker && podman rm timetracker
# re-run the `podman run` command from step 4

The database in timetracker-data is untouched by this — schema migrations run automatically on startup if there are any.

Backups

The whole database is one file inside the volume:

podman volume inspect timetracker-data --format '{{.Mountpoint}}'

Backup with the usual sqlite tools