For the complete documentation index, see llms-docs.txt or the site index llms.txt. Full docs corpus: llms-full-docs.txt. Prefer the markdown version of this page at /docs/self-host/vps.md. Product capabilities: skill.md. Docs MCP: /docs/mcp. Site MCP: /mcp.

Deploy on VPS

Install the full Reloop stack on a fresh Ubuntu or Debian server with one command.

The Reloop self-host installer takes a fresh VPS and turns it into a running Reloop deployment: it installs Docker, generates production secrets, writes the Compose stack, applies the database schema, starts every service, verifies it is healthy, and prints the DNS records you need to add.

curl -fsSL https://reloop.sh/install.sh | sudo bash

https://reloop.sh/install.sh is the canonical address. It serves install/install.sh from the Reloop repository, and the script downloads the rest of the installer (install/lib and install/templates) from the same place. To read it before running it as root:

curl -fsSL https://reloop.sh/install.sh -o install.sh
less install.sh
sudo bash install.sh

Before you start

RequirementValue
Operating systemUbuntu 22.04 LTS, Ubuntu 24.04 LTS, Debian 12, Debian 13
Architecturex86_64
CPU2 vCPUs minimum, 4+ recommended
RAM4 GB minimum, 8 GB recommended
Disk35 GB minimum, 50 GB recommended
Ports25, 465, 587 free, plus 80 and 443 unless you run behind your own reverse proxy
Accessroot or sudo

[!NOTE] The container images are roughly 22 GB in total, which is why the disk floor is higher than for a source install. The installer refuses to continue on a smaller disk rather than failing halfway through the download.

Many providers block outbound port 25 by default. Reloop still sends through the submission ports, but inbound mail needs 25 open in both directions — ask your provider to unblock it if you plan to receive mail.


What the installer asks

PromptPurposeDefault
Primary Reloop domainThe hostname the dashboard and API are served on—
TLS contact emailLet's Encrypt expiry and account notices—
Database namePostgreSQL databasereloop
Database userPostgreSQL rolereloop
Run behind an existing reverse proxyServe plain HTTP on a local port for your own Caddy, Nginx or Traefik instead of taking 80 and 443. See Behind an existing reverse proxyno
Local port for your reverse proxyOnly asked behind a proxy. Reloop listens on 127.0.0.1 at this port8080
Configure automatic HTTPSLet Caddy obtain and renew certificates. Behind your own proxy it asks whether that proxy serves Reloop over HTTPS insteadyes
Configure object storageS3-compatible storage for file uploadsno

Everything else is generated: database and Redis passwords, the Better Auth secret, the internal service secret, the tracking and preferences secrets, and the webhook encryption key. They are written to /opt/reloop/.env with mode 0600.

Enter the domain as a bare hostname — reloop.example.com, not https://reloop.example.com and not reloop.example.com/admin.


What gets installed

/opt/reloop/
├── .env                 production configuration and secrets (0600, root)
├── admin-setup.key      one-time first-run setup key (0600, emptied after setup)
├── docker-compose.yml   the production stack
├── Caddyfile            reverse proxy and TLS
└── backups/             automatic database dumps taken before schema changes

The stack runs 20 containers: 14 Reloop backend services, 2 frontends (dashboard, links), PostgreSQL, Redis, NATS and Caddy. Only Caddy (80 and 443, or 127.0.0.1:<port> behind your own proxy) and the two mail transports (25 inbound, 465/587 submission) publish host ports. The database and event bus stay on the internal Docker network.

A self-hosted install deliberately leaves out the parts of Reloop that only make sense for the hosted service: the public documentation site (/docs redirects to reloop.sh), the credits and quota service, the public email-validation tools, the operator console, and bundled object storage. Sending is not metered.

A reloop command is installed alongside it:

reloop status            # container health
reloop logs [service]    # follow logs
reloop restart [service] # recreate the stack or one service, applying .env changes
reloop update            # pull the configured version and redeploy

File uploads

Reloop stores template images and attachments in S3-compatible object storage. The installer does not bundle a storage server — point it at whichever provider you already use (AWS S3, Cloudflare R2, Hetzner Object Storage, Backblaze B2, a MinIO box of your own).

Answer yes to the storage prompt and supply the endpoint, access key, secret key, bucket and region. The upload service is then deployed alongside the rest of the stack.

Answer no and file uploads stay off. To turn them on later, set the S3_* values and COMPOSE_PROFILES=storage in /opt/reloop/.env, then run reloop restart.

