Linux VPS (no Docker)
Run Laju Go directly on a Linux VPS — no Docker, no container overhead. The Go binary serves the backend and the prebuilt Svelte frontend, SQLite handles storage, 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.
Prerequisites
Section titled “Prerequisites”- 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 gitif missing). - Your code on GitHub — clone your app repo (replace
<your-repo-url>with your repo URL).
Architecture
Section titled “Architecture”Internet → Cloudflare edge (TLS) → [tunnel | origin rule → VPS:8080] └→ ./laju-go (Go binary) └→ SQLite + uploads in /opt/laju-go/dataPhase 1 — First-time setup
Section titled “Phase 1 — First-time setup”All commands run on the server via SSH. SSH in first:
ssh root@your-server-ip1.1 Install Go and Node.js
Section titled “1.1 Install Go and Node.js”Laju Go needs Go 1.22+ and Node.js 20+ to build. CGO is required
(mattn/go-sqlite3), so you also need a C compiler:
# Gosudo rm -rf /usr/local/gocurl -L https://go.dev/dl/go1.22.5.linux-amd64.tar.gz | sudo tar -C /usr/local -xzf -echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrcsource ~/.bashrc
# Node.js 20 (via NodeSource)curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -sudo apt install -y nodejs
# C compiler + SQLite dev headers (CGO requirement)sudo apt install -y gcc libsqlite3-dev sqlite3
go version # should print 1.22.x or highernode --version # should print v20.x or higher1.2 Get the code
Section titled “1.2 Get the code”git clone <your-repo-url> /opt/laju-gocd /opt/laju-goThe /opt/laju-go path is just an example — name it after your app.
Adjust the path in the commands and systemd unit below to match.
1.3 Install dependencies and build
Section titled “1.3 Install dependencies and build”npm cinpm run build:allnpm run build:all runs vite build first (produces
dist/.vite/manifest.json), then go build. The order matters — the
Go binary reads the manifest at startup to resolve hashed asset
filenames.
1.4 Environment
Section titled “1.4 Environment”Create /opt/laju-go/.env:
cat > /opt/laju-go/.env << 'EOF'APP_PORT=8080APP_ENV=productionAPP_URL=https://your-domain.comDB_PATH=/opt/laju-go/data/app.dbSESSION_SECRET=change-this-to-a-random-32-char-stringALLOWED_ORIGINS=https://your-domain.comGOOGLE_REDIRECT_URL=https://your-domain.com/auth/google/callbackEOFAdjust the values for your deployment — see
Configuration for the full env table.
DB_PATH defaults to ./data/app.db; keep it inside /opt/laju-go/data
so it survives deploys and is easy to back up. Generate a strong
SESSION_SECRET:
openssl rand -hex 321.5 systemd user service
Section titled “1.5 systemd user service”Laju Go ships with a systemd user service file at
systemd/laju-go.service:
[Unit]Description=Laju Go ApplicationAfter=network.target
[Service]Type=simpleWorkingDirectory=%h/projects/laju-goExecStart=%h/projects/laju-go/laju-goRestart=alwaysRestartSec=5StandardOutput=journalStandardError=journalSyslogIdentifier=laju-go
# EnvironmentEnvironmentFile=%h/projects/laju-go/.env
[Install]WantedBy=default.targetKey points:
%hexpands to the user’s home directory — no hardcoded paths.- User service (
systemctl --user), not a system service. No root needed to manage it. EnvironmentFileloads.envfrom the project directory.Restart=alwayswithRestartSec=5auto-recovers from crashes.
If you cloned to /opt/laju-go (not ~/projects/laju-go), update
WorkingDirectory and ExecStart in the unit file to match.
Install the service:
# Enable linger so the user service runs after logout (once)sudo loginctl enable-linger $USER
# Copy the service filemkdir -p ~/.config/systemd/usercp systemd/laju-go.service ~/.config/systemd/user/
# Reload systemd, enable and startsystemctl --user daemon-reloadsystemctl --user enable --now laju-go1.6 Verify
Section titled “1.6 Verify”systemctl --user is-active laju-go # → activecurl http://127.0.0.1:8080/health # → {"status":"ok","version":...}If either check fails, see Troubleshooting below.
1.7 Reverse proxy and firewall
Section titled “1.7 Reverse proxy and firewall”Laju Go does not terminate TLS — that is the proxy’s job. Set up
Cloudflare, Caddy, or Nginx in front of 127.0.0.1:8080:
Phase 2 — Routine updates
Section titled “Phase 2 — Routine updates”After you push code to GitHub, SSH to the server and run:
cd /opt/laju-gogit pullnpm cinpm run build:allsystemctl --user restart laju-gocurl http://127.0.0.1:8080/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 --user restart is safe mid-traffic.
Cross-compile from macOS with Zig (alternative)
Section titled “Cross-compile from macOS with Zig (alternative)”If your dev machine is macOS and you don’t want Go + Node on the
server, cross-compile locally and upload the binary. Laju Go uses
mattn/go-sqlite3 (CGO-based), so a plain GOOS=linux go build fails
without a C cross-compiler. Zig solves this as a drop-in C
cross-compiler:
# On your Macbrew install zigmake build-linuxThis produces a laju-go binary targeting linux/amd64. Upload it
along with dist/, migrations/, and public/:
scp laju-go user@your-server:/opt/laju-go/scp -r dist migrations public user@your-server:/opt/laju-go/Then on the server, restart the service:
systemctl --user restart laju-goFor ARM64 targets (Graviton, Raspberry Pi):
GOOS=linux GOARCH=arm64 go build -trimpath -o laju-go ./cmd/laju-goTroubleshooting
Section titled “Troubleshooting”Service won’t start
Section titled “Service won’t start”systemctl --user status laju-gojournalctl --user -u laju-go -n 30 --no-pagerCommon causes:
laju-go: not found— the binary doesn’t exist at theExecStartpath. Check thatnpm run build:allsucceeded and the binary is atWorkingDirectory/laju-go. Update the unit file if your install path differs.no .env file found—.envis missing or theEnvironmentFilepath in the unit file is wrong. Check that/opt/laju-go/.envexists and the path in the service file matches.EADDRINUSE: Port 8080— another process is using port 8080. Find it:ss -tlnp | grep 8080. Kill it or changeAPP_PORTin.env.
Health check returns non-200
Section titled “Health check returns non-200”systemctl --user status laju-go # check if activejournalctl --user -u laju-go -f # live tail for errorscurl -v http://127.0.0.1:8080/health # verbose responseIf the service is active but /health returns 500, the database may be
locked or the DB file is missing. Check:
ls -la /opt/laju-go/data/app.db # file exists?sqlite3 /opt/laju-go/data/app.db 'PRAGMA integrity_check;'CGO build errors
Section titled “CGO build errors”# github.com/mattn/go-sqlite3: gcc: not foundInstall the C compiler and SQLite headers:
sudo apt install -y gcc libsqlite3-devLaju Go uses mattn/go-sqlite3, which requires CGO. Without gcc,
the build fails.
Port not reachable from outside
Section titled “Port not reachable from outside”The app binds to 0.0.0.0:8080 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). - Cloudflare timeout: check that your DNS record points to the right IP and the proxy status is Proxied (orange cloud). See Reverse proxy → Cloudflare.
Permission denied on data directory
Section titled “Permission denied on data directory”sudo chown -R $USER:$USER /opt/laju-go/datasudo chmod 755 /opt/laju-go/dataThe data directory must be owned by your user — SQLite needs write access
to create the -wal and -shm files alongside the database.
Operational notes
Section titled “Operational notes”- Single instance only. SQLite is single-writer and the session cache is in-memory — this guide runs one process. Horizontal scaling is a deliberate swap point (external session store, Redis limiter).
- Logs:
journalctl --user -u laju-go -f. - Backup:
data/app.db+storage/(uploads and avatars). - Migrations run automatically on startup via Goose — no separate migration step during deploy.
- Cross-compile alternative:
make build-linux(with Zig) produces a Linux binary on macOS — swap theExecStartfor the uploaded binary and skip installing Go + Node on the server.