Deploy TanStack Start on Your VPS with Dokploy - Complete Guide

Learn how to deploy TanStack Start applications with Drizzle ORM and PostgreSQL on your own VPS using Dokploy. Full tutorial from setup to production.

Deploy TanStack Start on Your VPS with Dokploy - Complete Guide

Table of Contents

TanStack Start is a powerful full-stack React framework that brings server-side rendering, type-safe APIs, and modern development patterns to your React applications. While deploying to platforms like Vercel or Netlify is convenient, self-hosting on your own VPS gives you complete control over your infrastructure, better cost predictability, and no platform limitations.

In this comprehensive guide, you’ll learn how to deploy a TanStack Start application with Drizzle ORM and PostgreSQL on your own VPS using Dokploy. We’ll walk through every step from creating your application to deploying it in production, explaining each command and configuration along the way.

Why Self-Host with Dokploy?

Before diving into the tutorial, here’s why this setup is powerful:

  • Full Control: Own your infrastructure and data without vendor lock-in
  • Cost Effective: Predictable monthly costs instead of pay-per-request
  • No Limits: No execution time limits, bandwidth restrictions, or function size constraints
  • Database Included: Managed PostgreSQL 17 with automatic backups
  • Easy Deployment: Git push to deploy, just like Vercel or Heroku
  • Modern Stack: TanStack Start + Drizzle ORM for type-safe, full-stack development

Prerequisite: Install Dokploy

Before starting, make sure your Dokploy server is installed and configured. Follow the complete setup guide: Dokploy Install – Ditch Vercel/Heroku and Self-Host Your SaaS. This includes VPS setup, security hardening with CrowdSec, and Dokploy installation.

What You’ll Need

  • GitHub Account: For hosting your code repository
  • VPS with Dokploy: A server with Dokploy installed (follow the link above)
  • Domain/Subdomain: Pointed to your VPS IP address
  • Node.js 20+: Installed on your local development machine
  • Basic Terminal Knowledge: Familiarity with command-line operations

Step 1: Set Up Your GitHub Account

If you don’t already have a GitHub account, create one at github.com. You’ll use this to host your application code and enable automatic deployments.

For secure authentication, set up SSH keys for GitHub:

# Generate a new SSH key (if you don't have one)
ssh-keygen -t ed25519 -C "[email protected]"

# Start the ssh-agent
eval "$(ssh-agent -s)"

# Add your SSH key to the agent
ssh-add ~/.ssh/id_ed25519

# Copy your public key
cat ~/.ssh/id_ed25519.pub

What these commands do:

  • ssh-keygen creates a new SSH key pair using the Ed25519 algorithm (more secure and faster than RSA)
  • ssh-agent manages your SSH keys in the background
  • ssh-add adds your private key to the agent so you don’t need to enter your passphrase repeatedly
  • cat displays your public key, which you’ll add to GitHub under Settings → SSH and GPG keys

Step 2: Create a GitHub Repository

Create a new repository on GitHub for your TanStack Start application:

  1. Go to github.com/new
  2. Name your repository (e.g., tanstack-dokploy-app)
  3. Keep it private or public (your choice)
  4. Don’t initialize with README, .gitignore, or license (we’ll create these locally)
  5. Click “Create repository”

Keep the repository URL handy - you’ll need it later to push your code.


Step 3: Provision PostgreSQL 17 in Dokploy

Before creating the application, let’s set up the database. This approach ensures your database is ready when you need it during development.