[!NOTE] The bucket must serve objects publicly over HTTP. Reloop hands out file URLs as {S3_ENDPOINT}/{S3_BUCKET}/{path}, so anything the dashboard uploads has to be readable at that address.


DNS

When the installer finishes it prints the records for the installation itself:

reloop.example.com            A    203.0.113.10
link.reloop.example.com       A    203.0.113.10
inbound.reloop.example.com    A    203.0.113.10
reloop.example.com            TXT  "v=spf1 ip4:203.0.113.10 -all"
  • The apex record serves the dashboard, the API and the Let's Encrypt HTTP challenge.
  • link. serves click and open tracking plus the unsubscribe pages, and is the CNAME target for customer tracking domains.
  • inbound. is the MX target for mail your verified domains receive.
  • The SPF record authorises the server to send for the host domain, which is what the include: in each sending domain's SPF resolves to.

These are separate from the SPF, DKIM, DMARC and MX records for the domains you send from. Those are generated per domain by Reloop — add a domain under Domains in the dashboard and it shows the exact records. See Connect a domain.

You can run the installer before adding DNS. Caddy keeps retrying certificate issuance in the background and HTTPS starts working on its own once the records resolve. Follow it with reloop logs proxy.


Behind an existing reverse proxy

Use this when the server already runs Caddy, Nginx or Traefik on 80 and 443, for example alongside other sites. Answer yes to Run behind an existing reverse proxy, or set RELOOP_EXTERNAL_PROXY=true for an unattended install. The installer then:

  • publishes the bundled Caddy on 127.0.0.1:<port> only, over plain HTTP, and no longer needs 80 or 443
  • keeps routing every Reloop path itself, so your proxy forwards whole hostnames
  • trusts X-Forwarded-For and X-Forwarded-Proto from private addresses, so Reloop sees the visitor's IP and the https scheme

Answer yes to Does your proxy serve Reloop over HTTPS if it does. That sets the https:// URLs Reloop puts in links and emails.

To move the port later, change RELOOP_PROXY_PORT in /opt/reloop/.env and run reloop restart proxy.

What your proxy needs to do

HostnameForward toTLS certificate
reloop.example.comhttp://127.0.0.1:8080Issued and renewed by your proxy
link.reloop.example.comhttp://127.0.0.1:8080Issued and renewed by your proxy
Customer tracking domainshttp://127.0.0.1:8080Issued on demand, see below
  • Keep the original Host header. Reloop decides between the dashboard and the tracking site by hostname.
  • Pass WebSocket upgrades. Live email logs, the inbox and template collaboration use them.
  • Set X-Forwarded-For and X-Forwarded-Proto.

inbound.reloop.example.com needs no proxy route. It is only an MX target, and Reloop serves port 25 directly.

Custom tracking domains. A customer can point their own hostname, such as track.customer.com, at link.reloop.example.com with a CNAME, and it needs a certificate as soon as it is added. Reloop answers Caddy's on-demand TLS check at https://reloop.example.com/api/domain/v1/caddy/ask?domain=<hostname>, returning 200 for an active tracking domain and 404 otherwise, so Caddy issues these automatically. Nginx and Traefik have no equivalent: add a route and certificate for each tracking domain yourself, or don't offer custom tracking domains.

Caddy

{
	on_demand_tls {
		ask https://reloop.example.com/api/domain/v1/caddy/ask
	}
}

reloop.example.com, link.reloop.example.com {
	reverse_proxy 127.0.0.1:8080
}

https:// {
	tls {
		on_demand
	}
	reverse_proxy 127.0.0.1:8080
}

Merge on_demand_tls into your existing global options block. The https:// block catches every hostname your other sites don't claim, which is what lets tracking domains work; if you already have a catch-all site, route tracking domains from there instead. Caddy keeps the Host header, sets the forwarded headers and passes WebSockets without extra configuration, and renews its certificates itself.

Nginx

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name reloop.example.com link.reloop.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name reloop.example.com link.reloop.example.com;

    ssl_certificate     /etc/letsencrypt/live/reloop.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/reloop.example.com/privkey.pem;

    client_max_body_size 25m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

The certificate must cover both hostnames, for example certbot certonly --nginx -d reloop.example.com -d link.reloop.example.com. Certbot's timer renews it. client_max_body_size raises Nginx's 1 MB default: uploads and email attachments go up to 10 MB, and attachments grow by about a third when base64-encoded in an API request.

Traefik

