I self-hosted Hoppscotch — GitHub OAuth failed until the callback path was exact

I self-hosted Hoppscotch — GitHub OAuth failed until the callback path was exact

I wanted a private API workbench for REST and GraphQL instead of another hosted client. Hoppscotch Community Edition came up on Ubuntu 24.04, then GitHub login returned invalid redirect_uri until the callback matched /backend/v1/auth/github/callback.

 Hoppscotch self-hosted API workspace

Caption: One HTTPS hostname for the app, /admin, and /backend — collections stay on my Postgres, not in a vendor workspace.

Why I wanted this on my server

Staging URLs, auth headers, and half-real payloads do not belong in a personal Postman cloud. I wanted a browser API client on my VPS: REST, GraphQL, environments, shared collections, login I control.

Hoppscotch Community Edition does that. Self-hosting is not “a prettier curl.” It is where those collections live, who can open them, and whether I can restore Postgres after a bad night.

What I actually installed

Ubuntu 24.04, Docker Compose, the official all-in-one Hoppscotch image, Postgres 17, a one-shot Prisma migrate container, Nginx. Subpath mode so one domain covers:

  • https://hoppscotch.example.com
  • https://hoppscotch.example.com/admin
  • https://hoppscotch.example.com/backend

AIO binds to 127.0.0.1:3080. 1–2 GB RAM is enough for a small team. SSD for Postgres.

Where it broke

On a fresh Ubuntu 24.04 box this install is famous for OAuth callback mismatches.

The UI loaded. Admin opened. GitHub login came back invalid redirect_uri. I had registered https://hoppscotch.example.com/auth/github/callback like a normal app. Hoppscotch in subpath mode wants the backend path:

GITHUB_CALLBACK_URL=https://hoppscotch.example.com/backend/v1/auth/github/callback
GITHUB_SCOPE=user:email

Scheme, host, and path have to match the GitHub OAuth App exactly. After I fixed the callback, login worked.

If the app boots with a missing migration, I inspect the migrate job and re-run:

docker compose run --rm --entrypoint sh hoppscotch -c "pnpm exec prisma migrate deploy"
docker compose logs hoppscotch-migrate

 Hoppscotch Docker stack architecture

Caption: Nginx on 443. AIO, migrate job, and Postgres stay private.

Prerequisites

Docker Engine + Compose v2, Nginx, Certbot, an OAuth app if the team logs in through GitHub/Google/Microsoft. Official env files want no quotes and no spaces around =. DATA_ENCRYPTION_KEY is 32 characters; lose it and stored secrets in the DB are junk.

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg git ufw nginx certbot python3-certbot-nginx openssl

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

The working install

1. Install Docker Engine

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version

2. Create the project directory

sudo mkdir -p /opt/hoppscotch/backups
sudo chown -R "$USER":"$USER" /opt/hoppscotch
chmod 700 /opt/hoppscotch/backups
cd /opt/hoppscotch

3. Generate secrets and create .env

openssl rand -hex 16 is 32 hex characters — that matches Hoppscotch’s encryption key length.

cd /opt/hoppscotch

POSTGRES_PASSWORD_VALUE="$(openssl rand -base64 36 | tr -d '\n')"
DATA_ENCRYPTION_KEY_VALUE="$(openssl rand -hex 16)"

cat > .env <<EOF
POSTGRES_USER=hoppscotch
POSTGRES_PASSWORD=${POSTGRES_PASSWORD_VALUE}
POSTGRES_DB=hoppscotch
DATABASE_URL=postgresql://hoppscotch:${POSTGRES_PASSWORD_VALUE}@hoppscotch-db:5432/hoppscotch?connect_timeout=300

HOPP_AIO_ALTERNATE_PORT=80
DATA_ENCRYPTION_KEY=${DATA_ENCRYPTION_KEY_VALUE}
WHITELISTED_ORIGINS=https://hoppscotch.example.com,app://localhost_3200,app://hoppscotch

VITE_BASE_URL=https://hoppscotch.example.com
VITE_SHORTCODE_BASE_URL=https://hoppscotch.example.com
VITE_ADMIN_URL=https://hoppscotch.example.com/admin
VITE_BACKEND_GQL_URL=https://hoppscotch.example.com/backend/graphql
VITE_BACKEND_WS_URL=wss://hoppscotch.example.com/backend/graphql
VITE_BACKEND_API_URL=https://hoppscotch.example.com/backend/v1
VITE_APP_TOS_LINK=https://hoppscotch.example.com/terms
VITE_APP_PRIVACY_POLICY_LINK=https://hoppscotch.example.com/privacy

