Deployment

Production self-hosting guide for taskpapr.


Table of contents

  1. Overview
  2. Prerequisites
  3. Server setup
    1. Install Node.js 22.5+
    2. Create a system user
    3. Clone the repository
  4. Environment variables
  5. systemd service
  6. Traefik configuration
  7. Verify deployment
  8. Logs
  9. Database backup
  10. PostgreSQL (optional)
    1. Setting up PostgreSQL
    2. PostgreSQL backup
  11. Upgrading
  12. Docker in production
  13. Troubleshooting
    1. Service won’t start
    2. Traefik can’t reach taskpapr
    3. “No route to host” errors
  14. Security notes
  15. Next steps

Overview

This guide walks through deploying taskpapr in production on a Linux server. The reference architecture uses:

  • EC2 (or any Linux VPS)
  • systemd (process management + auto-restart)
  • Traefik (HTTPS reverse proxy)
  • SQLite (single-file database)

The same approach works on DigitalOcean, Linode, Hetzner, or any provider where you have root access.


Prerequisites

Before starting, you need:

  1. A Linux server with root/sudo access
  2. A domain name pointing to your server’s IP address
  3. SSH access configured
  4. Traefik already running on the server (or install it alongside taskpapr)

For DNS, add an A record:

taskpapr.yourdomain.com  →  <your server's public IP>

For Traefik, see Traefik’s getting started guide if you don’t have it installed yet.


Server setup

SSH into your server and follow these steps.

Install Node.js 22.5+

# Install Node.js 22.x via NodeSource
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

# Verify
node --version  # should show v22.5.0 or higher

Create a system user

sudo useradd -r -m -d /opt/taskpapr -s /bin/bash taskpapr

This creates a dedicated user with a home directory at /opt/taskpapr.

Clone the repository

sudo su - taskpapr
git clone https://github.com/taskpapr/taskpapr.git /opt/taskpapr
cd /opt/taskpapr
npm install --production
exit

Environment variables

Create a .env file with your production configuration:

sudo nano /opt/taskpapr/.env

Paste (filling in your values):

# Required
SESSION_SECRET=<generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))">
NODE_ENV=production
PORT=3033

# Database
DB_PATH=/opt/taskpapr/data/taskpapr.db

# GitHub OAuth (if using)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_CALLBACK_URL=https://taskpapr.yourdomain.com/auth/github/callback

# OIDC / SSO (if using)
OIDC_ISSUER=https://auth.yourdomain.com/application/o/taskpapr/
OIDC_CLIENT_ID=your_oidc_client_id
OIDC_CLIENT_SECRET=your_oidc_client_secret
OIDC_CALLBACK_URL=https://taskpapr.yourdomain.com/auth/oidc/callback
OIDC_TRUST_IDP=true

Set permissions:

sudo chmod 600 /opt/taskpapr/.env
sudo chown taskpapr:taskpapr /opt/taskpapr/.env

See Authentication for the full .env reference.


systemd service

Create a systemd unit file to manage the taskpapr process:

sudo nano /etc/systemd/system/taskpapr.service

Paste:

[Unit]
Description=taskpapr — minimal task board
After=network.target

[Service]
Type=simple
User=taskpapr
WorkingDirectory=/opt/taskpapr
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/taskpapr/data

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable taskpapr
sudo systemctl start taskpapr
sudo systemctl status taskpapr

The service will start automatically on boot and restart if it crashes.


Traefik configuration

taskpapr needs to be exposed via Traefik for HTTPS.

Create a dynamic configuration file for Traefik’s file provider:

sudo nano /etc/traefik/dynamic/taskpapr.yaml

Paste:

http:
  routers:
    taskpapr:
      rule: "Host(`taskpapr.yourdomain.com`)"
      service: taskpapr
      entryPoints:
        - websecure
      tls:
        certResolver: letsencrypt

  services:
    taskpapr:
      loadBalancer:
        servers:
          - url: "http://localhost:3033"

Traefik will detect the file automatically (if file provider watch is enabled). No restart needed.

If you use a different Traefik setup (e.g., Docker labels), adapt accordingly. The key is to route taskpapr.yourdomain.comhttp://localhost:3033.


Verify deployment

Visit https://taskpapr.yourdomain.com in your browser.

  • If you configured auth, you should see the login page.
  • If you left auth variables empty, you should see the board immediately (single-user mode).

The first user to log in becomes the admin and can manage the whitelist from /admin.


Logs

View service logs:

