If you'd rather not use the self-service portal, you can run Chastity Tracker entirely on your own server. The Docker image is publicly available — you keep full control over your data, your server, and your update schedule.
This guide is aimed at people with basic Linux and Docker knowledge. The software is provided as-is, without warranty of any kind.
Updated 30 July 2026: added the section on image tags and release channels.
Prerequisites
Before you start, make sure you have the following in place:
- A Linux server (VPS or home server) with root or sudo access
- Docker and Docker Compose installed
- A domain or subdomain pointing to your server's IP address
- Port 3000 reachable on the server (or a reverse proxy in front of it)
The GitHub repository is at github.com/trublue-2/chastitytracker. Release notes and known issues are documented there.
Docker Compose Setup
Create a directory for the installation and add a docker-compose.yml file:
mkdir chastitytracker && cd chastitytracker
services:
chastitytracker:
image: ghcr.io/trublue-2/chastitytracker:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
NEXTAUTH_SECRET: "your-secret-here"
NEXTAUTH_URL: "https://your-domain.com"
volumes:
- ./data:/app/data
The volume mount ./data:/app/data ensures the SQLite database and all uploaded photos survive container restarts and image updates. Without it, all data will be lost the next time you run docker compose pull. Which image tag you put in that file is a decision of its own — see Image tags: release channels and version pinning further down.
Configuration (.env)
Environment variables can be set directly in the docker-compose.yml as shown above, or placed in a separate .env file in the same directory. If you use a .env file, you must explicitly reference it in docker-compose.yml — otherwise the values will not be passed into the container:
services:
chastitytracker:
env_file: .env
...
The .env file itself contains the variables without indentation:
NEXTAUTH_SECRET=your-secret-here
NEXTAUTH_URL=https://your-domain.com
NEXTAUTH_SECRET is required. Generate a random string of at least 32 characters, for example:
openssl rand -base64 32
NEXTAUTH_URL must match exactly the URL the app is served from — including the protocol (https://) and without a trailing slash.
Initial Admin Account
Without an admin user the app is not usable after startup. Set the following variables to have the admin account created automatically on first start, if no admin exists yet in the database:
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-strong-password
ADMIN_EMAIL=admin@your-domain.com
These variables are only evaluated on the very first start. They can be removed or left empty afterwards.
SMTP (optional, recommended)
Email notifications only work if SMTP is configured. Add the following variables:
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your-user@example.com
SMTP_PASS=your-password
SMTP_FROM=noreply@your-domain.com
Without SMTP, the app runs fine, but email-based features such as notifications and password reset will not be available.
AI Inspection Verification (optional)
The tracker can automatically recognise handwritten codes in inspection photos (Claude Vision). This requires an Anthropic API key:
ANTHROPIC_API_KEY=sk-ant-...
Without this key the app runs fully — automatic image analysis during inspections is simply disabled.
Push Notifications (optional)
Web push notifications (PWA) require VAPID keys. Generate them once with:
node -e "const c=require('crypto').createECDH('prime256v1');c.generateKeys();console.log(c.getPublicKey('base64url'));console.log(c.getPrivateKey('base64url'))"
VAPID_PUBLIC_KEY=generated-public-key
VAPID_PRIVATE_KEY=generated-private-key
VAPID_SUBJECT=mailto:admin@your-domain.com
Passkey / WebAuthn (optional)
For Face ID, Touch ID, or other passkeys to work in production, you need to set your domain:
WEBAUTHN_RP_ID=your-domain.com
WEBAUTHN_RP_ORIGIN=https://your-domain.com
These variables are not needed locally — without them the app falls back to localhost.
Starting the App
Start the container with:
docker compose up -d
On first start, the application initialises the database automatically and creates the admin account if ADMIN_USERNAME is set. The app will then be reachable at your domain.
Check the status with:
docker compose logs -f
Once you see Ready on http://0.0.0.0:3000 in the logs, the app is reachable.
On first start, the container prints a diagnostics box showing which configuration values were loaded — useful for troubleshooting:
┌─────────────────────────────────────────────────────┐
│ STARTUP DIAGNOSE │
│ ADMIN_USERNAME : admin │
│ ADMIN_EMAIL : admin@your-domain.com │
│ ADMIN_PASSWORD : (set, 20 characters) │
│ NEXTAUTH_URL : https://your-domain.com │
│ DATABASE_URL : file:/app/data/prod.db │
│ data/ owner : www-data (33) │
└─────────────────────────────────────────────────────┘
Check here that all values were loaded as expected. If ADMIN_USERNAME shows (not set → admin), the variable was not passed to the container correctly.
Traefik / Reverse Proxy (optional)
If you use Traefik as a reverse proxy, you can add labels directly to the Compose file. Remove the ports block and replace it with labels:
services:
chastitytracker:
image: ghcr.io/trublue-2/chastitytracker:latest
container_name: kg-yourname
restart: unless-stopped
environment:
NEXTAUTH_SECRET: "your-secret-here"
NEXTAUTH_URL: "https://yourname.your-domain.com"
volumes:
- ./data:/app/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.chastitytracker.rule=Host(`yourname.your-domain.com`)"
- "traefik.http.routers.chastitytracker.entrypoints=websecure"
- "traefik.http.routers.chastitytracker.tls.certresolver=letsencrypt"
- "traefik.http.services.chastitytracker.loadbalancer.server.port=3000"
networks:
- traefik
networks:
traefik:
external: true
Replace yourname.your-domain.com and the network name with values that match your Traefik setup. TLS is handled automatically via Let's Encrypt, provided your Traefik configuration includes a letsencrypt certificate resolver.
Other reverse proxies (nginx, Caddy) work just as well — simply proxy port 3000 of the container and set NEXTAUTH_URL accordingly.
Image tags: release channels and version pinning
The Docker image is published under several tags, and the one in your docker-compose.yml decides how often — and how predictably — your deployment changes. The official release lives at ghcr.io/trublue-2/chastitytracker:latest, and that is the recommended choice for your own server.
| Tag | What it is | Who it's for |
|---|---|---|
:latest | The official, released build. It only moves on a deliberate release — nothing is rebuilt, an image that has already proven itself is retagged. | Self-hosters (recommended) |
:v4.56.1 etc. | A fixed version number, immutable. Points at the same image forever. | Anyone who wants to decide when their instance changes |
:portal, :feature | The operator's internal channels. They track ongoing development with no stability promise. | Not meant for self-hosters |
This is a change from how it used to work: every commit went straight to :latest, so a docker compose pull could pick up an untested build at any moment. From now on :latest only moves on a deliberate release, and only to an image that has already been running in production. A release therefore reflects experience, not an assurance — there is no support obligation, no guarantee and no response time on any of the channels.
If you use an update automation such as Watchtower, keep that in mind: pointed at :latest it will pull every release automatically. Pin a version if you don't want that.
Pinning to a fixed version
Put a concrete version number in your docker-compose.yml instead of :latest:
services:
chastitytracker:
image: ghcr.io/trublue-2/chastitytracker:v4.56.1
That tag never moves on its own. Updating becomes a deliberate step: raise the version number, docker compose pull, docker compose up -d. Available versions are listed in the app changelog and in the GitHub repository.
Rolling back to an older version
Because version tags are immutable, a rollback is one line in the Compose file: put the older version number back, then docker compose pull && docker compose up -d. One caveat — this covers the application, not the database: if the newer version migrated the schema, the rollback needs your backup of the ./data directory from before the update. Another reason to actually take that backup.
The in-app update notice
The "new version available" notice in the header follows the released build, so it no longer points at a version for which no image exists yet. If you would rather use your own changelog source, the variables DISABLE_UPDATE_CENSUS and UPSTREAM_CHANGELOG_URL are documented in docs/update-check.md.
Applying updates
To update to a new image:
docker compose pull
docker compose up -d
Docker pulls the new image and restarts the container. The database in the volume is left untouched. Before each update, check the release notes in the GitHub repository for breaking changes or required migration steps.
Backing up the ./data directory before every update is strongly recommended.
If you run into issues, start with the GitHub Issues. If you'd rather not manage your own server, you can spin up a managed instance through the self-service portal.