Traefik usually runs in a container, where 127.0.0.1 is the Traefik container itself. Put Traefik on Reloop's Docker network and forward to the Reloop proxy container by name. In Traefik's compose file:

services:
  traefik:
    networks:
      - reloop

networks:
  reloop:
    external: true

Then add a router and service, here with the file provider:

http:
  routers:
    reloop:
      rule: Host(`reloop.example.com`) || Host(`link.reloop.example.com`)
      entryPoints:
        - websecure
      service: reloop
      tls:
        certResolver: letsencrypt
  services:
    reloop:
      loadBalancer:
        servers:
          - url: http://reloop-proxy:80

Replace websecure and letsencrypt with your own entry point and certificate resolver. Traefik passes the Host header and WebSockets by default and renews certificates through the resolver.

The same applies to Caddy or Nginx running in a container: join the reloop network and forward to http://reloop-proxy:80 instead of 127.0.0.1.


TLS certificates and mail ports

Hostname or portWith the bundled CaddyBehind your own proxy
reloop.example.comLet's Encrypt, issued and renewed by CaddyYour proxy
link.reloop.example.comLet's Encrypt, issued and renewed by CaddyYour proxy
Customer tracking domainsIssued on demand and renewed by CaddyCaddy on demand, or by hand
Port 25, 587, 465Self-signed certificate, see belowSame

The mail ports are published by Docker directly and never pass through the HTTP proxy, in either setup.

PortUsed byProtocol
25Other mail servers delivering to your verified domainsSMTP with STARTTLS
587Your applications submitting mail to ReloopSMTP submission with STARTTLS
465Your applications submitting mail to ReloopThe same STARTTLS listener as 587, not implicit TLS

The SMTP services generate a self-signed certificate, since no certificate is mounted into them. Most sending servers delivering to port 25 accept that for opportunistic TLS. For submission, use port 587 with STARTTLS: a client set to implicit TLS (often labelled SSL/TLS or SMTPS) on 465 will fail, and a client that verifies certificates has to be told to accept this one.


First sign-in

The installer prints a one-time administrator setup key when a new installation finishes. It also saves the key to /opt/reloop/admin-setup.key, a root-only file, in case you need to retrieve it before completing setup.

  1. Open https://your-domain/dashboard/setup
  2. Enter the setup key
  3. Choose the first administrator's name, email and password
  4. Name the instance and first organization
  5. Leave Turn off public sign-ups selected for a private instance, or clear it if anyone should be able to register

The first account is created as a super-admin. Completing setup consumes the key, empties admin-setup.key, sets SETUP_MODE=false and closes the setup page. The installer only prints the key once.

To supply your own key, download the installer before running it:

curl -fsSL https://reloop.sh/install.sh -o install.sh
sudo bash install.sh --admin-key='your-long-random-key'

RELOOP_ADMIN_EMAIL is only the TLS contact address; it does not preselect or create the administrator account. Self-host setup uses a password, not an emailed code. Reloop Cloud's one-time-code sign-in is unrelated and unchanged.

If the setup flow is unavailable after an account already exists, apps/backend/admin/scripts/promote-admin.ts remains available as a break-glass recovery tool. It is not the normal first-run setup path.

To change registration or organization creation later, edit these values in /opt/reloop/.env and run reloop restart auth:

VariableEffect
DISABLE_SIGNUP=trueNo new accounts on any sign-up path. Existing users still sign in, and invited addresses can still join
DISABLE_ORG_CREATION=trueNobody can create further organizations

The installer keeps both values across re-runs, so an update will not reopen registration. See Closing registration.

If sign-in succeeds but the dashboard then answers 401 on every protected page, the services cannot reach the auth service over your public origin. Set AUTH_INTERNAL_BASE_URL=http://auth:8000 in /opt/reloop/.env and run reloop restart. See session validation between services.


Configuration reference

Everything lives in /opt/reloop/.env. The installer writes it and keeps your edits across re-runs. After changing a value, run reloop restart, or reloop restart <service> for one service.