Create the Database in Dokploy

  1. Access Dokploy Dashboard: Navigate to your Dokploy URL (e.g., https://app.yourdomain.com)

  2. Navigate to Databases: Click on “Databases” in the sidebar

  3. Create New Database: Click “New Database” → Select “PostgreSQL”

  4. Configure Database:

    • Version: Select 17 (latest stable version)
    • Database Name: saas-app (or your preferred name)
    • Username: saas-app (matches the database name for simplicity)
    • Password: Generate a strong password (Dokploy can generate one for you)
    • Enable External Port: Check this option
    • External Port: Set to 5432 (PostgreSQL default port)
  5. Create: Click create and wait for the database to provision

You’ll receive a connection string that looks like this:

postgresql://saas-app:[email protected]:5432/saas-app

Connection String Breakdown:

  • postgresql:// - Protocol
  • saas-app: - Username
  • 123FFF4545FFFaaaa@ - Password
  • 91.98.95.196: - Your VPS IP address
  • 5432/ - PostgreSQL port
  • saas-app - Database name

Open Port 5432 in Your Firewall

If you followed the Dokploy installation guide and set up iptables with CrowdSec, you need to allow PostgreSQL connections:

# SSH into your VPS
ssh your-username@your-vps-ip

# Allow PostgreSQL port
sudo iptables -A INPUT -p tcp --dport 5432 -j ACCEPT

# Make the rule persistent
sudo netfilter-persistent save

What these commands do:

  • iptables -A INPUT adds a new rule to the INPUT chain (incoming traffic)
  • -p tcp specifies the TCP protocol
  • --dport 5432 targets port 5432 (PostgreSQL’s default port)
  • -j ACCEPT accepts/allows the traffic
  • netfilter-persistent save writes the rule to disk so it survives reboots

Security Note: Opening port 5432 to the internet allows external connections. For production:

  • Use internal Docker networking if your app runs on the same server
  • Restrict access to specific IP addresses using -s your.ip.address
  • Use connection pooling and SSL/TLS for production databases

Step 4: Create Your TanStack Start Application

Now let’s create the application using TanStack Start’s official scaffolding tool. This will set up a complete full-stack React application with all the modern tooling.

Initialize the Project

Run the following command in your terminal:

npm create @tanstack/start@latest

What this command does:

  • npm create is a shorthand for npx create-<package>
  • Downloads and runs the latest TanStack Start scaffolding tool
  • Uses @latest to ensure you get the newest version
  • Provides an interactive CLI to configure your project

Configuration Options

During setup, you’ll be prompted with several questions. Here are the recommended choices:

◇  What would you like to name your project?
│  tanstack-dokploy-test

◇  Would you like to use Tailwind CSS?
│  Yes

◇  Select toolchain
│  Biome

◇  What add-ons would you like for your project?
│  Drizzle, Shadcn, tRPC, Query

◇  Would you like any examples?
│  none

◇  Drizzle: Database Provider
│  PostgreSQL

Configuration Breakdown:

  • Project Name: tanstack-dokploy-test - This becomes your directory name
  • Tailwind CSS: Yes - Modern utility-first CSS framework for styling
  • Toolchain: Biome - Fast, modern linter and formatter (alternative to ESLint + Prettier)
  • Add-ons:
    • Drizzle: Type-safe ORM for database operations
    • Shadcn: Beautiful, accessible React components
    • tRPC: End-to-end type-safe APIs
    • Query: TanStack Query for data fetching and caching
  • Examples: none - Start with a clean slate
  • Database: PostgreSQL - Matches our Dokploy database
cd tanstack-dokploy-test

Start Development Server (Optional Test)

Before configuring the database, you can test that everything works:

npm run dev

What happens:

  • Vite starts a development server (usually on http://localhost:3000)
  • Hot Module Replacement (HMR) is enabled for instant updates
  • The app compiles and serves your React application
  • Press Ctrl+C to stop the server

Step 5: Configure Drizzle with Your Database

Now we’ll connect Drizzle ORM to your PostgreSQL database and set up the schema.

Create Environment File

Create a .env file in your project root:

touch .env

Add your database connection string:

# .env
DATABASE_URL="postgresql://saas-app:[email protected]:5432/saas-app"

# Some Drizzle CLI tools use this variable name
DRIZZLE_DATABASE_URL="${DATABASE_URL}"

Important: Replace the connection string with your actual credentials from Step 3.

Why two variables?

  • DATABASE_URL is the standard convention used by most tools
  • DRIZZLE_DATABASE_URL is specifically for Drizzle CLI commands
  • Setting both ensures compatibility

Add .env to .gitignore

Make sure your .env file is in .gitignore to avoid committing secrets:

echo ".env" >> .gitignore

Run Drizzle Commands

Now execute the Drizzle setup commands:

# Generate migrations and TypeScript types
npm run db:generate

# Push schema to database
npm run db:push

# Open Drizzle Studio (optional)
npm run db:studio

Command Explanations:

1. npm run db:generate

  • Analyzes your schema files (in src/db/schema.ts or similar)
  • Generates SQL migration files
  • Creates TypeScript types for type-safe queries
  • Outputs to drizzle/ directory

2. npm run db:push

  • Reads your schema definitions
  • Connects to your PostgreSQL database
  • Creates tables, columns, indexes, and constraints
  • Syncs your database structure with your code
  • Idempotent: Safe to run multiple times

3. npm run db:studio

  • Starts Drizzle Studio on http://localhost:4983
  • Visual database browser in your web browser
  • Lets you view/edit data, inspect schema, run queries
  • Great for development and debugging

Troubleshooting Database Connection

If db:push fails, check:

  1. Connection String: Verify credentials and IP address
  2. Firewall: Ensure port 5432 is open (sudo iptables -L | grep 5432)
  3. Database Running: Check Dokploy dashboard that PostgreSQL is active
  4. Network: Test connection with telnet your-vps-ip 5432

Step 6: Prepare for Production with Nitro

TanStack Start needs a server runtime for production. We’ll use Nitro with Node.js preset, which Dokploy can easily run.

6.1 Install Nitro V2 Plugin

npm install @tanstack/nitro-v2-vite-plugin

What is Nitro?

  • Universal server framework by Unjs
  • Supports multiple platforms (Node.js, Cloudflare Workers, Vercel, etc.)
  • Handles SSR, API routes, and asset serving
  • Optimizes bundle size and performance

6.2 Configure Vite with Nitro

Open vite.config.ts and update it with the Nitro plugin:

// vite.config.ts
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import viteTsConfigPaths from "vite-tsconfig-paths";
import tailwindcss from "@tailwindcss/vite";
import { nitroV2Plugin } from "@tanstack/nitro-v2-vite-plugin";

const config = defineConfig({
  plugins: [
    // Enables TypeScript path aliases from tsconfig.json
    // Allows imports like @/components instead of ../../components
    viteTsConfigPaths({
      projects: ["./tsconfig.json"],
    }),

    // Tailwind CSS with Vite integration
    tailwindcss(),

    // TanStack Start core plugin - handles routing, SSR, etc.
    tanstackStart(),

    // Nitro plugin for production server
    // preset: "node-server" creates a Node.js compatible server
    nitroV2Plugin({
      preset: "node-server",
    }),

    // React plugin for Fast Refresh and JSX transformation
    viteReact(),
  ],
});

export default config;

Configuration Breakdown:

  • viteTsConfigPaths: Maps TypeScript path aliases (@/components) to actual file paths
  • tailwindcss: Processes Tailwind utilities at build time
  • tanstackStart: Core plugin that enables routing, SSR, and server functions
  • nitroV2Plugin: Builds the production server with preset: "node-server"
  • viteReact: Adds React Fast Refresh and JSX support

Output: After build, Nitro generates .output/server/index.mjs - a standalone Node.js server.

6.3 Update package.json Scripts

Ensure your package.json has the correct build and start commands:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}

Script Purposes:

  • dev: Runs Vite development server with HMR
  • build: Creates production-optimized build in .output/
  • start: Runs the production Node.js server

6.4 Remove package-lock.json (Optional)

Dokploy uses Nixpacks or Railpacks for builds, which auto-detects package managers. To ensure it uses npm consistently:

rm -f package-lock.json

Why remove it?

  • If you have multiple lockfiles (pnpm-lock.yaml, yarn.lock, package-lock.json), Nixpacks might get confused
  • Removing package-lock.json forces npm to be used
  • You can regenerate it with npm install

Note: This step is optional if you’re only using npm and not mixing package managers.


Step 7: Push Your Code to GitHub

Now that your application is ready, let’s push it to GitHub.

Initialize Git and Commit

# Stage all files for commit
git add -A

# Create initial commit
git commit -m "first commit"

# Rename branch to main (GitHub's default)
git branch -M main

# Add your GitHub repository as remote
git remote add origin [email protected]:yourusername/tanstack-dokploy-test.git

# Push to GitHub
git push -u origin main

Command Explanations:

1. git add -A

  • Stages all changes (new files, modifications, deletions)
  • -A is shorthand for --all
  • Prepares files for the commit

2. git commit -m "first commit"

  • Creates a commit with the message “first commit”
  • -m flag allows inline message instead of opening an editor
  • Commits the staged changes to your local repository

3. git branch -M main

  • Renames current branch to main
  • -M forces the rename even if branch exists
  • Aligns with GitHub’s default branch name

4. git remote add origin [email protected]:yourusername/tanstack-dokploy-test.git

  • Adds GitHub repository as remote named “origin”
  • [email protected]: uses SSH authentication
  • Replace yourusername/tanstack-dokploy-test with your actual repository

5. git push -u origin main

  • Pushes local main branch to origin remote
  • -u (or --set-upstream) sets up tracking
  • Future pushes can use just git push

Troubleshooting Git Push

SSH Authentication Failed:

# Use HTTPS instead
git remote set-url origin https://github.com/yourusername/tanstack-dokploy-test.git

Permission Denied:

  • Verify SSH key is added to GitHub (Settings → SSH and GPG keys)
  • Test SSH: ssh -T [email protected]

Step 8: Deploy to Dokploy with Nixpacks or Railpacks

Now for the exciting part - deploying your application to production!

8.1 Connect GitHub to Dokploy

  1. Navigate to Providers: In Dokploy dashboard, go to “Providers” → “Git”
  2. Connect GitHub:
    • Click “Connect GitHub”
    • Choose GitHub App (recommended) or Personal Access Token
    • Authorize Dokploy to access your repositories
  3. Verify Connection: You should see your repositories listed

8.2 Create New Application

  1. Go to Applications: Click “Applications” in the sidebar
  2. New Application: Click “New Application” → “From Git”
  3. Select Repository: Choose yourusername/tanstack-dokploy-test
  4. Configure Build Settings:

Basic Settings:

  • Name: tanstack-dokploy-app
  • Branch: main
  • Builder: Select Railpacks
  • Root Directory: ./ (project root)

Build Configuration:

  • Build Command: npm run build
  • Start Command: npm start
  • Internal Port: 3000

What is Railpacks?

  • Railway’s open-source build system
  • Auto-detects languages and frameworks
  • Generates optimized Docker images
  • Alternative to Dockerfile/Buildpacks

8.3 Configure Environment Variables

Add the following environment variables in Dokploy:

DATABASE_URL=postgresql://saas-app:[email protected]:5432/saas-app
DRIZZLE_DATABASE_URL=postgresql://saas-app:[email protected]:5432/saas-app
NODE_ENV=production
PORT=3000

Environment Variable Purposes:

  • DATABASE_URL: Connection string for your application to connect to PostgreSQL
  • DRIZZLE_DATABASE_URL: Used by Drizzle CLI tools (for migrations if needed)
  • NODE_ENV: Tells Node.js this is production (enables optimizations)
  • PORT: Port your app listens on (must match Internal Port setting)

8.4 Add Domain

  1. Navigate to Domains: In your application settings, find “Domains”
  2. Add Domain: Enter your domain (e.g., app.yourdomain.com)
  3. Enable HTTPS: Dokploy automatically provisions Let’s Encrypt SSL certificates
  4. DNS Verification: Ensure your domain’s A record points to your VPS IP

Traefik Handles:

  • Automatic HTTPS with Let’s Encrypt
  • Reverse proxy to your application
  • Load balancing (if you scale)

8.5 Deploy

  1. Click Deploy: Trigger the initial deployment
  2. Monitor Logs: Watch the build process in real-time
  3. Wait for Completion: First build takes 2-5 minutes

Build Process Steps:

  1. Dokploy clones your GitHub repository
  2. Nixpacks detects Node.js and installs dependencies
  3. Runs npm run build to create production bundle
  4. Creates Docker image with Node.js runtime
  5. Starts container with npm start
  6. Traefik routes traffic to your container

8.6 Run Database Migrations (First Deploy Only)

For the first deployment, you need to push your schema to production:

Option 1: Local Push

# From your local machine, use production DATABASE_URL
DATABASE_URL="postgresql://saas-app:[email protected]:5432/saas-app" npm run db:push

Option 2: In Dokploy

  • Navigate to your app’s “Console” tab
  • Run: npm run db:push

Option 3: Pre-deploy Hook (Advanced)

  • Add a pre-deploy script in Dokploy settings
  • Automatically runs migrations before each deployment

How It All Works Together

Let’s break down the deployment architecture:

Request Flow

  1. User visits https://app.yourdomain.com
  2. DNS resolves to your VPS IP address
  3. Traefik receives the HTTPS request (port 443)
  4. SSL termination - Traefik decrypts with Let’s Encrypt certificate
  5. Route matching - Traefik forwards to your container on port 3000
  6. TanStack Start handles the request:
    • Server-side rendering for initial page load
    • API routes for data fetching
    • tRPC for type-safe API calls
  7. Drizzle ORM queries PostgreSQL for data
  8. Response sent back through Traefik to user

Component Responsibilities

  • Dokploy: Orchestration, deployments, database management
  • Railpacks: Builds your application into a Docker image
  • Docker: Isolates your application in containers
  • Traefik: Reverse proxy, HTTPS, routing
  • PostgreSQL 17: Data persistence
  • GitHub: Source control and deployment trigger

Troubleshooting Common Issues

Build Fails

Error: Cannot find module

# Ensure all dependencies are in package.json
npm install --save <missing-package>
git commit -am "Add missing dependency"
git push

Error: Node version mismatch

// Add to package.json
{
  "engines": {
    "node": ">=20.0.0"
  }
}

Or add environment variable in Dokploy:

NODE_VERSION=20

Application Won’t Start

Error: Port already in use

  • Verify PORT=3000 in environment variables
  • Check Internal Port is set to 3000 in Dokploy

Error: Connection refused

  • Check container logs in Dokploy
  • Verify npm start command is correct
  • Ensure .output/server/index.mjs exists after build

Database Connection Issues

Error: Connection timeout

# Verify firewall on VPS
sudo iptables -L | grep 5432

# Test connection from your container
# In Dokploy console:
telnet 91.98.95.196 5432

Error: Authentication failed

  • Double-check DATABASE_URL credentials
  • Verify database user exists in Dokploy
  • Check for special characters in password (URL encode if needed)

SSL Certificate Issues

Error: Certificate provisioning failed

  • Ensure DNS A record points to VPS IP
  • Wait 5-10 minutes for DNS propagation
  • Check Traefik logs in Dokploy
  • Verify port 80 and 443 are open

Performance Optimization Tips

1. Database Connection Pooling

For production, use connection pooling to handle concurrent requests:

// src/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Maximum pool size
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

export const db = drizzle(pool);

2. Use Internal Networking

Instead of connecting via public IP, use Docker internal networking:

# If database is on same server, use Docker network name
DATABASE_URL=postgresql://saas-app:password@postgres-service:5432/saas-app

This is faster and doesn’t expose database port publicly.

3. Enable Caching

Add caching headers for static assets:

// In your server configuration
app.use('/_build/*', (req, res, next) => {
  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
  next();
});

4. Monitor Resources

Use Dokploy’s monitoring to track:

  • CPU usage
  • Memory consumption
  • Response times
  • Database connections

Security Best Practices

1. Restrict Database Access

Only allow connections from your VPS:

# Replace with your VPS internal IP
sudo iptables -I INPUT -p tcp --dport 5432 -s 10.0.0.0/8 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 5432 -j DROP
sudo netfilter-persistent save

2. Use Environment Variables

Never hardcode secrets:

// ❌ Bad
const apiKey = "sk_live_abc123";

// ✅ Good
const apiKey = process.env.API_KEY;

3. Enable CORS Properly

Restrict API access to your domain:

// src/server.ts
app.use(cors({
  origin: process.env.FRONTEND_URL,
  credentials: true,
}));

4. Regular Updates

Keep dependencies updated:

# Check for outdated packages
npm outdated

# Update packages
npm update

# For major version updates
npx npm-check-updates -u
npm install

Conclusions

You’ve successfully deployed a modern, full-stack TanStack Start application on your own VPS using Dokploy! This setup gives you:

Complete Control: Your infrastructure, your rules

Modern Stack: TanStack Start, Drizzle ORM, PostgreSQL 17

Type Safety: End-to-end TypeScript with tRPC

Production Ready: HTTPS, monitoring, backups

Cost Effective: Fixed monthly VPS cost

Scalable: Easy to add more resources or containers

This deployment approach combines the convenience of platforms like Vercel with the control and cost-effectiveness of self-hosting. You’re no longer limited by serverless function timeouts, bandwidth costs, or vendor lock-in.

Getting Started with Dokploy

If you haven’t set up Dokploy yet, start here:

Install Dokploy Guide

The installation guide covers everything from VPS setup to security hardening with CrowdSec, ensuring your self-hosted infrastructure is production-ready.

Happy deploying! 🚀

Related Posts