- Rust 86.1%
- HTML 9.8%
- CSS 1.6%
- Dockerfile 1.3%
- JavaScript 1.2%
| .sqlx | ||
| migrations | ||
| src | ||
| static | ||
| templates | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Containerfile | ||
| README.md | ||
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/callbackfor 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