Workload · Running your own API workbench

Hoppscotch self hosted: your team's own API workbench on a dedicated server

To run Hoppscotch self hosted, you start two containers: the Community Edition all-in-one image (web interface, admin pages and API server in one) and a Postgres database beside it. A prebuilt image needs very little, since the official prerequisites name a single core and 2 GB of memory for hosting it. The work is in the order of steps: sign-in configured, you made admin while nobody else can reach it, and only then a public address with HTTPS.

The short version
  1. Two containers: hoppscotch/hoppscotch and postgres:15
  2. No passwords: sign-in is an emailed link (SMTP needed) or a Google, GitHub or Microsoft account
  3. Whoever signs in to the admin dashboard first is the admin, so do it through a tunnel
  4. Back up the database and the .env file together; one is useless without the other
  5. The Community Edition is MIT-licensed

What you are setting up

Hoppscotch is an open-source tool for building and testing API requests; hosting it yourself keeps your team's collections and history on your own machine. The install guide offers two shapes:

  • The all-in-one (AIO) container, hoppscotch/hoppscotch. With ENABLE_SUBPATH_BASED_ACCESS on, one port serves it all, under /, /admin and /backend, and the desktop app works (the docs require subpath mode for it). We use this one.
  • Three separate images, one each for frontend, backend and admin, each on its own port; you wire up the routing.

Postgres stores everything in both. Building from source would want 4 cores plus 4 GB. No per-user figure exists, so plan disk around request history (admins may stop recording it).

The server that fits

Your collections hold API keys, bearer tokens and internal URLs. With a dedicated server, the entire machine is yours: nobody else on it, root to it, and a port nobody meters. For a team workbench, in our view:

  • A two-processor server if Hoppscotch will share the box with other team tools, such as a Jenkins server or a load-testing server. These are rack servers and blades, so they come with the out-of-band console: the screen and the power button even when the system will not start.
  • A small cartridge if the workbench is all it runs. Among cartridges, the console comes only on the m510 and m710x, and the others can only be powered on and off, so check the model on the order page.

Leave out the ARM cartridge, which installs Ubuntu 16.04 or 18.04 only; these steps assume Ubuntu 24.04. A ready server is yours within 30 minutes of approval; a made-to-order one takes hours rather than minutes.

You manage the server; we keep the hardware and the network running.

Settings you have to get right

SettingWhat it doesWhere we checked
DATABASE_URLHow the backend reaches Postgresinstall guide
DATA_ENCRYPTION_KEYA 32-character key that encrypts sensitive settings in the databaseinstall guide
JWT_SECRETNot set by you any more: generated at first start and stored encrypted in the database, with a session secretsrc/infra-config/helper.ts (backend, 2026.9.0)
WHITELISTED_ORIGINSOrigins the backend accepts for cross-origin calls in production.env.example, src/main.ts
VITE_* URLsWhere the browser finds the app, dashboard, GraphQL, its WebSocket and the REST APIinstall guide
ENABLE_SUBPATH_BASED_ACCESSOne address with /admin and /backend instead of three portsinstall guide

The guide's example URLs use separate ports. Our subpath values come from reading aio-subpath-access.Caddyfile, which drops the /backend prefix before forwarding. Keep values unquoted, as the docs warn. Because aio_run.mjs bakes the VITE values in at every container start, editing them means recreating the container, never rebuilding.

Getting it running

  1. Install Docker from docs.docker.com first; this assumes a recent Engine (28+) with Compose included as a plugin.
  2. Paste the block as root. It switches the firewall on with only SSH allowed (for IPv6 as well) before any container starts.
  3. Running this block a second time leaves .env alone: it writes the file only when it is missing, because a new DATA_ENCRYPTION_KEY would leave the settings already stored in the database unreadable. Never delete or regenerate .env on a running install.
  4. It creates .env with mode 600 while still empty, then fills it. Passwords enter through a heredoc, so ps and shell history never show them; docker inspect does, to docker group members, so keep that group small.
  5. The compose file is our own. The repo's version builds from source and exposes Postgres on 5432 with the password testpass; ours pulls a fixed tag, keeps Postgres private and listens on 127.0.0.1.
  6. Migrations have to happen first: the AIO image's start command in prod.Dockerfile skips them. We run the documented pnpm exec prisma migrate deploy in a throwaway container, named with -f so the command works wherever you are.
  7. The final lines depend on each other. If Docker is older than Engine 28, the database never turns healthy or a migration fails, the chain stops there and nothing later runs. Seeing ok means every step worked and the app is reachable from this machine alone.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
