📦

Installation (Production)

Prerequisites

  • OS: Debian 13 (Trixie) recommended — also works on Ubuntu 22.04+, Debian 12, or any Linux with Docker support
  • Node.js >= 20.0 (LTS recommended)
  • pnpm >= 9.0 (npm install -g pnpm)
  • Docker + Docker Compose (for PostgreSQL and Redis)
  • Git
  • Nginx or Caddy as reverse proxy
  • A domain pointing to your server (e.g. planner.tactihub.de)

Recommended:

  • SMTP server or email service (e.g. Gmail App Password, Brevo, Mailgun) — users must verify their email after registration. Without SMTP, admins must manually verify every user.

Proxmox LXC: If you run TactiHub inside a Proxmox LXC container, make sure Nesting is enabled in the container features (Options > Features > Nesting). For unprivileged containers, also enable keyctl.


1. Install System Dependencies

# Update packages
apt update && apt upgrade -y

# Install essentials
apt install -y curl git ca-certificates gnupg nginx certbot python3-certbot-nginx

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

# Install pnpm
npm install -g pnpm

# Install Docker
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker

Verify:

node -v    # v22.x
pnpm -v    # 10.x
docker -v  # 27.x
nginx -v   # 1.26+

2. Clone the Repository

cd /opt
git clone https://github.com/niklask52t/TactiHub.git
cd TactiHub

3. Configure Environment Variables

cp .env.example .env
nano .env

Configure at minimum:

VariableDescription
NODE_ENVSet to production
APP_URLYour public frontend URL (e.g. https://planner.tactihub.de). Must point to the frontend, not the API server.
JWT_SECRETA long random string (openssl rand -base64 48)
JWT_REFRESH_SECRETAnother long random string
SMTP_*Your email server credentials

Full example:

NODE_ENV=production

APP_URL=https://planner.tactihub.de

JWT_SECRET=<openssl rand -base64 48>
JWT_REFRESH_SECRET=<openssl rand -base64 48>

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@example.com
SMTP_PASS=your-password
SMTP_FROM=noreply@yourdomain.com

# Optional
UPLOAD_DIR=uploads
MAX_FILE_SIZE=10485760
VITE_API_URL=https://planner.tactihub.de
VITE_SOCKET_URL=https://planner.tactihub.de

Important: Change the default database password in your .env and docker-compose.yml for production!

Important: If you change VITE_API_URL or VITE_SOCKET_URL, you must run pnpm build again — Vite bakes these values into the client at build time.


4. Start Infrastructure

docker compose up -d

This starts:

  • PostgreSQL 16 on port 5432
  • Redis 7 on port 6379

5. Install Dependencies & Build

pnpm install

# Generate migration files
pnpm db:generate

# Apply migrations
pnpm db:migrate

# Seed initial data
pnpm db:seed

# Build everything (shared → server → client)
pnpm build

The seed creates:

  • Admin account: admin / admin@tactihub.local / changeme
  • Rainbow Six Siege: 21 maps, 42 operators, 55 gadgets
  • Valorant: 4 maps, 11 agents, 40 abilities

6. Set Up systemd Service

Create a dedicated system user (no login shell, no home directory):

sudo useradd --system --no-create-home --shell /usr/sbin/nologin tactihub
sudo chown -R tactihub:tactihub /opt/TactiHub

Create a unit file for autostart:

sudo nano /etc/systemd/system/tactihub.service
[Unit]
Description=TactiHub Server
After=network.target docker.service
Requires=docker.service

[Service]
Type=simple
User=tactihub
WorkingDirectory=/opt/TactiHub
EnvironmentFile=/opt/TactiHub/.env
ExecStart=/usr/bin/node packages/server/dist/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Enable and start:

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

Useful commands:

sudo journalctl -u tactihub -f     # View logs
sudo systemctl restart tactihub     # Restart
sudo systemctl stop tactihub        # Stop

7. Nginx Reverse Proxy

sudo nano /etc/nginx/sites-available/tactihub
server {
    listen 80;
    server_name planner.tactihub.de;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name planner.tactihub.de;

    ssl_certificate /etc/letsencrypt/live/planner.tactihub.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/planner.tactihub.de/privkey.pem;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Max upload size
    client_max_body_size 10M;

    # API + Socket.IO to backend
    location /api/ {
        proxy_pass http://localhost:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /socket.io/ {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Uploads (operator/gadget/map images)
    location /uploads/ {
        proxy_pass http://localhost:3001;
        proxy_set_header Host $host;
    }

    # Static files from client build
    location / {
        root /opt/TactiHub/packages/client/dist;
        try_files $uri $uri/ /index.html;
    }
}

Enable:

sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/tactihub /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Caddy (Alternative)

planner.tactihub.de {
    handle /api/* {
        reverse_proxy localhost:3001
    }
    handle /socket.io/* {
        reverse_proxy localhost:3001
    }
    handle /uploads/* {
        reverse_proxy localhost:3001
    }
    handle {
        root * /opt/TactiHub/packages/client/dist
        try_files {path} /index.html
        file_server
    }
}

Caddy manages SSL certificates automatically.


8. SSL with Let’s Encrypt

sudo certbot --nginx -d planner.tactihub.de

Tip: If Certbot fails, temporarily comment out the listen 443 block, reload Nginx, and run Certbot again.

Test renewal:

sudo certbot renew --dry-run

9. Firewall (ufw)

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Port 3001 stays closed — only Nginx accesses it internally.


10. Configure SMTP

Without SMTP, admins must manually verify every new user (Admin Panel > Users > Verify).

Gmail

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-gmail@gmail.com
SMTP_PASS=your-app-password    # App Password, not your regular password!
SMTP_FROM=your-gmail@gmail.com

Port Configuration

PortProtocolSMTP_SECURE
587STARTTLSfalse
465SSLtrue

11. Google reCAPTCHA v2 (Optional)

  1. Go to Google reCAPTCHA Admin
  2. Select “Challenge (v2)” (DE: “Aufgabe (v2)”) as the type
  3. Select “I’m not a robot Checkbox” (DE: “Kästchen: Ich bin kein Roboter”)
  4. Add your domain
  5. Add the keys to .env:
RECAPTCHA_SITE_KEY=6Le...your-site-key
RECAPTCHA_SECRET_KEY=6Le...your-secret-key
  1. Restart TactiHub — the checkbox appears automatically

Without keys, registration works normally without reCAPTCHA.


12. Open the App & Verify

Navigate to https://planner.tactihub.de and log in with the default admin credentials:

FieldValue
Usernameadmin
Emailadmin@tactihub.local
Passwordchangeme

Important: Change the admin password after the first login.


Checklist

  • NODE_ENV=production is set
  • APP_URL points to the frontend
  • Secure JWT secrets generated
  • Database password changed
  • SMTP configured and tested
  • pnpm build successful
  • systemd service created and enabled
  • Nginx/Caddy reverse proxy configured
  • SSL certificates active (Let’s Encrypt)
  • Firewall (ufw) configured
  • Admin password changed
  • Socket.IO connects (check browser DevTools)
  • File uploads work (test in admin panel)
  • reCAPTCHA configured (optional)