---
title: "Deploy TanStack Start on Your VPS with Dokploy - Complete Guide"
description: "Learn how to deploy TanStack Start applications with Drizzle ORM and PostgreSQL on your own VPS using Dokploy. Full tutorial from setup to production."
date: 2025-10-29
categories: ["vps"]
tags: ["tanstack","dokploy","deployment"]
---

TanStack Start is a full-stack React framework that handles server-side rendering, type-safe APIs, and modern development patterns. While Vercel and Netlify are convenient for deployment, self-hosting on your VPS gives you control over your infrastructure, predictable costs, and no platform restrictions.

This guide shows how to deploy a TanStack Start app with Drizzle ORM and PostgreSQL on your VPS using Dokploy. I'll walk through each step from creating your app to production deployment.

## Why Self-Host with Dokploy?

**Dokploy Setup Video**

<YouTubeEmbed
  url="https://www.youtube.com/embed/EaOvNN-RJgI"
  label="Self-Hosting Made Easy: Secure VPS + HTTPS + Dokploy Setup"
/>

Here's why this setup works well:

- **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, like Vercel or Heroku
- **Modern Stack**: TanStack Start + Drizzle ORM for type-safe, full-stack development

<Notice type="info" title="Prerequisite: Install Dokploy">
Before starting, make sure your Dokploy server is installed and configured. Follow the complete setup guide: <a href="https://www.bitdoze.com/dokploy-install/" rel="noopener">Dokploy Install – Ditch Vercel/Heroku and Self-Host Your SaaS</a>. This includes VPS setup, security hardening with CrowdSec, and Dokploy installation. For updating deployed apps, see <a href="https://www.bitdoze.com/dokploy-update-docker-compose/" rel="noopener">How to Update Docker Compose Stacks in Dokploy</a>.
</Notice>



## 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

---

## Video with Setting Everything on Dokploy

<YouTubeEmbed
  url="https://www.youtube.com/embed/K3O2Cjq6Iho"
  label="Deploy TanStack Start on Your VPS with Dokploy"
/>

## Step 1: Set Up Your GitHub Account

If you don't already have a GitHub account, create one at [github.com](https://github.com). You'll use this to host your application code and enable automatic deployments.

### Configure SSH Keys (Recommended)

Set up SSH keys for secure authentication:

```sh
# Generate a new SSH key (if you don't have one)
ssh-keygen -t ed25519 -C "your_email@example.com"

# 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 Ed25519 (more secure and faster than RSA)
- `ssh-agent` manages your SSH keys in the background
- `ssh-add` adds your private key to the agent
- `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](https://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

Let's set up the database before creating the application. This way the 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 "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:123FFF4545FFFaaaa@91.98.95.196: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](https://www.bitdoze.com/dokploy-install/) and set up iptables with CrowdSec, allow PostgreSQL connections:

```sh
# 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 scaffolding tool. This sets up a complete React application with modern tooling.

### Initialize the Project

Run the following command in your terminal:

```sh
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

### Navigate to Your Project

```sh
cd tanstack-dokploy-test
```

### Start Development Server (Optional Test)

Before configuring the database, test that everything works:

```sh
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:

```sh
touch .env
```

Add your database connection string:

```sh
# .env
DATABASE_URL="postgresql://saas-app:123FFF4545FFFaaaa@91.98.95.196: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:

```sh
echo ".env" >> .gitignore
```

### Run Drizzle Commands

Now execute the Drizzle setup commands:

```sh
# 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 run.

### 6.1 Install Nitro V2 Plugin

```sh
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:

```ts
// 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:

```json
{
  "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:

```sh
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

```sh
# 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 git@github.com: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 git@github.com:yourusername/tanstack-dokploy-test.git`**
- Adds GitHub repository as remote named "origin"
- `git@github.com:` 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:**
```sh
# 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 git@github.com`

---

## Step 8: Deploy to Dokploy with Nixpacks or Railpacks

Time to deploy 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:

```sh
DATABASE_URL=postgresql://saas-app:123FFF4545FFFaaaa@91.98.95.196:5432/saas-app
DRIZZLE_DATABASE_URL=postgresql://saas-app:123FFF4545FFFaaaa@91.98.95.196: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**
```sh
# From your local machine, use production DATABASE_URL
DATABASE_URL="postgresql://saas-app:123FFF4545FFFaaaa@91.98.95.196: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**
```sh
# Ensure all dependencies are in package.json
npm install --save <missing-package>
git commit -am "Add missing dependency"
git push
```

**Error: Node version mismatch**
```json
// Add to package.json
{
  "engines": {
    "node": ">=20.0.0"
  }
}
```

Or add environment variable in Dokploy:
```sh
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**
```sh
# 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

Use connection pooling for production to handle concurrent requests:

```ts
// 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:

```sh
# 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:

```ts
// 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:

```sh
# 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:

```ts
// ❌ Bad
const apiKey = "sk_live_abc123";

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

### 3. Enable CORS Properly

Restrict API access to your domain:

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

### 4. Regular Updates

Keep dependencies updated:

```sh
# Check for outdated packages
npm outdated

# Update packages
npm update

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



## Conclusion

You've deployed a full-stack TanStack Start application on your 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 aren't limited by serverless timeouts, bandwidth costs, or vendor lock-in.

If you'd rather use a managed database instead of running Postgres yourself, I have a companion guide on [building a TanStack Start app with Bunny Database and Drizzle](/tanstack-start-bunny-database-drizzle/), which swaps Postgres for managed libSQL that idles to zero when unused.

## Getting Started with Dokploy

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

<Button link="https://www.bitdoze.com/dokploy-install/" text="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! 🚀