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:
| Variable | Description |
|---|---|
NODE_ENV | Set to production |
APP_URL | Your public frontend URL (e.g. https://planner.tactihub.de). Must point to the frontend, not the API server. |
JWT_SECRET | A long random string (openssl rand -base64 48) |
JWT_REFRESH_SECRET | Another 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
.envanddocker-compose.ymlfor production!
Important: If you change
VITE_API_URLorVITE_SOCKET_URL, you must runpnpm buildagain — 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 443block, 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
| Port | Protocol | SMTP_SECURE |
|---|---|---|
| 587 | STARTTLS | false |
| 465 | SSL | true |
11. Google reCAPTCHA v2 (Optional)
- Go to Google reCAPTCHA Admin
- Select “Challenge (v2)” (DE: “Aufgabe (v2)”) as the type
- Select “I’m not a robot Checkbox” (DE: “Kästchen: Ich bin kein Roboter”)
- Add your domain
- Add the keys to
.env:
RECAPTCHA_SITE_KEY=6Le...your-site-key
RECAPTCHA_SECRET_KEY=6Le...your-secret-key
- 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:
| Field | Value |
|---|---|
| Username | admin |
admin@tactihub.local | |
| Password | changeme |
Important: Change the admin password after the first login.
Checklist
-
NODE_ENV=productionis set -
APP_URLpoints to the frontend - Secure JWT secrets generated
- Database password changed
- SMTP configured and tested
-
pnpm buildsuccessful - 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)