Skip to content

Install with Docker Compose

Docker Compose is the supported install path for CookTrace, LiftTrace, and NutriTrace. One docker-compose.yml, one .env, docker compose up -d, done. All three images are multi-arch (linux/amd64 and linux/arm64) and published to ghcr.io/traceapps/<app>.

Prerequisites

  • Docker Engine 24+ with the Compose plugin (docker compose, not the legacy docker-compose).
  • Roughly 200 MB of disk for the container image, plus whatever your data grows to (SQLite database plus uploaded images).
  • A long random string for JWT_SECRET. Generate one with openssl rand -base64 48.

Minimal compose file

Pick the tab for the app you're installing. Each snippet is a complete, working file. Save it as docker-compose.yml in an empty directory and run docker compose up -d from there.

services:
  cooktrace:
    image: ghcr.io/traceapps/cooktrace:1
    container_name: cooktrace
    ports:
      - "3000:3001"
    volumes:
      - ./data/db:/data/db
      - ./data/uploads:/data/uploads
    environment:
      DB_PATH: /data/db/cooktrace.db
      UPLOADS_PATH: /data/uploads
      JWT_SECRET: change-me-to-a-long-random-string
    env_file: .env
    restart: unless-stopped

Container listens on 3001, exposed on host port 3000. Open http://localhost:3000 after the container is up.

services:
  lifttrace:
    image: ghcr.io/traceapps/lifttrace:1
    container_name: lifttrace
    ports:
      - "3002:3003"
    volumes:
      - ./data/db:/data/db
      - ./data/uploads:/data/uploads
    environment:
      DB_PATH: /data/db/lifttrace.db
      UPLOADS_PATH: /data/uploads
      JWT_SECRET: change-me-to-a-long-random-string
    restart: unless-stopped

Container listens on 3003, exposed on host port 3002. Open http://localhost:3002 after the container is up.

services:
  nutritrace:
    image: ghcr.io/traceapps/nutritrace:1
    container_name: nutritrace
    ports:
      - "3000:3001"
    volumes:
      - ./data/db:/data/db
      - ./data/uploads:/data/uploads
    environment:
      DB_PATH: /data/db/nutritrace.db
      UPLOADS_PATH: /data/uploads
      JWT_SECRET: change-me-to-a-long-random-string
    env_file: .env
    restart: unless-stopped

Container listens on 3001, exposed on host port 3000. Open http://localhost:3000 after the container is up. NutriTrace's image is node:20-slim (Debian) rather than Alpine because DuckDB's node bindings need glibc.

Don't run all three on the same host port

CookTrace and NutriTrace both default to host port 3000. If you're running both on one host, change one of the left-hand port values (e.g. "3010:3001" for CookTrace) or put both behind a reverse proxy.

Picking an image tag

Each release publishes to several tags at once. Pin to whatever risk level fits.

Tag Updates when Use case
:1.2.3 Never (exact pin) Reproducible pin to one release
:1.2 Any 1.2.x patch Auto-receive bug fixes, no new features
:1 Any 1.x.y minor Auto-receive minor releases within the major
:latest Every stable release Whatever the newest stable happens to be
:dev Every push to dev branch Leading edge, not for production

The default compose files above use :1 because that's the sweet spot for most self-hosters: patch and minor updates land automatically, breaking changes never do. Skittish? Pin to :1.2 or a full :1.2.3. Adventurous? Use :dev and read the CHANGELOG before each pull.

More detail in Docker image tag matrix.

Ports and volumes at a glance

App Container port Suggested host port DB volume Uploads volume
CookTrace 3001 3000 /data/db /data/uploads
LiftTrace 3003 3002 /data/db /data/uploads
NutriTrace 3001 3000 /data/db /data/uploads

Both volumes are plain bind mounts. Back them up with the same tool you use for the rest of your host (rsync, restic, borg, whatever).

Using an .env file

Anything past the required trio (DB_PATH, UPLOADS_PATH, JWT_SECRET) is easier to keep out of the compose file. Create .env next to docker-compose.yml:

JWT_SECRET=paste-openssl-rand-base64-48-output-here
TOKEN_ENC_KEY=paste-another-openssl-rand-base64-48-here
RECOVERY_TOKEN=one-more-random-string

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=user@example.com
SMTP_PASS=your-smtp-password
SMTP_FROM="TraceApps" <noreply@example.com>

CookTrace and NutriTrace include env_file: .env in their compose examples so every variable in .env reaches the container. LiftTrace works the same way once you add the env_file: line, or you can enumerate variables under environment:. See Environment reference for the full list.

For values you don't want on disk in plaintext (SMTP passwords, AI keys, OIDC client secrets), the container also accepts any variable name suffixed with _FILE. See Docker Secrets.

Healthcheck

None of the three images include a built-in healthcheck. If you want one, add it yourself:

    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Adjust the port to 3003 for LiftTrace. The /api/health endpoint returns 200 OK with a short JSON body once the app finishes booting.

First-boot verification

After docker compose up -d, tail the container log until it stops moving:

docker compose logs -f

You should see the app announce its listen port, run migrations, then go quiet. Open the app in a browser at the host port from the table above. First screen is the setup wizard, covered in First-run wizard.

If the browser loads but login fails silently over plain HTTP, jump to Plain-HTTP LAN (INSECURE_COOKIES). That's the single most common first-time issue.