mkdir -pm 750 /srv/hoppscotch; cd /srv/hoppscotch
if [ ! -e .env ]; then
install -m 600 /dev/null .env
K=$(openssl rand -hex 24); H=http://localhost:3280
cat >> .env <<EOF
HOPP_TAG=2026.9.0
DBPW=$K
DATABASE_URL=postgresql://hopp:$K@database:5432/hopp
DATA_ENCRYPTION_KEY=$(openssl rand -hex 16)
ENABLE_SUBPATH_BASED_ACCESS=true
WHITELISTED_ORIGINS=$H,app://localhost_3200,app://hoppscotch
VITE_BASE_URL=$H
VITE_SHORTCODE_BASE_URL=$H
VITE_ADMIN_URL=$H/admin
VITE_BACKEND_GQL_URL=$H/backend/graphql
VITE_BACKEND_WS_URL=ws://localhost:3280/backend/graphql
VITE_BACKEND_API_URL=$H/backend/v1
EOF
fi
cat > compose.yaml <<'EOF'
services:
  database:
    image: postgres:15
    restart: unless-stopped
    environment: {POSTGRES_USER: hopp, POSTGRES_DB: hopp, POSTGRES_PASSWORD: "${DBPW}"}
    volumes: [pd:/var/lib/postgresql/data]
    healthcheck: {test: [CMD-SHELL, pg_isready -U hopp], interval: 5s, retries: 10}
  app:
    image: hoppscotch/hoppscotch:${HOPP_TAG}
    restart: unless-stopped
    env_file: .env
    depends_on: {database: {condition: service_healthy}}
    ports: ["127.0.0.1:3280:80"]
volumes: {pd: {}}
EOF
dc="docker compose -f /srv/hoppscotch/compose.yaml"
[ $(docker version -f '{{.Server.Version}}' | cut -d. -f1) -ge 28 ] &&
$dc up -d --wait database &&
$dc run --entrypoint sh --rm app -ec "pnpm exec prisma migrate deploy" &&
$dc up -d && $dc port app 80 | grep -qx '127.0.0.1:3280' && echo ok

Why 127.0.0.1 matters: if a port line names no address, Docker listens on 0.0.0.0 and [::]. IPv4 connections to it slip past ufw entirely. IPv6 ones slip past too, but only if the Docker daemon has IPv6 enabled in its settings (merely having ip6tables does not count); if not, a docker-proxy process answers on [::] and ufw still applies. From Engine 28 onward, a loopback binding stays off the network (moby issue 45610).

Making yourself the admin, privately