ENABLE_SUBPATH_BASED_ACCESS=true
EOF

chmod 600 .env
nano .env

If you split app, admin, and backend across hostnames, every VITE_* URL and WHITELISTED_ORIGINS has to match those hostnames. I stayed on one domain.

4. Create the Docker Compose file

services:
  hoppscotch:
    image: hoppscotch/hoppscotch:latest
    restart: unless-stopped
    env_file: ./.env
    depends_on:
      hoppscotch-db:
        condition: service_healthy
      hoppscotch-migrate:
        condition: service_completed_successfully
    ports:
      - "127.0.0.1:3080:80"
    healthcheck:
      test: ["CMD-SHELL", "wget -q --tries=1 --spider http://localhost || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 45s

  hoppscotch-migrate:
    image: hoppscotch/hoppscotch:latest
    env_file: ./.env
    depends_on:
      hoppscotch-db:
        condition: service_healthy
    entrypoint: ["/bin/sh", "-c"]
    command: "pnpm exec prisma migrate deploy"
    restart: "no"

  hoppscotch-db:
    image: postgres:17
    restart: unless-stopped
    user: postgres
    env_file: ./.env
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Community Edition can also run as separate frontend/backend/admin containers. AIO is enough for a first private box. Pin tags after a known-good boot.

5. Start Hoppscotch

cd /opt/hoppscotch
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=2m hoppscotch-migrate hoppscotch

The migrate service must finish pnpm exec prisma migrate deploy before AIO stays up. DATABASE_URL uses the Compose hostname hoppscotch-db, and that password must match POSTGRES_PASSWORD.

6. Configure Nginx and HTTPS

Create /etc/nginx/sites-available/hoppscotch.example.com:

server {
    listen 80;
    listen [::]:80;
    server_name hoppscotch.example.com;

    client_max_body_size 50M;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        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 "upgrade";
        proxy_pass http://127.0.0.1:3080;
    }
}

Upgrade and Connection matter. GraphQL over WebSocket uses VITE_BACKEND_WS_URL with wss:// on HTTPS. Without those headers, the page loads and API calls die quietly.

sudo ln -s /etc/nginx/sites-available/hoppscotch.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d hoppscotch.example.com

Keep in mind before deploy

If the UI loads but GraphQL and REST from the app fail, the VITE_BACKEND_* URLs are usually still localhost or missing /backend. Subpath mode is not optional once you chose it: app, admin, and API URLs have to agree.

 Hoppscotch OAuth and admin flow

Caption: Callback URL first, then one test account, then anyone else.

I configure SMTP from admin or env only after login works. Some VPS providers block outbound 25; use the relay they document.

First workspace I actually used

Health checks and unauthenticated /version routes first. Dev and staging environments with fake tokens. One REST request, one GraphQL query. Production secrets stay out until I have proven OAuth, backups, and who is admin.

Restart the stack once and confirm collections survive. If a restore comes back empty, I dumped the wrong Compose project or database name.

Backup, expose, next step

cd /opt/hoppscotch
mkdir -p backups
BACKUP_DATE="$(date +%F-%H%M%S)"

docker compose exec -T hoppscotch-db pg_dump -U hoppscotch hoppscotch > "backups/hoppscotch-postgres-${BACKUP_DATE}.sql"
tar czf "backups/hoppscotch-config-${BACKUP_DATE}.tar.gz" compose.yaml .env
sha256sum backups/hoppscotch-*-"${BACKUP_DATE}".* > "backups/hoppscotch-${BACKUP_DATE}.sha256"
rsync -avh /opt/hoppscotch/backups/ backup-user@backup.example.com:/srv/backups/hoppscotch/

Restore on a spare host: same .env (especially DATA_ENCRYPTION_KEY), import Postgres, start Compose, GitHub login, admin, collections.

 Hoppscotch backup routine

Caption: Dump, Compose, encryption key, checksum, off-server copy. The key is not optional.

What I have running now is Hoppscotch on one HTTPS hostname, AIO + Postgres, GitHub OAuth with the backend callback. Next I add a small internal collection convention and a restore drill before this becomes the team’s source of API examples.

Did you hit the same wall?

I got stuck on GitHub invalid redirect_uri until the callback was https://hoppscotch.example.com/backend/v1/auth/github/callback. Did you hit the same thing, or a different one? Tell me in the comments. I read them.

Need this done on your server?

I deploy and harden Laravel/CodeCanyon apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.