Skip to content

Linux VPS (no Docker)

Run Dulak directly on a Linux VPS — no Docker, no container overhead. Bun runs the TypeScript server, client assets are prebuilt, and systemd handles restarts. You’ll need a reverse proxy for HTTPS (Cloudflare, Caddy, or Nginx). This guide targets Ubuntu 22.04/24.04/26.04.

  • A Linux VPS with root or sudo access (Ubuntu 22.04/24.04/26.04).
  • SSH access to the server.
  • Git installed on the server (sudo apt install git if missing).
  • Your code on GitHub — clone your app repo (replace <your-repo-url> with your repo URL).
Internet → Cloudflare edge (TLS) → [tunnel | origin rule → VPS:4000]
└→ bun run src/index.ts
└→ SQLite + uploads in /opt/dulak/data

All commands run on the server via SSH. SSH in first:

Terminal window
ssh root@your-server-ip
Terminal window
curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
bun --version # should print 1.4.x or higher
Terminal window
git clone <your-repo-url> /opt/dulak
cd /opt/dulak

The /opt/dulak path is just an example — name it after your app. Adjust the path in the commands and systemd unit below to match.

Or copy the app from your build machine — the server runs TS directly, so the whole repo (minus .git) works. The data/ directory is created automatically on first startup.

Terminal window
bun install
bun run build

Create /opt/dulak/.env:

Terminal window
cat > /opt/dulak/.env << 'EOF'
PORT=4000
NODE_ENV=production
APP_URL=https://your-domain.com
DATABASE_PATH=/opt/dulak/data/app.sqlite
MAIL_DRIVER=resend
RESEND_API_KEY=...
EOF

Adjust the values for your deployment — see Configuration for the full env table. UPLOAD_DIR defaults to ./data/uploads — keep it inside /opt/dulak/data so it survives deploys and is easy to back up.

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

Terminal window
sudo tee /etc/systemd/system/dulak.service << 'EOF'
[Unit]
Description=Dulak app
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/dulak
EnvironmentFile=/opt/dulak/.env
ExecStart=%h/.bun/bin/bun run src/index.ts
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF

Enable and start:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now dulak
Terminal window
sudo systemctl is-active dulak # → active
curl http://127.0.0.1:4000/health # → {"status":"ok","uptime":...}

If either check fails, see Troubleshooting below.

Dulak does not terminate TLS — that is the proxy’s job. Set up Cloudflare, Caddy, or Nginx in front of 127.0.0.1:4000:

Reverse proxy guide


After you push code to GitHub, SSH to the server and run:

Terminal window
cd /opt/dulak
git pull
bun install
bun run build
sudo systemctl restart dulak
curl http://127.0.0.1:4000/health # → {"status":"ok",...}

That’s it — 5 commands, all on the server. The app handles SIGTERM gracefully (drains in-flight requests, closes the DB), so systemctl restart is safe mid-traffic.


Terminal window
sudo journalctl -u dulak -n 30 --no-pager

Common causes:

  • Invalid configuration: ... — a required env var is missing or invalid. Check /opt/dulak/.env against Configuration. The error message names the exact var.
  • bun: not found — the systemd unit points to %h/.bun/bin/bun but Bun is installed elsewhere. Check the path: which bun, and update ExecStart in the unit file.
  • EADDRINUSE: Port 4000 — another process is using port 4000. Find it: ss -tlnp | grep 4000. Kill it or change PORT in .env.
Terminal window
sudo systemctl status dulak # check if active
sudo journalctl -u dulak -f # live tail for errors
curl -v http://127.0.0.1:4000/health # verbose response

If the service is active but /health returns 500, the database may be locked or the DB file is missing. Check:

Terminal window
ls -la /opt/dulak/data/app.sqlite # file exists?
sqlite3 /opt/dulak/data/app.sqlite 'PRAGMA integrity_check;'

The app binds to 0.0.0.0:4000 by default. If you can’t reach it:

  • Firewall blocking: sudo ufw status — if UFW is active, allow the port (or better, set up the reverse proxy and only expose 80/443).
  • App bound to localhost: if you set HOST=127.0.0.1, only local processes can reach it. This is correct behind a reverse proxy — the proxy connects locally, the port is not public.
  • Cloudflare timeout: check that your DNS record points to the right IP and the proxy status is Proxied (orange cloud). See Reverse proxy → Cloudflare.
Terminal window
sudo chown -R $USER:$USER /opt/dulak/data
sudo chmod 755 /opt/dulak/data

The data directory must be owned by your user — SQLite needs write access to create the -wal and -shm files alongside the database.


  • Single instance only. SQLite is single-writer and the rate limiter is in-memory — this guide runs one process. Horizontal scaling is a deliberate swap point (external session/upload stores, Redis limiter).
  • Logs: journalctl -u dulak -f.
  • Backup: data/app.sqlite + data/uploads (the uploads directory).
  • Single binary alternative: bun build --compile produces a standalone executable with the same runtime — swap the ExecStart for the binary and prebuild dist/ on the build machine.