Skip to content

Docker Compose

The recommended way to run Punchlist. All examples are in examples/docker.

SQLite (the default)

One container, data in a named volume. Good for a household or a small team: SQLite handles many people comfortably.

compose.yaml
# Punchlist with SQLite (the default): one container, data in a named volume.
#   docker compose up -d
#   docker compose logs punchlist      # the first admin password is printed here, once
# Then open http://localhost:8080. Docs: docs/install/docker-compose.md

services:
  punchlist:
    image: registry.gitlab.com/killswitch10/punchlist:latest
    container_name: punchlist
    restart: unless-stopped
    ports:
      - '8080:8080'
    volumes:
      - punchlist-data:/data
      # Attached files, apart from the database.
      - punchlist-files:/files
    environment:
      # Months in stats and "today" on the server follow this time zone.
      TZ: Etc/UTC
      # The address people open, once it is behind a reverse proxy with HTTPS.
      # PUBLIC_URL: https://tasks.example.com
      # Choose the first admin password instead of reading it from the logs (12+ characters).
      # ADMIN_PASSWORD_FILE: /run/secrets/admin_password

volumes:
  punchlist-data:
  punchlist-files:
docker compose up -d
docker compose logs punchlist   # the first admin password, printed once

Open http://localhost:8080. Read first start for what comes next.

Data in a folder instead of a volume

compose.bind-mount.yaml keeps the data in ./data and attached files in ./files (point that one at bigger storage if you like). Both folders must belong to uid 1654 (sudo chown 1654:1654 data files), or run the container as your own user with user:.

With HTTPS (Caddy)

Caddy gets and renews a certificate by itself. Point a DNS name at the machine, open ports 80 and 443, and put the name in both files.

# Punchlist behind Caddy with automatic HTTPS (Let's Encrypt).
# Point a DNS name at this machine, open ports 80 and 443, put the name in Caddyfile and below, then:
#   docker compose -f compose.caddy.yaml up -d
# Only Caddy is published; Punchlist is reachable on the compose network alone.

services:
  punchlist:
    image: registry.gitlab.com/killswitch10/punchlist:latest
    container_name: punchlist
    restart: unless-stopped
    volumes:
      - punchlist-data:/data
      # Attached files, apart from the database.
      - punchlist-files:/files
    environment:
      TZ: Etc/UTC
      PUBLIC_URL: https://tasks.example.com

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
      - '443:443/udp'
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config

volumes:
  punchlist-data:
  punchlist-files:
  caddy-data:
  caddy-config:
# Used by compose.caddy.yaml. Replace the name with yours.
# Punchlist sends its own security headers and compressed answers: nothing else is needed here.
tasks.example.com {
    reverse_proxy punchlist:8080
}

Behind Traefik instead: compose.traefik.yaml. Other proxies: reverse proxy and HTTPS.

PostgreSQL

For people who already run PostgreSQL or want the database on its own server. Make the secrets first (the commands are at the top of the file).

compose.postgres.yaml
# Punchlist with PostgreSQL 15+ instead of the built-in SQLite.
# Secrets come from files (every setting also works as NAME_FILE). Make them first:
#   mkdir -p secrets
#   openssl rand -hex 24 > secrets/db_password
#   printf 'Host=db;Username=punchlist;Password=%s;Database=punchlist' "$(cat secrets/db_password)" > secrets/db_connection
#   docker compose -f compose.postgres.yaml up -d

services:
  punchlist:
    image: registry.gitlab.com/killswitch10/punchlist:latest
    container_name: punchlist
    restart: unless-stopped
    ports:
      - '8080:8080'
    environment:
      TZ: Etc/UTC
      DB_PROVIDER: postgres
      DB_CONNECTION_STRING_FILE: /run/secrets/db_connection
    secrets:
      - db_connection
    volumes:
      # Snapshots, logs and audit archives still go to /data, attached files to /files.
      - punchlist-data:/data
      # Attached files, apart from the database.
      - punchlist-files:/files
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: punchlist
      POSTGRES_DB: punchlist
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - db-data:/var/lib/postgresql/data
    secrets:
      - db_password
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'punchlist']
      interval: 10s
      retries: 5

secrets:
  db_password:
    file: secrets/db_password
  db_connection:
    file: secrets/db_connection

volumes:
  punchlist-data:
  punchlist-files:
  db-data:

More in database, including moving an existing SQLite server to PostgreSQL.

Single sign-on

compose.oidc.yaml
# Punchlist with single sign-on through an OpenID Connect provider (Pocket ID here; any works).
# Register this redirect URI with the provider:  https://tasks.example.com/auth/oidc/pocket/callback
# Put the client secret in a file first:  mkdir -p secrets && echo 'the-secret' > secrets/oidc_secret
# Docs: docs/guides/single-sign-on.md

services:
  punchlist:
    image: registry.gitlab.com/killswitch10/punchlist:latest
    container_name: punchlist
    restart: unless-stopped
    ports:
      - '8080:8080'
    volumes:
      - punchlist-data:/data
      # Attached files, apart from the database.
      - punchlist-files:/files
    environment:
      TZ: Etc/UTC
      PUBLIC_URL: https://tasks.example.com
      OIDC_PROVIDERS: pocket
      OIDC_POCKET_AUTHORITY: https://id.example.com
      OIDC_POCKET_CLIENT_ID: punchlist
      OIDC_POCKET_CLIENT_SECRET_FILE: /run/secrets/oidc_secret
      OIDC_POCKET_DISPLAY_NAME: Pocket ID
      # Only members of this group may sign in; new ones get an account by themselves.
      OIDC_POCKET_REQUIRED_GROUPS: punchlist
      OIDC_POCKET_AUTO_CREATE: 'true'
      # Members of punchlist-admins become admins at every sign-in.
      OIDC_POCKET_ROLE_MAP: punchlist-admins=admin
      # No passwords at all, in the app or the API (punchlist admin login-link gets an admin back in).
      # LOCAL_LOGIN: disabled
    secrets:
      - oidc_secret

secrets:
  oidc_secret:
    file: secrets/oidc_secret

volumes:
  punchlist-data:
  punchlist-files:

Provider by provider: single sign-on.

Everyday commands

docker compose pull && docker compose up -d       # upgrade (read the changelog first)
docker compose logs -f punchlist                  # follow the logs
docker compose exec punchlist punchlist --version
docker compose exec punchlist punchlist snapshot  # a backup now
docker compose restart punchlist                  # after changing a setting that needs it

The Restart now button on the admin settings page stops the server; the restart: policy in these files brings it back. Without one, the server stays stopped.