VariablePurpose
RELOOP_VERSIONImage tag deployed for every service
RELOOP_DOMAINDashboard and API hostname
RELOOP_TRACKING_HOST / RELOOP_INBOUND_HOSTlink. and inbound. hostnames, derived from RELOOP_DOMAIN
RELOOP_ADMIN_EMAILLet's Encrypt contact address only
RELOOP_PUBLIC_IPAddress printed in the DNS records and SPF
RELOOP_HTTPSWhether Reloop is reached over https://
RELOOP_EXTERNAL_PROXY / RELOOP_PROXY_PORTRun behind your own proxy, and the local port it forwards to
BASE_URL / TRACKING_BASE_URLPublic origins used in links and emails, generated from the values above
RELOOP_SITE_ADDRESS / RELOOP_TRACKING_SITE_ADDRESSSite addresses for the bundled Caddy, generated
POSTGRES_*, REDIS_PASSWORD, BETTER_AUTH_SECRET, RELOOP_INTERNAL_SECRET, TRACKING_SECRET, PREFERENCES_SECRET, WEBHOOK_ENCRYPTION_KEYGenerated once and kept across re-runs. Back them up with the file and don't replace them by hand
SETUP_MODE / ADMIN_SETUP_KEY_FILEFirst-run setup state and the setup key path inside the auth container
DISABLE_SIGNUP / DISABLE_ORG_CREATIONClose registration and organization creation
AUTH_INTERNAL_BASE_URLInternal origin for session checks between services
APP_NAMEName used in system email. See Branding system email
RELOOP_API_KEY, RELOOP_SENDER_DOMAIN, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECUREHow system email is delivered. See How system email is delivered
SMTP_HOSTNAMEHostname senders connect to for submission, shown in the dashboard
COMPOSE_PROFILES, S3_*File uploads, see File uploads
DNS_RESOLVERSResolvers used to verify DKIM, SPF, DMARC and MX records

Per-service variables are documented under Backend services.


Running it again

Re-running the installer is safe. It detects the existing installation and offers to abort, reconfigure (re-ask the domain and HTTPS settings while keeping data and secrets), or redeploy with the existing configuration. Secrets, volumes and the database are never regenerated or dropped, and a database dump is taken before any schema change.


Updating and rolling back

reloop update

This pulls the images for RELOOP_VERSION, dumps the database to /opt/reloop/backups/pre-migration-<timestamp>.sql.gz, applies the schema and recreates the containers that changed. It does not touch docker-compose.yml, the Caddyfile or the reloop command. To pick up changes to those as well, re-run the install command and choose to redeploy with the existing configuration.

Reloop images are published as latest, and not every image has a per-release tag yet, so rolling back to an earlier image version is not supported. What you can roll back is the database, to the dump taken before an update:

cd /opt/reloop
ls -t backups/
reloop stop
docker compose up -d postgres
docker compose exec -T postgres psql -U reloop -d reloop \
  -c 'drop schema public cascade; create schema public authorization reloop'
gunzip -c backups/pre-migration-20260917101500.sql.gz |
  docker compose exec -T postgres psql -U reloop -d reloop -v ON_ERROR_STOP=1
reloop start

Run these as root, use the file name ls shows, and replace reloop after -U and -d if you changed the database user or name. This discards everything written since the dump was taken.


Unattended installs

Every prompt has an environment variable, so the installer can run without a terminal:

curl -fsSL https://reloop.sh/install.sh | sudo \
  RELOOP_NONINTERACTIVE=true \
  RELOOP_DOMAIN=reloop.example.com \
  RELOOP_ADMIN_EMAIL=admin@example.com \
  RELOOP_PUBLIC_IP=203.0.113.10 \
  bash
VariablePurpose
RELOOP_NONINTERACTIVENever prompt; fail if a required value is missing
RELOOP_DOMAINPrimary hostname
RELOOP_ADMIN_EMAILTLS contact email
RELOOP_ADMIN_SETUP_KEYOptional administrator setup key override; equivalent to --admin-key=
RELOOP_DB_NAME / RELOOP_DB_USERDatabase name and role
RELOOP_HTTPSyes or no. Behind your own proxy, whether that proxy serves HTTPS
RELOOP_EXTERNAL_PROXYyes to run behind your own reverse proxy instead of taking 80 and 443
RELOOP_PROXY_PORTLocal port your proxy forwards to (default 8080)
RELOOP_S3yes to configure object storage, no to disable file uploads
RELOOP_S3_ENDPOINT / RELOOP_S3_ACCESS_KEY / RELOOP_S3_SECRET_KEYObject storage credentials
RELOOP_S3_BUCKET / RELOOP_S3_REGIONBucket and region
RELOOP_PUBLIC_IPSkip public address detection
RELOOP_VERSIONImage tag to deploy (default latest)
RELOOP_EXISTINGabort, reconfigure or continue for a re-run
RELOOP_INSTALL_DIRInstallation directory (default /opt/reloop)

Was this page helpful?

Edit this page