Docs
BlogHomeStart building

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.

No database to install

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

RequirementNotes
A Linux serverUbuntu 22.04+ / Debian 12, 1 GB RAM minimum
Node.js 20 LTSThe project targets Node 20
Build toolsbuild-essential + python3 (a couple of dependencies compile native code)
curl, jq, tarTo call the API and unpack the source
Your project IDe.g. my-app — the ID you created the project with
A Totalum API keyKeep it server-side only. Prefixed tlm_sk_
A domain namePointed at your server, so you can serve over HTTPS

#The flow at a glance

  1. Prepare the server — Node.js 20 + build tools.
  2. Download the source code via the API (a signed link to a .tar.gz).
  3. Download the environment variables via the API and write your .env.
  4. Install, build and runnpm ci · npm run build · npm start.
  5. 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:

bash
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

GET/api/v1/vcaas/projects/:projectId/source-code1 credit

This 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).

bash
# 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"
The link expired?

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

GET/api/v1/vcaas/projects/:projectId/github/envFree

This 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.).

bash
# 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:

bash
# in $APP_DIR/.env
NEXT_PUBLIC_APP_URL=https://app.yourdomain.com
Set your URL *before* you build

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

bash
cd "$APP_DIR"
npm ci
npm run build
npm start

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

bash
# Run on port 3000 instead of 80
npx next start -H 127.0.0.1 -p 3000

Visit 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):

ini
[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.target

Next.js automatically loads the .env from the working directory, so there's nothing else to wire up. Enable and start it:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
Prefer PM2?

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

caddyfile
app.yourdomain.com {
    reverse_proxy 127.0.0.1:3000
}
bash
sudo systemctl reload caddy

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

dockerfile
# ---- 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:

yaml
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-stopped
bash
docker compose up -d --build

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

bash
#!/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"
bash
chmod +x deploy.sh
TOTALUM_API_KEY=tlm_sk_your_key ./deploy.sh

Run 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

SymptomFix
Login / auth doesn't work, redirect loopsServe 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 ciInstall build-essential and python3 (Step 1).
403 when downloading the sourceThe signed URL expired (~30 min) — request a new one.
Port already in useOnly the reverse proxy should own 80/443; run the app on an internal port (3000).
App can't reach its dataEnsure outbound HTTPS is allowed and TOTALUM_API_KEY in .env is correct.
Keep your secrets safe

Your .env contains TOTALUM_API_KEY and BETTER_AUTH_SECRET. Never commit it, keep it chmod 600, and never expose it to the browser.