Self-Host on a Linux Server
Deploy your Totalum project to your own infrastructure. Pull the source and env through the API, build it, run it, and keep it stable behind HTTPS — a complete, copy-paste guide.
Your Totalum project is a real, self-contained Next.js application that you fully own — so you can run it on any Linux server (a VPS from Hetzner, DigitalOcean, AWS, etc.) instead of Totalum's managed hosting. This guide takes you end to end: pull the latest source code and environment variables through the API, install and build, run it as a service, and put it behind HTTPS. Everything is copy-paste.
Your project talks to its managed database, auth and file storage over HTTPS using its API key — there is nothing to provision on your server. All it needs is Node.js and outbound internet access.
#What you'll need
| Requirement | Notes |
|---|---|
| A Linux server | Ubuntu 22.04+ / Debian 12, 1 GB RAM minimum |
| Node.js 20 LTS | The project targets Node 20 |
| Build tools | build-essential + python3 (a couple of dependencies compile native code) |
curl, jq, tar | To call the API and unpack the source |
| Your project ID | e.g. my-app — the ID you created the project with |
| A Totalum API key | Keep it server-side only. Prefixed tlm_sk_ |
| A domain name | Pointed at your server, so you can serve over HTTPS |
#The flow at a glance
- Prepare the server — Node.js 20 + build tools.
- Download the source code via the API (a signed link to a
.tar.gz). - Download the environment variables via the API and write your
.env. - Install, build and run —
npm ci·npm run build·npm start. - Keep it stable — a process manager (systemd) + HTTPS reverse proxy.
The last section wraps steps 2–4 into a single script you can re-run to redeploy after every change.
#Step 1 — Prepare the server
Install Node.js 20 and the build essentials:
sudo apt update
sudo apt install -y curl jq tar build-essential python3
# Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v # should print v20.x#Step 2 — Download the source code
/api/v1/vcaas/projects/:projectId/source-code1 creditThis returns a signed download URL (data.downloadUrl) to a .tar.gz of your project. The URL is valid for about 30 minutes, so download it promptly. The archive contains your full application source at its latest committed state — without node_modules or secrets (you add those in the next steps).
# Set these once — reused throughout
export API_KEY="tlm_sk_your_key"
export PROJECT_ID="my-app"
export BASE="https://api-accounts.totalum.app"
export APP_DIR="$HOME/myapp"
# 1) Ask the API for a fresh signed download URL
URL=$(curl -sf -H "api-key: $API_KEY" \
"$BASE/api/v1/vcaas/projects/$PROJECT_ID/source-code" | jq -r '.data.downloadUrl')
# 2) Download and extract the source
mkdir -p "$APP_DIR"
curl -sf "$URL" -o "$APP_DIR/source.tar.gz"
tar -xzf "$APP_DIR/source.tar.gz" -C "$APP_DIR"
cd "$APP_DIR"Signed URLs last ~30 minutes. If the download fails with 403, just call the endpoint again to get a fresh downloadUrl.
#Step 3 — Download your environment variables
/api/v1/vcaas/projects/:projectId/github/envFreeThis returns two ready-made .env files as strings — data.envDev (for local development) and data.envProd (for a production server). Use envProd. It already contains everything the app needs to boot: TOTALUM_API_KEY, BETTER_AUTH_SECRET, NODE_ENV=production, NEXT_PUBLIC_APP_URL, plus any secrets you added to the project (Stripe keys, etc.).
# Write the production .env
curl -sf -H "api-key: $API_KEY" \
"$BASE/api/v1/vcaas/projects/$PROJECT_ID/github/env" | jq -r '.data.envProd' > "$APP_DIR/.env"
# Lock it down — it holds your API key and auth secret
chmod 600 "$APP_DIR/.env"Now open .env and set NEXT_PUBLIC_APP_URL to your own domain — the downloaded value points at the default managed URL, and this must match the address your users will actually visit:
# in $APP_DIR/.env
NEXT_PUBLIC_APP_URL=https://app.yourdomain.comNEXT_PUBLIC_APP_URL is compiled into the app during npm run build, and it drives the auth base URL and secure-cookie behaviour. Set it to your real https domain before building. If you change the domain later, update .env and rebuild. Serving over HTTPS is required for authentication to work (see Step 5).
#Step 4 — Install, build and run
With the source and .env in place:
cd "$APP_DIR"
npm ci
npm run build
npm startnpm start serves the app on port 80. That's fine for a quick check, but for a real deployment you'll run it on an internal port behind a reverse proxy (next section) — start it directly on the port you want:
# Run on port 3000 instead of 80
npx next start -H 127.0.0.1 -p 3000Visit your server to confirm the app responds. Once it works, make it permanent.
#Step 5 — Keep it running & serve over HTTPS
For a stable deployment you want two things: the app restarts automatically (process manager) and it's served over HTTPS (reverse proxy). Run the app on an internal port (3000) and let the proxy handle TLS on 443.
#Run it as a service (systemd)
Create /etc/systemd/system/myapp.service (adjust User and the paths):
[Unit]
Description=Totalum project (Next.js)
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/myapp
ExecStart=/home/ubuntu/myapp/node_modules/.bin/next start -H 127.0.0.1 -p 3000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetNext.js automatically loads the .env from the working directory, so there's nothing else to wire up. Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myappIf you already use PM2: npm i -g pm2 && pm2 start "npx next start -H 127.0.0.1 -p 3000" --name myapp && pm2 startup && pm2 save.
#Put HTTPS in front (Caddy)
Caddy is the simplest option — it obtains and renews a Let's Encrypt certificate automatically. Point your domain's A record at the server first, then create /etc/caddy/Caddyfile:
app.yourdomain.com {
reverse_proxy 127.0.0.1:3000
}sudo systemctl reload caddyYour app is now live at https://app.yourdomain.com, auto-restarting and TLS-terminated. (Prefer Nginx? Proxy proxy_pass http://127.0.0.1:3000; and issue a cert with certbot.)
#Optional — Run it in Docker
For a reproducible, portable deployment, containerize it. Add a Dockerfile to the project root:
# ---- build ----
FROM node:20-slim AS build
WORKDIR /app
RUN apt-get update && apt-get install -y python3 build-essential && rm -rf /var/lib/apt/lists/*
COPY package*.json ./
RUN npm ci
COPY . .
# NEXT_PUBLIC_APP_URL is baked in at build time
ARG NEXT_PUBLIC_APP_URL
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
RUN npm run build
# ---- run ----
FROM node:20-slim AS run
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app ./
EXPOSE 3000
CMD ["node_modules/.bin/next", "start", "-H", "0.0.0.0", "-p", "3000"]And a docker-compose.yml:
services:
app:
build:
context: .
args:
NEXT_PUBLIC_APP_URL: https://app.yourdomain.com
env_file: .env
ports:
- "127.0.0.1:3000:3000"
restart: unless-stoppeddocker compose up -d --buildKeep Caddy (or any TLS proxy) in front of 127.0.0.1:3000, exactly as in Step 5. Because NEXT_PUBLIC_APP_URL is baked in at build time, it's passed as a build arg here, not just an env var.
#Automate it — one redeploy script
Whenever your project changes in Totalum, re-run this to pull the latest source, rebuild and restart. Save it as deploy.sh:
#!/usr/bin/env bash
set -euo pipefail
API_KEY="${TOTALUM_API_KEY:?set TOTALUM_API_KEY}"
PROJECT_ID="my-app"
APP_DIR="/home/ubuntu/myapp"
PUBLIC_URL="https://app.yourdomain.com"
BASE="https://api-accounts.totalum.app"
# 1) Download the latest source (signed URL valid ~30 min)
URL=$(curl -sf -H "api-key: $API_KEY" \
"$BASE/api/v1/vcaas/projects/$PROJECT_ID/source-code" | jq -r '.data.downloadUrl')
tmp=$(mktemp -d)
curl -sf "$URL" -o "$tmp/source.tar.gz"
rm -rf "$APP_DIR" && mkdir -p "$APP_DIR"
tar -xzf "$tmp/source.tar.gz" -C "$APP_DIR"
# 2) Write production env, then point NEXT_PUBLIC_APP_URL at your own domain
curl -sf -H "api-key: $API_KEY" \
"$BASE/api/v1/vcaas/projects/$PROJECT_ID/github/env" | jq -r '.data.envProd' > "$APP_DIR/.env"
sed -i "s#^NEXT_PUBLIC_APP_URL=.*#NEXT_PUBLIC_APP_URL=$PUBLIC_URL#" "$APP_DIR/.env"
chmod 600 "$APP_DIR/.env"
# 3) Install, build, restart
cd "$APP_DIR"
npm ci
npm run build
sudo systemctl restart myapp
echo "Deployed — live at $PUBLIC_URL"chmod +x deploy.sh
TOTALUM_API_KEY=tlm_sk_your_key ./deploy.shRun it by hand after each change, from a CI pipeline, or on a schedule (cron). The source-code endpoint always returns your project's latest committed state, so this always deploys the newest version.
#Troubleshooting
| Symptom | Fix |
|---|---|
| Login / auth doesn't work, redirect loops | Serve over HTTPS and make sure NEXT_PUBLIC_APP_URL matches your domain and was set before npm run build. |
node-gyp / native build errors on npm ci | Install build-essential and python3 (Step 1). |
403 when downloading the source | The signed URL expired (~30 min) — request a new one. |
| Port already in use | Only the reverse proxy should own 80/443; run the app on an internal port (3000). |
| App can't reach its data | Ensure outbound HTTPS is allowed and TOTALUM_API_KEY in .env is correct. |
Your .env contains TOTALUM_API_KEY and BETTER_AUTH_SECRET. Never commit it, keep it chmod 600, and never expose it to the browser.