Do this while the app is still invisible. From your own computer, open a tunnel through your normal login with ssh -N -L 3280:127.0.0.1:3280 you@server.example.com, and in your browser go to http://localhost:3280/admin.

  1. The dashboard starts with onboarding, which asks how people will sign in. Hoppscotch has no password login: the backend's routes in src/auth/auth.controller.ts cover only an emailed magic link and GitHub, Google and Microsoft accounts.
  2. The emailed link is the easiest first choice. Give it SMTP details (an smtps:// address and sender, or a custom host and port). According to src/auth/auth.service.ts, a link asked for from the admin dashboard starts with VITE_ADMIN_URL, and one asked for from the app with VITE_BASE_URL; right now both are the tunnel.
  3. For GitHub, Google or Microsoft, the docs' callback is the backend address plus /v1/auth/, the provider and /callback; in subpath mode we take that to be http://localhost:3280/backend/v1/auth/google/callback for now.
  4. Sign in. The setup guide is clear that the first person to log in through the admin dashboard is made administrator.

The reason for the tunnel: src/infra-config/infra-config.service.ts allows onboarding to be run again as long as no user exists, so a stranger reaching /admin before you could choose the sign-in providers. Also in settings: switching off Data sharing ends the weekly anonymous ping covered on the telemetry page.

Opening it to your team

Caddy handles HTTPS for you. Ubuntu 24.04 ships it in universe; 22.04 needs Caddy's own apt repository. Running straight on the host, it needs ufw to open 80 and 443 over both IPv4 and IPv6. Keep the order: DNS, certificate, settings. The second half is one chain, so a missing certificate or a sed that changed nothing halts it before the app is touched, and ok only shows up after a clean run.

# Step 1, DNS: A and AAAA records for hoppscotch.example.org pointing here. Wait for both.
ufw allow 80,443/tcp
apt install -y caddy
cat > /etc/caddy/Caddyfile <<'EOF'
hoppscotch.example.org {
	@ours remote_ip 198.51.100.0/24 2001:db8:20::/48
	handle @ours {
		reverse_proxy 127.0.0.1:3280
	}
	handle {
		respond 403
	}
}
EOF
systemctl reload caddy
# Steps 2 and 3, certificate then config, chained: a failure stops the rest
cd /srv/hoppscotch &&
curl -sI https://hoppscotch.example.org | head -1 | grep -q '^HTTP' &&
sed -i 's|http://localhost:3280|https://hoppscotch.example.org|; s|ws://localhost:3280|wss://hoppscotch.example.org|' .env &&
grep -qx 'VITE_BACKEND_WS_URL=wss://hoppscotch.example.org/backend/graphql' .env &&
docker compose -f /srv/hoppscotch/compose.yaml up -d --force-recreate app && echo ok
  • Live updates need WebSockets. The browser holds a GraphQL WebSocket at VITE_BACKEND_WS_URL; Caddy's plain reverse_proxy passes it, just as the image's internal Caddyfile does for /backend. On nginx, forward the Upgrade and Connection headers.
  • Decide who can reach it. On the Community Edition, sign-up is open to anyone who reaches it, says the admin dashboard page; restricting sign-ups by email domain is Enterprise-only. The remote_ip line admits only your office and VPN ranges; swap in your own.
  • Fix the sign-in callbacks. Each provider now needs https://hoppscotch.example.org/backend/v1/auth/google/callback (or github, microsoft), which, like the tunnel version, is how we read the Caddyfile. Enter it in the dashboard and on the provider's side. Everyone logs in again, because the old session cookies were tied to the localhost address of the tunnel. New tokens name the public address as their issuer (auth.service.ts), and with https in that address helper.ts marks cookies secure.
  • Where proxied requests go. If someone turns on the in-app proxy, .env.example suggests proxy.hoppscotch.io by default; change it from the admin pages when requests must stay in-house.

Keeping a copy you can restore

All your data is in Postgres, yet a dump alone will not bring the instance back: stored settings, sign-in and SMTP secrets included, are locked with DATA_ENCRYPTION_KEY from .env, and helper.ts throws a bad-key failure if decryption fails. So the nightly job keeps dump, .env and compose file together, and reads each dump back.

install -d -m 700 /srv/hoppscotch-backups
install -m 700 /dev/null /srv/hoppscotch/save.sh
cat > /srv/hoppscotch/save.sh <<'EOF'
set -euo pipefail
umask 077
out=/srv/hoppscotch-backups; day=$(date +%F)
dc="docker compose -f /srv/hoppscotch/compose.yaml"
$dc exec -T database pg_dump -U hopp -Fc hopp > $out/hopp-$day.dump
$dc exec -T database pg_restore --list < $out/hopp-$day.dump > /dev/null
cp /srv/hoppscotch/.env $out/env-$day
cp /srv/hoppscotch/compose.yaml $out/compose-$day.yaml
echo "saved $day ok"
EOF
printf '%s\n' '45 2 * * * root umask 077; bash /srv/hoppscotch/save.sh > /srv/hoppscotch-backups/last.log 2>&1' \
  > /etc/cron.d/hoppscotch-save

Then copy /srv/hoppscotch-backups somewhere else: copies kept on one machine are lost with it. Not saved: the desktop-app signing key in /data/webapp-server, recreated when absent per prod.Dockerfile; desktop users can give /data a volume and include it.

  1. Restoring on the same server: first docker compose -f /srv/hoppscotch/compose.yaml stop app.
  2. Save what is there now under a name of its own first: `umask 077; docker compose -f /srv/hoppscotch/compose.yaml exec -T database pg_dump -U hopp -Fc hopp > /srv/hoppscotch-backups/before-restore.dump && cat /srv/hoppscotch-backups/before-restore.dump | docker compose -f /srv/hoppscotch/compose.yaml exec -T database pg_restore --list --file=/dev/null && echo ok`. Continue only once it has printed ok.
  3. Load the dump you want (the .env saved with it must be the one in place): cat /srv/hoppscotch-backups/hopp-DATE.dump | docker compose -f /srv/hoppscotch/compose.yaml exec -T database pg_restore -U hopp -d hopp --clean --if-exists.
  4. Migrate and start, chained: docker compose -f /srv/hoppscotch/compose.yaml stop app && docker compose -f /srv/hoppscotch/compose.yaml run --entrypoint sh --rm app -ec "pnpm exec prisma migrate deploy && echo migrated" && docker compose -f /srv/hoppscotch/compose.yaml up -d, then log in to check. Changed your mind? Do steps 3 and 4 again with before-restore.dump.
  5. Fresh server? Place the backed-up .env at mode 600, plus compose.yaml, in /srv/hoppscotch before you start the database container for the first time: postgres takes POSTGRES_PASSWORD only on its first start with an empty volume. Then start the database, load without --clean, and run the migrate-and-start chain above.

Moving to a new release

Each GitHub release has a matching image tag. The edition table says quarterly, yet 2026.8.0 to 2026.9.0 (four tags) landed between late August and 30 September 2026. Major versions need migrations, per the upgrade page; we run them on every update, as only missing ones get applied.

  1. Read what changed, then run bash /srv/hoppscotch/save.sh and wait for its ok.
  2. Change HOPP_TAG in /srv/hoppscotch/.env to the new version and pull it with docker compose -f /srv/hoppscotch/compose.yaml pull app.
  3. Run the same chain: docker compose -f /srv/hoppscotch/compose.yaml stop app && docker compose -f /srv/hoppscotch/compose.yaml run --entrypoint sh --rm app -ec "pnpm exec prisma migrate deploy && echo migrated" && docker compose -f /srv/hoppscotch/compose.yaml up -d. Trouble? Put the previous tag back and restore the copy from step 1.

What the licence lets you do

The LICENSE file at the root of hoppscotch/hoppscotch is MIT, and the README says the same. The self-hosting docs add that the Community Edition can be used for personal and commercial projects. How we read it in general (not legal advice): run it, change it and share it, at work too. Hand copies to anyone and the copyright notice plus licence text go with them; nothing more is asked, and you never have to publish changes. The Enterprise Edition is a different, paid, open-core product with a licence key (SAML or OIDC single sign-on, audit trails, email-domain limits); there is no licence file for it in the repository; check the vendor's own terms.

Questions people ask

Why can't I log in to my new Hoppscotch instance?
Usually because no sign-in method is configured. There is no username and password: you need SMTP for the emailed link, or an OAuth app at GitHub, Google or Microsoft with the right callback address. Set one up during onboarding, through your SSH tunnel.
Is there a JWT_SECRET to set in the .env file?
Not in current versions. On first start the backend invents both its token-signing secret and its session secret and keeps them in the database in encrypted form. One more reason to keep DATA_ENCRYPTION_KEY safe next to every database backup.
Does my self-hosted Hoppscotch send data to Hoppscotch?
Only an anonymous weekly ping with the version plus counts of users and workspaces, per the telemetry page; nothing from your API requests. The Data sharing toggle in the admin settings stops it.

Give your team its own API workbench

Choose a server with a console, then work through the steps above from the firewall to the first saved backup.

Get a server for Hoppscotch

Read next