Skip to content

Script-based Setup (setup-server.sh)

For a non-Docker, bare-metal deployment on a fresh Ubuntu host, Doable ships an automated installer: setup-server.sh.

What it installs

On a clean Ubuntu 22.04 or 24.04 server, run as root:

sudo ./setup-server.sh

It will install and configure:

Component Purpose
Node.js 22 Runtime
pnpm Package manager
PostgreSQL 16 + extensions (pgvector, pgcrypto, pg_trgm) Database
Caddy Reverse proxy with automatic Let's Encrypt SSL
cloudflared Cloudflare Tunnel β€” public access without opening ports
tmux Multiplexer that holds the API/Web/WS processes
fail2ban SSH brute-force protection
UFW Firewall (deny-all, allow only SSH)
Chromium dependencies For Puppeteer thumbnail rendering
Swap file Helps small VPSes survive build spikes
systemd unit doable.service Auto-start on boot, wraps the tmux session
systemd unit cloudflared.service Auto-start the tunnel

What it asks you for

The script is interactive and prompts for:

  • Domain (e.g. app.example.com) β€” required.
  • API subdomain (default api) β†’ api.example.com.
  • WS subdomain (default ws) β†’ ws.example.com.
  • GitHub repo to clone (default doable-me/doable).
  • Database password (default doable).
  • Optional: GitHub / Google OAuth, Anthropic / OpenAI keys, Stripe keys.

Random secrets (JWT_SECRET, ENCRYPTION_KEY, INTERNAL_SECRET) are generated automatically with openssl rand -hex.

What runs after install

systemd
 └── doable.service          # Wraps a tmux session named "doable"
      β”œβ”€β”€ window: api        # tsx watch services/api/src/index.ts
      β”œβ”€β”€ window: web        # next start -H 127.0.0.1
      └── window: ws         # tsx watch services/ws/src/index.ts
 └── cloudflared.service     # Cloudflare Tunnel β†’ 127.0.0.1:* services
 └── caddy.service           # Reverse proxy with SSL
 └── postgresql.service      # Database

All application services bind to 127.0.0.1 only. Public access goes through Cloudflare Tunnel and/or Caddy. See Network Binding.

Day-2 operations

# Watch a window in tmux
tmux attach -t doable
# Ctrl-b 0/1/2 to switch windows; Ctrl-b d to detach

# Restart everything
systemctl restart doable

# Update Doable
cd /root/doable
git pull
pnpm install
pnpm db:migrate
systemctl restart doable

The API and WS services use tsx watch β€” file changes are picked up automatically without a build step. The web app needs a rebuild for production:

cd /root/doable
pnpm build --filter=@doable/web
systemctl restart doable

Disabling components

You don't want… Edit
Cloudflare Tunnel Skip the cloudflared login prompt; use Caddy directly with a public IP
OAuth Leave the OAuth prompts blank β€” email/password still works
Stripe billing Leave Stripe prompts blank β€” credit system runs in disabled mode

Verifying the install

Run these after the script finishes:

# All services bound to localhost only?
ss -tlnp | grep -v 127.0.0.1
# Should show only sshd on 0.0.0.0:22

# Caddy serving?
curl -I https://your-domain/

# API healthy?
curl https://api.your-domain/health

Troubleshooting

See Troubleshooting. The most common issues:

  • PostgreSQL connection failed β€” setup-server.sh writes the password to /root/doable/.env. Compare it to pg_hba.conf and make sure it matches the DATABASE_URL.
  • Port 80/443 already in use β€” another web server is running; systemctl stop it before installing Caddy.
  • cloudflared login fails β€” re-run interactively: cloudflared tunnel login.

β†’ For the Docker path see Docker Setup.