Manual Setup¶
Use this guide if you want full control: no setup-server.sh, no Docker. You install every component yourself. Recommended only if the scripted paths don't fit your environment.
1. Install dependencies¶
# Ubuntu 22.04 / 24.04
sudo apt update
sudo apt install -y curl git build-essential ca-certificates tmux ufw fail2ban
# Node.js 22 (via NodeSource)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
# pnpm
corepack enable
corepack prepare pnpm@9.15.4 --activate
# PostgreSQL 16 + pgvector
sudo apt install -y postgresql-16 postgresql-16-pgvector
2. Configure PostgreSQL¶
sudo -u postgres psql <<SQL
CREATE USER doable WITH PASSWORD 'change-me';
CREATE DATABASE doable OWNER doable;
\c doable
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pgvector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SQL
Edit /etc/postgresql/16/main/postgresql.conf:
Restart PostgreSQL:
3. Clone & build Doable¶
sudo mkdir -p /opt/doable
sudo chown $USER /opt/doable
git clone https://github.com/doable-me/doable.git /opt/doable
cd /opt/doable
pnpm install
pnpm build
4. Create the env file¶
Edit .env and set at least:
JWT_SECRET=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
INTERNAL_SECRET=$(openssl rand -hex 32)
DATABASE_URL=postgres://doable:change-me@localhost:5432/doable
API_HOST=127.0.0.1
WS_HOST=127.0.0.1
NODE_ENV=production
NEXT_PUBLIC_API_URL=https://api.your-domain.com
NEXT_PUBLIC_WS_URL=wss://ws.your-domain.com
NEXT_PUBLIC_APP_URL=https://your-domain.com
CORS_ORIGINS=https://your-domain.com
Add an AI provider key:
See the full Environment Variables reference.
5. Run migrations¶
6. Reverse proxy¶
Pick one. Both work. Both must terminate TLS.
Caddy (easiest — auto-SSL)¶
/etc/caddy/Caddyfile:
your-domain.com {
bind 127.0.0.1
reverse_proxy 127.0.0.1:3000
}
api.your-domain.com {
bind 127.0.0.1
reverse_proxy 127.0.0.1:4000
}
ws.your-domain.com {
bind 127.0.0.1
reverse_proxy 127.0.0.1:4001
}
The
bind 127.0.0.1directive matters when you're routing public traffic via Cloudflare Tunnel. Drop it (or usebind 0.0.0.0) if Caddy is your public-facing entrypoint.
nginx¶
Use docker/nginx.conf.template as a starting point. The Docker setup script substitutes {{DOMAIN}} and writes the result to /etc/nginx/sites-enabled/doable.
7. systemd unit¶
Create /etc/systemd/system/doable.service:
[Unit]
Description=Doable (tmux session: api, web, ws)
After=network.target postgresql.service
[Service]
Type=forking
User=doable
WorkingDirectory=/opt/doable
ExecStart=/usr/bin/tmux new-session -d -s doable \
'pnpm dev:api' \; \
new-window -n web 'pnpm dev:web' \; \
new-window -n ws 'pnpm dev:ws'
ExecStop=/usr/bin/tmux kill-session -t doable
Restart=on-failure
[Install]
WantedBy=multi-user.target
For production, replace
pnpm dev:*withpnpm start:*after apnpm build. The dev scripts are convenient but trade memory for hot-reload.
Enable and start:
8. Firewall¶
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Verify nothing else is listening publicly:
9. Optional pieces¶
| Feature | What to install |
|---|---|
| Chromium for thumbnails | sudo apt install -y chromium-browser fonts-liberation libasound2t64 libnss3 |
| Cloudflare Tunnel | Follow Cloudflare's install guide, then cloudflared tunnel route dns <id> your-domain.com |
| Redis | sudo apt install -y redis-server, then REDIS_URL=redis://127.0.0.1:6379 in .env |
10. Verify¶
Open https://your-domain.com in a browser and sign up.
→ For OAuth, integrations, and other extras, see Environment Variables and User Guide / Integrations.