Skip to main content

Self-Hosted Deployment

Deploy PUNT on your own infrastructure for full control.

Prerequisites

  • Node.js 20 or later
  • pnpm (or npm/yarn)
  • A server (VPS, dedicated, or local)
  • Optional: nginx or Caddy for reverse proxy

Installation

Clone Repository

git clone https://github.com/jmynes/punt.git
cd punt

Install Dependencies

pnpm install

The guided installer automates database setup, environment configuration, and admin user creation:

pnpm run setup

The installer will:

  1. Detect PostgreSQL and provide install instructions if missing
  2. Create the database user and database
  3. Generate .env with a secure AUTH_SECRET
  4. Run prisma db push to initialize the schema
  5. Create an admin user with your chosen credentials
tip

pnpm setup is a built-in pnpm command. Always use pnpm run setup to run the PUNT installer.

After setup completes, skip ahead to Build Application.

Manual Setup

If you prefer to configure manually:

Configure Environment

Create a .env file:

# Required
AUTH_SECRET=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/punt

# If behind reverse proxy
AUTH_TRUST_HOST=true
TRUST_PROXY=true

Generate AUTH_SECRET:

openssl rand -base64 32

Set Up Database

Install PostgreSQL 16+ and create a database and user:

sudo apt install postgresql
sudo -u postgres psql -c "CREATE USER punt WITH PASSWORD 'yourpassword' CREATEDB;"
sudo -u postgres psql -c "CREATE DATABASE punt OWNER punt;"

Update DATABASE_URL in your .env file to match your credentials, then initialize the schema:

pnpm db:push

Build Application

pnpm build

Start Server

pnpm start

PUNT is now running at http://localhost:3000.

Production Setup

Process Manager (PM2)

Use PM2 to keep PUNT running:

# Install PM2
npm install -g pm2

# Start PUNT
pm2 start pnpm --name punt -- start

# Save process list
pm2 save

# Enable startup script
pm2 startup

PM2 commands:

pm2 status       # View status
pm2 logs punt # View logs
pm2 restart punt # Restart
pm2 stop punt # Stop

Systemd Service

Alternative to PM2, use systemd:

Create /etc/systemd/system/punt.service:

[Unit]
Description=PUNT Ticketing System
After=network.target

[Service]
Type=simple
User=punt
WorkingDirectory=/opt/punt
ExecStart=/usr/bin/pnpm start
Restart=on-failure
RestartSec=10

# Environment
Environment=NODE_ENV=production
EnvironmentFile=/opt/punt/.env

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable punt
sudo systemctl start punt

Reverse Proxy

Nginx

Install nginx:

sudo apt install nginx

Create /etc/nginx/sites-available/punt:

server {
listen 80;
server_name punt.example.com;

# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name punt.example.com;

# SSL certificates (use certbot)
ssl_certificate /etc/letsencrypt/live/punt.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/punt.example.com/privkey.pem;

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

location / {
proxy_pass http://localhost:3000;
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;
proxy_cache_bypass $http_upgrade;

# SSE support
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
}

# File upload size
client_max_body_size 100M;
}

Enable site:

sudo ln -s /etc/nginx/sites-available/punt /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Caddy

Simpler alternative with automatic HTTPS:

Install Caddy:

sudo apt install -y caddy

Create /etc/caddy/Caddyfile:

punt.example.com {
reverse_proxy localhost:3000
}

Reload Caddy:

sudo systemctl reload caddy

Caddy automatically obtains and renews SSL certificates.

Subpath Deployment

