Self-host your bots, APIs & AI workers on a VPS.
A beginner-friendly field guide for moving an existing bot, API, webhook, or AI worker to an always-on internet computer. Every major technical term is explained in plain English first, then shown in its proper technical form.
Built as a practical, beginner-friendly guide for anyone learning how to self-host bots, APIs, webhooks, and AI workers. The screenshots are from a real TierHive VPS setup, with account/public endpoint details hidden.
What exactly is a VPS?
Think: a computer you rent on the internet
A Virtual Private Server (VPS) is a small always-on Linux computer in a data center. You connect to it using SSH — basically a secure remote terminal — install what your app needs, and keep your code running there.
Docker makes the server predictable
Instead of manually installing every Node/Python dependency on the VPS, Docker packages the app runtime into a container. Think of Docker as a sealed lunchbox containing your app and everything it needs; the container is that lunchbox while it is running. A Compose file describes how it starts, restarts, mounts storage, and exposes ports.
admin only
SSH / HTTPS
Ubuntu
bot / API / worker
Why not just keep it on a laptop or PaaS?
| Question | Local PC | Managed PaaS (managed hosting) | Your VPS |
|---|---|---|---|
| Runs while your laptop is off? | No | Yes | Yes |
| Beginner convenience | Easy | Easiest | More setup |
| OS / networking control | Full local | Limited | Full |
| Host several small services together | Possible, PC must stay on | Usually billed per service | Yes, within VPS resources |
| Persistent files / SQLite | Local disk | Needs provider volume | You control the disk |
| Custom domains / reverse proxy | Extra tunneling | Usually built in | Full control |
| Maintenance responsibility | Your PC | Mostly provider | You patch, secure, back up |
Consolidation
A single VPS can host multiple lightweight bots, APIs, webhooks, and workers — as long as CPU/RAM/disk stay healthy.
Control
You decide firewall rules, storage paths, Docker networks, reverse proxy, backups, and update timing.
Portability
Docker + Git + environment variables make it easier to move from one VPS provider to another.
AI-friendly
Great for API-based AI agents, background workers, queues, vector services, and webhook orchestration without keeping a workstation on.
Bot, API, or AI worker?
The deployment is similar, but networking changes depending on who initiates the connection.
Usually outbound-only (your bot calls out)
The bot connects outward to Discord/Slack. You typically do not need to expose the app port publicly.
127.0.0.1 so only the VPS itself can reach it.Critical token rule
For a single production bot token, avoid running two production instances at the same time unless the app is explicitly designed for sharding/HA.
Inbound traffic is required (the internet calls your app)
Clients must reach your service. The safer pattern is Internet → HTTPS 443 → Caddy/Nginx → app on localhost.
Domain + TLS
Point DNS to the VPS (or use your provider’s NAT/HAProxy feature), then let a reverse proxy handle HTTPS certificates and forward traffic to the container.
Most AI workers call APIs outward
Gemini/OpenAI/Anthropic calls are outbound HTTPS. If the agent also receives webhooks, treat its inbound side like an API.
Watch memory and concurrency
API-based AI is light on the VPS; local model inference is not. A 2 GB VPS is suitable for small orchestration services, not large on-box LLMs.
Inventory the app first.
Where is the source?
GitHub/GitLab repo, local folder, Docker image, or provider-connected repository?
What must stay private?
Bot tokens, API keys, database URLs, webhook secrets, OAuth credentials. Never paste them into docs or screenshots.
What must survive?
SQLite files, uploads, generated files, logs, vector indexes, user data, and existing backups.
Does it need inbound traffic?
Bots may not. APIs/webhooks do. Note every port and domain currently in use.
How do we know it works?
HTTP /health//ready, bot health command, test request, log message, or worker heartbeat.
How do we go back?
Keep the old host intact and stopped for a short rollback window. Do not delete it on cutover day.
Provision a small Ubuntu VPS.
The exact provider can be TierHive, Hetzner, DigitalOcean, Vultr, Linode, AWS Lightsail, or another VPS host.
Safe starter profile
OS: Ubuntu 24.04 LTS (or current provider-supported LTS)
RAM: 2 GB for small bots/APIs
Disk: 20–30 GB NVMe
CPU: 1 vCPU is enough for many lightweight Node/Python services
Region: near your users/external services
Login: SSH key preferred over password
free -h, df -h, and docker stats after launch.Connect, patch, and install Docker.
Commands below are intentionally shown one at a time. Replace placeholders inside <...>.
SSH into the VPS
ssh <admin-user>@<public-host-or-ip> -p <ssh-port>Some providers use port 22 directly. NAT-based providers may give you an external forwarded port that maps to internal SSH port 22.
Patch Ubuntu
sudo apt updatesudo apt upgrade -yInstall Docker + Compose
sudo apt install -y docker.io docker-compose-v2sudo systemctl enable --now dockersudo usermod -aG docker $USEREnable a simple firewall
sudo ufw allow 22/tcpsudo ufw enableOptional: add swap on a small VPS
Swap is disk space used as emergency overflow when RAM gets tight. It is much slower than real RAM, so treat it as a safety cushion, not a replacement for RAM. A 2 GB swapfile is common on a 2 GB starter VPS.
sudo fallocate -l 2G /swapfilesudo chmod 600 /swapfilesudo mkswap /swapfilesudo swapon /swapfileecho '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabMake the application portable.
Recommended repository shape
your-app/
├── Dockerfile
├── compose.yaml
├── .dockerignore
├── .env.example
├── package.json / requirements.txt
├── src/
└── scripts/Keep runtime data outside the Git repository. The repository should be replaceable without deleting production data.
Persistent host layout
~/apps/your-app/
├── app/ ← Git clone / source
└── runtime/
├── data/
├── backups/
└── logs/This separation lets you rebuild the container or pull new code while keeping the database and backups on the host disk.
.env
Production secrets live on the VPS, not Git. Restrict with chmod 600 .env.
.dockerignore
Exclude .env, databases, runtime folders, backups, logs, and node_modules from image builds.
Health check
Provide a low-cost readiness check or app-native status command so deployment success is measurable.
How do I move the source?
git clone [email protected]:<org-or-user>/<repo>.git ~/apps/your-app/appBest long-term move: put the deployable code in a private Git repository. For a one-time transfer, use scp or rsync — but still keep secrets/data separate.
Export the current environment variables and download the persistent data using that host’s supported method. The provider can be Railway, Render, Fly.io, a cPanel server, another VPS, or anything else — the same principles apply.
Build, validate, start, verify.
Bot / worker Compose pattern
services:
app:
build: .
restart: unless-stopped
env_file: .env
volumes:
- ../runtime/data:/app/data
- ../runtime/backups:/app/backups
- ../runtime/logs:/app/logs
# Optional local-only health endpoint:
ports:
- "127.0.0.1:3000:3000"If the bot has no HTTP health server, omit the ports block entirely.
API Compose pattern
services:
api:
build: .
restart: unless-stopped
env_file: .env
volumes:
- ../runtime/data:/app/data
ports:
- "127.0.0.1:3000:3000"Bind to localhost and let a reverse proxy expose HTTPS publicly. This keeps the raw application port off the public internet.
Validate Compose without printing secrets
docker compose config --quietBuild the image
docker compose buildStart in the background
docker compose up -dCheck container status
docker compose psRead recent logs if anything looks wrong
docker compose logs --tail=100Test a local readiness endpoint
curl -i http://127.0.0.1:3000/readyUse your app’s real endpoint. A readiness check is the app saying “I am not only running — I am actually ready to do work.” A healthy service commonly answers with HTTP 200, which simply means “OK.”
Add a domain and HTTPS safely.
Skip this section for a pure outbound Discord bot that does not need a public HTTP endpoint.
Caddy example
After DNS points to your reachable VPS endpoint and ports 80/443 can reach the server:
api.example.com {
reverse_proxy 127.0.0.1:3000
}Provider networking matters
Some VPS providers give a direct public IPv4. Others use NAT, forwarded ports, HAProxy domains, or provider-managed ingress. Configure the method your host supports.
Do a safe production cutover.
SHA-256 on Linux
sha256sum /path/to/backup.sqliteSHA-256 on Windows PowerShell
Get-FileHash "C:\path\backup.sqlite" -Algorithm SHA256Operate it like a tiny production server.
Containers
docker compose psdocker compose logs --tail=100Server resources
free -hdf -hdocker statsBackups
Automate local backups, verify them periodically, and copy at least one recent backup off the VPS. A backup that has never been tested is only a hope.
Simple update workflow
When something fails, check in this order.
Run docker compose ps, then docker compose logs --tail=100. Look for missing environment variables, permission errors, database errors, port conflicts, or startup checks that have not finished yet.
Confirm the VPS is running in the provider dashboard, confirm the correct public endpoint/forwarded port, and use the provider console if necessary. A firewall or SSH configuration change can lock you out.
Check bot permissions, guild/server IDs, command registration, maintenance mode, and whether a second production instance is still running somewhere else.
Check DNS, provider NAT/ingress, ports 80/443, reverse-proxy service status, TLS issuance, and firewall rules. Do not change the app to bind publicly just to “make it work” unless you understand the security impact.
The database was likely stored in the container’s ephemeral filesystem instead of a persistent host mount/volume. Stop writes, find the last valid backup, fix the mount, then restore offline.
Use docker stats and free -h. Check application memory leaks, concurrency, queue buildup, log growth, and whether the VPS is simply undersized. Swap can soften spikes but does not fix a leak.
MarkHQ: managed host → TierHive VPS
A live Discord/AI assistant was migrated to a 2 GB Ubuntu VPS using Docker, persistent SQLite storage, backups, and health checks.
Ubuntu 24.04
2 GB RAM · 25 GB NVMe · Singapore · Docker + Compose.
SQLite preserved
Final snapshot verified before cutover, restored offline, then a new post-cutover backup was created and verified.
Health kept private
Container health server listened internally; host publication was bound to 127.0.0.1:3000 rather than exposed publicly.
Production validation
✅ HTTP readiness returned 200
✅ Discord connected
✅ Database integrity OK
✅ 3 active workspaces present
✅ Required permissions present
✅ Deployment preflight: no warnings
Recovery posture
✅ Post-cutover backup verified
✅ Off-server copy transferred to Windows PC
✅ SHA-256 matched on both machines
✅ Old host stopped but retained for rollback
✅ Temporary transfer copy of secrets removed after success
Technical words, translated into normal human language.
You do not need to memorize these before starting. Use this section as a cheat sheet while you work.
Use this as your migration runbook.
Check items as you complete them. Your browser remembers this checklist locally.