# Live stream
sudo journalctl -u taskpapr -f

# Last 100 lines
sudo journalctl -u taskpapr -n 100

# Logs from the last hour
sudo journalctl -u taskpapr --since "1 hour ago"

Database backup

The database is a single SQLite file at /opt/taskpapr/data/taskpapr.db.

Back it up by copying it:

sudo cp /opt/taskpapr/data/taskpapr.db /opt/taskpapr/data/taskpapr.db.backup

For automated backups, add a cron job:

sudo crontab -e

Add:

# Daily backup at 3am
0 3 * * * cp /opt/taskpapr/data/taskpapr.db /opt/taskpapr/data/taskpapr-$(date +\%Y\%m\%d).db

SQLite databases can be copied while the app is running, but for critical production use, consider stopping the service first or using sqlite3 .backup.


PostgreSQL (optional)

By default, taskpapr uses SQLite — a single file, zero configuration, no database server needed. This is the right choice for most self-hosted deployments.

PostgreSQL is available as an alternative for deployments that need it — e.g. a managed database, PlanetScale, or a hosted environment where SQLite file persistence isn’t available.

Setting up PostgreSQL

  1. Create a PostgreSQL database and user
  2. Set DATABASE_URL in your .env:
DATABASE_URL=postgresql://taskpapr:yourpassword@localhost:5432/taskpapr
  1. Run the schema bootstrap once:
node db-migrate.js
  1. Start taskpapr normally. When DATABASE_URL is set, PostgreSQL is used automatically. When it’s unset, SQLite is used.

Existing SQLite users do not need to do anything. DATABASE_URL is unset by default and SQLite behaviour is unchanged.

PostgreSQL backup

Use pg_dump:

pg_dump -U taskpapr taskpapr > taskpapr-$(date +%Y%m%d).sql

Upgrading

When a new version is released:

sudo su - taskpapr
cd /opt/taskpapr
git pull
npm ci --production
exit

sudo systemctl restart taskpapr

Check the logs to confirm the new version started successfully:

sudo journalctl -u taskpapr -n 50

Docker in production

If you prefer Docker, use the official image:

docker run -d \
  --name taskpapr \
  --restart unless-stopped \
  -p 3033:3033 \
  -v taskpapr-data:/data \
  -e DB_PATH=/data/taskpapr.db \
  -e SESSION_SECRET=<generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"> \
  -e NODE_ENV=production \
  ghcr.io/taskpapr/taskpapr:latest

Or use docker-compose.yml:

version: '3.8'

services:
  taskpapr:
    image: ghcr.io/taskpapr/taskpapr:latest
    container_name: taskpapr
    restart: unless-stopped
    ports:
      - "3033:3033"
    volumes:
      - taskpapr-data:/data
    environment:
      DB_PATH: /data/taskpapr.db
      SESSION_SECRET: <generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))">
      NODE_ENV: production
      # Add auth variables as needed (see Authentication docs)

volumes:
  taskpapr-data:

Start:

docker compose up -d

Update:

docker compose pull
docker compose up -d

Data persists in the taskpapr-data volume. To back up:

docker run --rm -v taskpapr-data:/data -v $(pwd):/backup alpine tar czf /backup/taskpapr-backup.tar.gz -C /data .

Troubleshooting

Service won’t start

Check logs:

sudo journalctl -u taskpapr -n 50

Common issues:

  • Missing .env file or incorrect permissions
  • Port 3033 already in use (check with sudo lsof -i :3033)
  • Node.js version < 22.5.0

Traefik can’t reach taskpapr

Verify taskpapr is listening:

curl http://localhost:3033

If that works but Traefik can’t reach it, check:

  • Traefik dynamic config file syntax
  • Traefik logs: sudo docker logs traefik (if running Traefik in Docker)

“No route to host” errors

Check firewall rules:

sudo ufw status

Ensure port 80 and 443 are open for Traefik. Port 3033 should NOT be open to the internet — only Traefik needs to reach it internally.


Security notes

  • Always use HTTPS in production (Traefik + Let’s Encrypt handles this)
  • Keep SESSION_SECRET secret — do not commit it to git
  • Use OIDC_TRUST_IDP=true when using your own IdP (e.g. Authentik) to skip redundant whitelist checks
  • Run taskpapr as a non-root user (the systemd unit file does this)
  • Restrict file permissions on .env (chmod 600)

For hardening a public-facing deployment (Cloudflare, Traefik rate limiting, IP allowlisting, resource quotas), see the Security Reference.


Next steps