DocsSelf-hosting

Running Spindle on your own server

Run Spindle from one Docker image and a Postgres database, with every feature for tracking and dividing time. Self-hosting is free.

On this page

Spindle runs on your own server from one Docker image, ghcr.io/threennovate/spindle, next to a PostgreSQL database. The image serves the web app and the API that the desktop and phone apps talk to. It is the same app as Spindle Cloud, and running it is free.

Note. Spindle is open source under AGPL-3.0. The code, including the Dockerfile the image is built from, is on GitHub: github.com/Threennovate/spindle.

What you need

  • A server with Docker and Docker Compose. A small one is enough to start: in our own deployment, Spindle uses about 90 MB of memory at rest and its database about 45 MB. Watch it under your own use before you settle on a size.
  • A domain name pointing at the server, such as time.example.com.
  • An SMTP server to send email from. Spindle sends invitations and sign-in emails, and it doesn’t start without one.

Start it

Put these two files in an empty folder on the server. The compose file runs Spindle, PostgreSQL 18, and Caddy in front of them for HTTPS.

compose.yaml

services:
  spindle:
    image: ghcr.io/threennovate/spindle:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__Default: Host=db;Port=5432;Database=spindle;Username=spindle;Password=${POSTGRES_PASSWORD}
      Auth__SigningKey: ${SIGNING_KEY}
      App__BaseUrl: https://${DOMAIN}
      AllowedHosts: ${DOMAIN};localhost;127.0.0.1
      ReverseProxy__KnownNetworks__0: 172.28.0.0/24
      Smtp__Host: ${SMTP_HOST}
      Smtp__Port: ${SMTP_PORT}
      Smtp__UserName: ${SMTP_USER}
      Smtp__Password: ${SMTP_PASSWORD}
      Smtp__FromAddress: ${SMTP_FROM}
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      start_period: 60s

  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: spindle
      POSTGRES_USER: spindle
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U spindle -d spindle"]
      interval: 5s
      timeout: 5s
      retries: 30

  caddy:
    image: caddy:2
    restart: unless-stopped
    command: caddy reverse-proxy --from ${DOMAIN} --to spindle:8080
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - caddy:/data

networks:
  default:
    ipam:
      config:
        - subnet: 172.28.0.0/24

volumes:
  db:
  caddy:

.env

# The address people open, without https://
DOMAIN=time.example.com

# Make each one with: openssl rand -hex 32
POSTGRES_PASSWORD=
SIGNING_KEY=

# The mail server Spindle sends email from
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=spindle@example.com
SMTP_PASSWORD=
SMTP_FROM=spindle@example.com

Fill in .env, then start it:

docker compose up -d

On its first start, Spindle creates its tables in the database. Caddy gets a certificate for your domain on its own, so give it a minute, then open https://time.example.com.

Create your account and close sign-ups

  1. Open your address and create an account. It comes with its own organization and workspace, and you are its admin.

  2. Close sign-ups, so strangers can’t create accounts on your server. Add this line under environment of the spindle service:

    Auth__AllowRegistration: "false"

    and apply it with docker compose up -d. From then on, people join only through an invitation.

  3. Invite your team from the workspace. Each person gets an email with a link.

Connect the apps

The web app is at your address. In the Mac, Windows and Android apps, choose Your own server on the sign-in screen and enter the same address. On your own server, the apps sign in with email and password; passkeys in the apps work only with Spindle Cloud. The iPhone app from the App Store connects only to Spindle Cloud for now; see below.

Suggestions from Claude Code and Codex work the same as on Spindle Cloud: the activity stays on each person’s computer, and with activity sync on, it goes to your server and nowhere else. See How suggestions are made.

Settings

Spindle reads its settings from environment variables. A double underscore separates the parts of a name.

Variable What it is
ConnectionStrings__Default The database, as Host=…;Port=5432;Database=…;Username=…;Password=…. It isn’t a postgres:// address. Use a password without ; in it, such as one from openssl rand -hex 32.
Auth__SigningKey A random secret of at least 32 characters that signs sign-ins. Keep it secret, and keep it the same between restarts.
App__BaseUrl The full address people open, with https://. Links in emails point here.
AllowedHosts The host names Spindle answers to, separated by ;. Keep localhost;127.0.0.1 for the container’s health check.
Auth__AllowRegistration true by default. With false, people join only through an invitation.
ReverseProxy__KnownNetworks__0 The network your HTTPS proxy connects from, such as 172.28.0.0/24. Spindle trusts the proxy’s forwarded address and scheme only from there. Add more with __1, __2.
Smtp__Host, Smtp__Port The mail server. The port is 587 if left out.
Smtp__UserName, Smtp__Password Sign-in for the mail server, if it needs one.
Smtp__Security Auto by default, which uses TLS when the server offers it. Also StartTls, SslOnConnect or None.
Smtp__FromAddress, Smtp__FromName Who the email comes from. The name is Spindle if left out.
Smtp__ReplyToAddress Where replies go, when nobody reads the sender address.
App__TermsUrl, App__PrivacyUrl, App__TermsVersion Optional. With a terms address, new accounts have to agree to your terms; the version is kept with each account.

Behind another proxy

Any HTTPS proxy works in place of Caddy, such as nginx, Traefik or the one in Coolify. It has to:

  • forward to port 8080 of the spindle container;
  • pass WebSockets through, which keep the apps in sync live;
  • send X-Forwarded-For and X-Forwarded-Proto, with its network in ReverseProxy__KnownNetworks__0.

Find the network of a Docker proxy with:

docker network inspect NETWORK --format '{{range .IPAM.Config}}{{.Subnet}} {{end}}'

Don’t use 0.0.0.0/0: Spindle would then trust any address a visitor claims.

Updating

docker compose pull
docker compose up -d

Spindle updates its database tables when it starts. Make a backup before you update.

Backups

Everything Spindle keeps is in the database. Back it up every day, and keep the copies away from the server:

docker compose exec -T db pg_dump -U spindle -d spindle -Fc > spindle-$(date +%F).dump

To restore, stop Spindle, restore the dump into an empty database, then start Spindle again:

docker compose stop spindle
docker compose exec -T db pg_restore -U spindle -d spindle --clean --if-exists --no-owner < spindle-2026-10-10.dump
docker compose start spindle

A backup you haven’t restored once is a guess, so try a restore into a spare database now and then.

Checking it

https://time.example.com/health answers Healthy when Spindle is running and can reach its database. Point your uptime monitor at it. To see what Spindle is doing:

docker compose logs -f spindle

Moving between Spindle Cloud and your own server

Export a detailed report as CSV from one, and import it into the other. Clients, projects, tasks, tags and time entries come along; people need to be invited first. See Importing.

How it differs from Spindle Cloud

Your own server has everything for tracking and dividing time: suggestions from Claude Code and Codex, splits, the MCP for agents, reports, exports, teams and the API. There is no trial, no plan page and no limit on people. You run the updates, the backups and the mail server; on Spindle Cloud, we do.

A few things depend on Spindle Cloud’s domain or on our credentials, and don’t work with your own server yet:

  • The iPhone app from the App Store connects only to Spindle Cloud for now. To use it with your server, build it yourself with your server’s address. The web app works on a phone in the meantime.
  • Passkeys in the apps. The released apps are signed for Spindle Cloud’s domain, so on your own server they sign in with email and password. Passkeys in the browser work on your server, over HTTPS.
  • Push notifications on iPhone need the key of the team that signed the app. If you build the iPhone app yourself, set Push__Apns__KeyId, Push__Apns__TeamId, Push__Apns__PrivateKey and Push__Apns__BundleId to use your own key.

Type to search every page of the docs.