Skip to content

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.

  • 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:8080]
└→ ./laju-go (Go binary)
└→ SQLite + uploads in /opt/laju-go/data

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

Terminal window
ssh root@your-server-ip

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:

Terminal window
# Go
sudo rm -rf /usr/local/go
curl -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' >> ~/.bashrc
source ~/.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 higher
node --version # should print v20.x or higher
Terminal window
git clone <your-repo-url> /opt/laju-go
cd /opt/laju-go

The /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.

Terminal window
npm ci
npm run build:all

npm 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.

Create /opt/laju-go/.env:

Terminal window
cat > /opt/laju-go/.env << 'EOF'
APP_PORT=8080
APP_ENV=production
APP_URL=https://your-domain.com
DB_PATH=/opt/laju-go/data/app.db
SESSION_SECRET=change-this-to-a-random-32-char-string
ALLOWED_ORIGINS=https://your-domain.com
GOOGLE_REDIRECT_URL=https://your-domain.com/auth/google/callback
EOF

Adjust 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:

Terminal window
openssl rand -hex 32

Laju Go ships with a systemd user service file at systemd/laju-go.service:

[Unit]
Description=Laju Go Application
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/projects/laju-go
ExecStart=%h/projects/laju-go/laju-go
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=laju-go
# Environment
EnvironmentFile=%h/projects/laju-go/.env
[Install]
WantedBy=default.target

Key points:

  • %h expands to the user’s home directory — no hardcoded paths.
  • User service (systemctl --user), not a system service. No root needed to manage it.
  • EnvironmentFile loads .env from the project directory.
  • Restart=always with RestartSec=5 auto-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:

Terminal window
# Enable linger so the user service runs after logout (once)
sudo loginctl enable-linger $USER
# Copy the service file
mkdir -p ~/.config/systemd/user
cp systemd/laju-go.service ~/.config/systemd/user/
# Reload systemd, enable and start
systemctl --user daemon-reload
systemctl --user enable --now laju-go
Terminal window
systemctl --user is-active laju-go # → active
curl http://127.0.0.1:8080/health # → {"status":"ok","version":...}

If either check fails, see Troubleshooting below.

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:

Reverse proxy guide


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

Terminal window
cd /opt/laju-go
git pull
npm ci
npm run build:all
systemctl --user restart laju-go
curl 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:

Terminal window
# On your Mac
brew install zig
make build-linux

This produces a laju-go binary targeting linux/amd64. Upload it along with dist/, migrations/, and public/:

Terminal window
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:

Terminal window
systemctl --user restart laju-go

For ARM64 targets (Graviton, Raspberry Pi):

Terminal window
GOOS=linux GOARCH=arm64 go build -trimpath -o laju-go ./cmd/laju-go

Terminal window
systemctl --user status laju-go
journalctl --user -u laju-go -n 30 --no-pager

Common causes:

  • laju-go: not found — the binary doesn’t exist at the ExecStart path. Check that npm run build:all succeeded and the binary is at WorkingDirectory/laju-go. Update the unit file if your install path differs.
  • no .env file found.env is missing or the EnvironmentFile path in the unit file is wrong. Check that /opt/laju-go/.env exists 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 change APP_PORT in .env.
Terminal window
systemctl --user status laju-go # check if active
journalctl --user -u laju-go -f # live tail for errors
curl -v http://127.0.0.1:8080/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/laju-go/data/app.db # file exists?
sqlite3 /opt/laju-go/data/app.db 'PRAGMA integrity_check;'
# github.com/mattn/go-sqlite3: gcc: not found

Install the C compiler and SQLite headers:

Terminal window
sudo apt install -y gcc libsqlite3-dev

Laju Go uses mattn/go-sqlite3, which requires CGO. Without gcc, the build fails.

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.
Terminal window
sudo chown -R $USER:$USER /opt/laju-go/data
sudo chmod 755 /opt/laju-go/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 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 the ExecStart for the uploaded binary and skip installing Go + Node on the server.