← Back to blog

2026-07-30 · 7 min read · TruBlue

Self-Hosting Chastity Tracker with Docker

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:

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.

TagWhat it isWho it's for
:latestThe 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, :featureThe 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.