To serve PUNT at a subpath (e.g., https://example.com/punt), set the NEXT_PUBLIC_BASE_PATH environment variable:

NEXT_PUBLIC_BASE_PATH=/punt
NEXT_PUBLIC_APP_URL=https://example.com

Then configure your reverse proxy to route the subpath:

Nginx:

location /punt {
proxy_pass http://localhost:3000;
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;
proxy_cache_bypass $http_upgrade;

# SSE support
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
}

Caddy:

example.com {
handle_path /punt/* {
reverse_proxy localhost:3000
}
}
note

When using basePath, MCP credentials must include the full URL with the subpath:

{
"url": "https://example.com/punt",
"apiKey": "mcp_xxxxx..."
}

SSL/HTTPS

Let's Encrypt with Certbot

# Install certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d punt.example.com

# Auto-renewal (usually configured automatically)
sudo certbot renew --dry-run

File Storage

Configure Upload Directory

By default, uploads go to ./uploads/. For production:

  1. Create dedicated directory:

    sudo mkdir -p /var/data/punt/uploads
    sudo chown punt:punt /var/data/punt/uploads
  2. Symlink or configure path (if needed in future releases)

Backup Uploads

Add to your backup script:

rsync -av /var/data/punt/uploads/ /backup/punt/uploads/

Database Backup

Automated Backups

Create /opt/punt/backup.sh:

#!/bin/bash
BACKUP_DIR=/backup/punt
DATE=$(date +%Y%m%d_%H%M%S)

# Create backup directory
mkdir -p $BACKUP_DIR

# Backup database
pg_dump $DATABASE_URL > $BACKUP_DIR/punt_$DATE.sql

# Backup uploads
tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz /var/data/punt/uploads/

# Keep last 7 days of backups
find $BACKUP_DIR -name "punt_*.sql" -mtime +7 -delete
find $BACKUP_DIR -name "uploads_*.tar.gz" -mtime +7 -delete

Schedule with cron:

crontab -e
# Add:
0 2 * * * /opt/punt/backup.sh

Restore from Backup

# Stop PUNT
sudo systemctl stop punt

# Restore database
psql $DATABASE_URL < /backup/punt/punt_YYYYMMDD.sql

# Restore uploads
tar -xzf /backup/punt/uploads_YYYYMMDD.tar.gz -C /

# Start PUNT
sudo systemctl start punt

Security Hardening

Firewall

# Allow only necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

Dedicated User

Run PUNT as a dedicated user:

# Create user
sudo useradd -r -s /bin/false punt

# Set ownership
sudo chown -R punt:punt /opt/punt
sudo chown -R punt:punt /var/data/punt

Environment Security

Protect the .env file:

chmod 600 /opt/punt/.env
chown punt:punt /opt/punt/.env

Updates

Update Process

# Stop PUNT
sudo systemctl stop punt

# Backup database
pg_dump $DATABASE_URL > /var/data/punt/punt_before_update.sql

# Pull updates
cd /opt/punt
git pull

# Install dependencies
pnpm install

# Run migrations
pnpm db:push

# Build
pnpm build

# Start PUNT
sudo systemctl start punt

Rollback

If an update fails:

# Stop PUNT
sudo systemctl stop punt

# Restore database
cp /var/data/punt/punt_before_update.db /var/data/punt/punt.db

# Checkout previous version
git checkout <previous-tag>

# Rebuild
pnpm install
pnpm build

# Start PUNT
sudo systemctl start punt

Monitoring

Log Rotation

Create /etc/logrotate.d/punt:

/var/log/punt/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 punt punt
sharedscripts
postrotate
systemctl reload punt >/dev/null 2>&1 || true
endscript
}

Health Monitoring

Simple health check script:

#!/bin/bash
if ! curl -sf http://localhost:3000/ > /dev/null; then
echo "PUNT is down!"
# Send alert (email, Slack, etc.)
fi

Add to cron for periodic checks:

*/5 * * * * /opt/punt/health-check.sh

Docker (Alternative)

If you prefer containerization, create a Dockerfile:

FROM node:20-alpine

WORKDIR /app

# Install pnpm
RUN npm install -g pnpm

# Copy package files
COPY package.json pnpm-lock.yaml ./

# Install dependencies
RUN pnpm install --frozen-lockfile

# Copy source
COPY . .

# Generate Prisma client
RUN pnpm db:generate

# Build
RUN pnpm build

# Expose port
EXPOSE 3000

# Start
CMD ["pnpm", "start"]

Build and run:

docker build -t punt .
docker run -d \
-p 3000:3000 \
-e AUTH_SECRET=your-secret \
-e DATABASE_URL=postgresql://user:password@your-db-host:5432/punt \
punt