Sign In

Documentation

Deployment

Container, fleet and environment configuration.

Source: api.packr.blueforge.studio/-/docs/deployment

Packr Registry β€” Deployment Guide

Fly.io and Vercel are retired β€” Phases 1 and 2 below are historical notes only. Both the registry and the dashboard run on forge-control behind Caddy (registry on app-3 since 2026-09-06; Fly decommissioned 2026-09-09, the dashboard left Vercel earlier). For current topology, deploys and secrets, start at 09-operations-guide.md and deployment-architecture.md. (blueforge.manifest.json still says "vercel": "packr-site" β€” a stale declaration nothing enforces.)

Architecture

Packr has two deployable components, both on forge-control app-3 (116.203.97.105) behind the host Caddy, deployed by docker compose from the forge-control worker (see 09-operations-guide.md):

        public DNS (verified 2026-09-24)
        packr.blueforge.studio    β†’ A 116.203.97.105 (app-3)
        api.packr.blueforge.studio β†’ A 116.203.97.105 (app-3)
                      β”‚
                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        Host Caddy (app-3) β€” TLS + routing   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Dashboard Site    β”‚          β”‚ Registry Server    β”‚
β”‚ (Next.js 16)      │─────────>β”‚ (Go binary)        β”‚
β”‚ :3555 (loopback)  β”‚ INTERNAL β”‚ :4873 (loopback)   β”‚
β”‚ packr.blueforge…  β”‚ API_SECRET api.packr.blue…    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚ postgres-shared  (app_packr_…)     β”‚
                     β”‚ fleet MinIO      (app-packr-…)     β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Secrets: forge vault (project packr-registry) β†’ rendered to /opt/packr-registry/registry.secrets.env / site.secrets.env on app-3.


Phase 1 (retired): Vercel + Supabase

The dashboard originally deployed to Vercel while the Go registry ran locally or on any VPS (Supabase for DB/blobs). Both halves moved to forge-control β€” the registry to app-3 on 2026-09-06, the site behind Caddy on the fleet β€” so those Vercel steps and env tables no longer describe anything deployable. Current procedures: 09-operations-guide.md; migration log: HANDOVER-registry-migration-to-forge-control.md.


Phase 2 (retired): Fly.io for Registry Server

The registry once ran as a Fly.io app (packr-registry) with fly secrets set for configuration. Fly was decommissioned 2026-09-09 after both halves moved to forge-control (registry on app-3, Postgres + fleet MinIO). The old fly.toml is kept in the repo for history only. Secrets now live in the forge vault (project packr-registry) rendered to the app-3 env files β€” see 09-operations-guide.md Β§ "Secrets & env files"; migration log: HANDOVER-registry-migration-to-forge-control.md. The INTERNAL_API_SECRET pairing lesson from that era still holds: the secret must exist and match on both registry and site, or every login fails closed with a confusing error.


Email Configuration

Password reset emails are sent via a configurable email provider.

VariableDefaultDescription
EMAIL_PROVIDERlogEmail provider: resend or log
EMAIL_API_KEY(empty)API key for the chosen provider (required for resend)
EMAIL_FROMnoreply@packr.blueforge.studioSender address shown in emails

Providers

log (default) β€” Logs email content to stdout instead of sending. Safe for development and staging.

resend β€” Sends transactional email via Resend. Requires an API key and a verified sender domain.

# Development β€” emails logged to stdout (default)
EMAIL_PROVIDER=log

# Production β€” send via Resend
EMAIL_PROVIDER=resend
EMAIL_API_KEY=re_your_resend_api_key
EMAIL_FROM=noreply@yourdomain.com

The DASHBOARD_ORIGIN variable is used to construct the reset link in the email (e.g., https://packr.blueforge.studio/login/reset?token=...). Ensure it is set correctly in production.

(Deploy/custom-domain/cost instructions for this phase were retired with Fly β€” see the Phase 2 note above.)


Phase 3: Kubernetes (When Needed)

Move to K8s when you need:

  • 2 replicas with auto-scaling

  • Zero-downtime canary deployments
  • Enterprise customers requiring "runs in our cluster"
  • Traffic exceeding a single-host deployment
ProviderMin CostBest For
DigitalOcean K8s$24/moFirst K8s, simple
Hetzner + k3s$15/moCheapest, EU-based
GKE AutopilotPay-per-podScale-to-zero
AWS EKS$72/mo + nodesEnterprise/AWS

Helm Chart (Future)

When the time comes, a Helm chart would include:

  • Deployment (Go server, 2+ replicas)
  • Service + Ingress (TLS termination)
  • ConfigMap (non-secret env vars)
  • Secret (JWT_SECRET, DB passwords)
  • HPA (auto-scale on CPU/request count)
  • PodDisruptionBudget (HA guarantees)
# Future usage:
helm install packr ./charts/packr \
  --set supabase.projectId=your-id \
  --set supabase.dbPassword=your-pass \
  --set image.tag=v1.0.0

DNS Setup

RecordTypeValue
packr.blueforge.studioA116.203.97.105 β€” forge-control app-3, site behind Caddy
api.packr.blueforge.studioA116.203.97.105 β€” forge-control app-3, registry behind Caddy

(Both verified 2026-09-24. The former Vercel/Fly CNAMEs died with those providers.)


Environment Variable Reference

Registry Server (Go)

VariableRequiredDescription
PORTNo (4873)HTTP port
STORAGE_DRIVERNo (sqlite)sqlite, postgres, supabase
SUPABASE_PROJECT_IDIf supabaseSupabase project ID
SUPABASE_DB_PASSWORDIf supabaseDatabase password
BLOB_DRIVERNo (file)file, supabase, backblaze, s3
S3_ACCESS_KEYIf S3S3 access key
S3_SECRET_KEYIf S3S3 secret key
JWT_SECRETYesJWT signing key
SESSION_SECRETYesSession cookie key
ALLOW_REGISTRATIONNo (false)Allow public self-registration via /-/v1/login
INTERNAL_API_SECRETYes (dashboard)Shared secret for admin session creation
DASHBOARD_ORIGINNoAllowed CORS origin for admin API
MAX_TARBALL_SIZENo (5242880)Max tarball upload in bytes (default 5 MB)
DEFAULT_PACKAGE_VISIBILITYNo (public)Visibility given to a package on first publish: public or private. See the warning below before changing.
RATE_LIMIT_LOGINNo (10)Login rate limit/min
RATE_LIMIT_PUBLISHNo (30)Publish rate limit/min
LOG_LEVELNo (info)debug, info, warn, error

Dashboard Site (Next.js)

VariableRequiredDescription
PACKR_REGISTRY_URLYesRegistry server URL
NEXT_PUBLIC_SITE_URLYesThis site's public URL
SESSION_SECRETYesSession cookie key
INTERNAL_API_SECRETYesMust match registry's INTERNAL_API_SECRET
GITHUB_OAUTH_CLIENT_IDFor OAuthGitHub App client ID
GITHUB_OAUTH_CLIENT_SECRETFor OAuthGitHub App secret
STRIPE_SECRET_KEYFor billingStripe secret key
STRIPE_WEBHOOK_SECRETFor billingStripe webhook secret
STRIPE_SOLO_PRICE_IDFor billingSolo plan price ID
STRIPE_TEAM_PRICE_IDFor billingTeam plan price ID
DATABASE_URLOptionalDirect Postgres (Drizzle)

OAuth Configuration

Packr supports OAuth login via GitHub, Google, GitLab, and any OIDC-compliant provider. Set the env vars for the providers you want to enable. See docs/06-oauth.md for full details.

Auth Mode

VariableDefaultDescription
PACKR_AUTH_MODEhybridstandalone (password only), forge-auth (SSO only), hybrid (both)

All OAuth providers share one redirect base env var:

OAUTH_REDIRECT_BASE=https://packr.example.com  # public dashboard URL

Each provider's callback URL is built as {OAUTH_REDIRECT_BASE}/api/auth/callback/{provider} β€” configure this on the provider's OAuth app settings.

GitHub OAuth

docker run -p 4873:4873 \
  -e OAUTH_GITHUB_CLIENT_ID=Ov23liABCDEFGH123456 \
  -e OAUTH_GITHUB_CLIENT_SECRET=abc123def456... \
  -e OAUTH_REDIRECT_BASE=https://packr.example.com \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  packr-registry

Callback URL to configure in GitHub: https://packr.example.com/api/auth/callback/github

Google OAuth

docker run -p 4873:4873 \
  -e OAUTH_GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com \
  -e OAUTH_GOOGLE_CLIENT_SECRET=GOCSPX-abc123... \
  -e OAUTH_REDIRECT_BASE=https://packr.example.com \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  packr-registry

GitLab OAuth

docker run -p 4873:4873 \
  -e OAUTH_GITLAB_CLIENT_ID=abc123def456... \
  -e OAUTH_GITLAB_CLIENT_SECRET=gloas-abc123... \
  -e OAUTH_GITLAB_BASE_URL=https://gitlab.com \
  -e OAUTH_REDIRECT_BASE=https://packr.example.com \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  packr-registry

Generic OIDC (Keycloak, Okta, Azure AD, Authentik)

docker run -p 4873:4873 \
  -e OAUTH_OIDC_CLIENT_ID=packr \
  -e OAUTH_OIDC_CLIENT_SECRET=super-secret \
  -e OAUTH_OIDC_ISSUER_URL=https://auth.example.com/realms/myrealm \
  -e OAUTH_REDIRECT_BASE=https://packr.example.com \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  packr-registry

Forge-auth SSO

docker run -p 4873:4873 \
  -e PACKR_AUTH_MODE=forge-auth \
  -e FORGE_AUTH_URL=https://auth.blueforge.studio \
  -e FORGE_AUTH_CLIENT_ID=packr-registry \
  -e FORGE_AUTH_CLIENT_SECRET=forge-client-secret \
  -e OAUTH_REDIRECT_BASE=https://packr.example.com \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  packr-registry

Multiple Providers + Password Login

docker run -p 4873:4873 \
  -e PACKR_AUTH_MODE=hybrid \
  -e OAUTH_GITHUB_CLIENT_ID=... \
  -e OAUTH_GITHUB_CLIENT_SECRET=... \
  -e OAUTH_GOOGLE_CLIENT_ID=... \
  -e OAUTH_GOOGLE_CLIENT_SECRET=... \
  -e OAUTH_REDIRECT_BASE=https://packr.example.com \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  packr-registry

Credential Store (CLI)

VariableDefaultDescription
PACKR_CREDENTIAL_STOREfileWhere CLI stores tokens: file, keychain, secret-service

This variable is used by the CLI, not the registry server.


Security Configuration

Generating Secrets

All secrets should be cryptographically random. Use openssl rand -hex 32 for each:

openssl rand -hex 32   # once per secret: JWT_SECRET, SESSION_SECRET, INTERNAL_API_SECRET

Store them in the forge vault (project packr-registry); deploys render them to /opt/packr-registry/registry.secrets.env (worker path) and /opt/packr-registry/registry.env (manual path) on app-3. The site's copy of INTERNAL_API_SECRET lives in /opt/packr-registry/site.secrets.env and must be the same value as the registry's. Rotation procedure (update both env files, then recreate the container β€” docker restart silently keeps the old env): 09-operations-guide.md Β§ "Secrets & env files".

Key Security Variables

VariableWhereNotes
INTERNAL_API_SECRETRegistry + SiteMust match on both sides. Used to authenticate dashboard β†’ registry admin calls via X-Internal-Secret header.
ALLOW_REGISTRATIONRegistryDefaults to false. Set to true only if you want open sign-up.
DASHBOARD_ORIGINRegistryCORS origin allowed to call admin endpoints. Defaults to https://packr.blueforge.studio. Set to your dashboard URL.
MAX_TARBALL_SIZERegistryDefaults to 5 MB (5242880 bytes). Increase only if you publish large packages.
DEFAULT_PACKAGE_VISIBILITYRegistrypublic (default) or private. Applies on first publish only. Read the warning under Package visibility first β€” enabling it without working install credentials breaks consumers.

Password Requirements

All user passwords must meet these requirements:

  • Minimum 10 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one digit
  • At least one special character

Post-Deployment Checklist

After deploying with new secrets, complete these steps:

  • Verify INTERNAL_API_SECRET is the same value in the forge vault (project packr-registry), /opt/packr-registry/registry.secrets.env (and registry.env), and the site's /opt/packr-registry/site.secrets.env
  • Revoke any existing tokens that were issued before the security hardening (JWT algorithm and hash changes invalidate old tokens)
  • Confirm ALLOW_REGISTRATION=false is set (or intentionally omitted β€” it defaults to false) unless open sign-up is desired
  • Set DASHBOARD_ORIGIN to your actual dashboard URL if it differs from the default
  • Test dashboard login end-to-end after deployment
  • Rotate JWT_SECRET and SESSION_SECRET if they were previously auto-generated (random at startup means they change on every restart)

Package visibility

Package reads are anonymous by default. This is deliberate: visibility was designed as opt-in privacy so that adding the feature broke no existing install (see docs/superpowers/plans/2026-08-22-package-visibility.md). The packages.visibility column defaults to public and the migration backfilled every existing row the same way.

The consequence is worth stating plainly, because the default is easy to leave in place without realising its reach: unless you have opted individual packages in, every package in your registry β€” metadata and tarballs β€” is readable without a token:

curl -s -o /dev/null -w '%{http_code}\n' \
  https://your-registry/@scope%2fpackage        # 200, no credentials

For a registry deployed as a private replacement for Artifactory or GitHub Packages, that is often not what is wanted once the migration settles β€” the default optimises for not breaking installs, not for confidentiality. Audit what you are actually exposing with:

SELECT visibility, count(*) FROM packages GROUP BY visibility;

Making packages private

Do this in order. The trap is that because reads have always been anonymous, consumers may never have needed a working token β€” so their credentials can be long expired without anyone noticing, and making packages private is the moment that surfaces.

  1. Verify install credentials actually authenticate. GET /-/whoami accepts stored tokens as well as JWTs and reports which path accepted it:

    curl -s -H "Authorization: Bearer $PACKR_TOKEN" https://your-registry/-/whoami
    # {"auth":"token","name":"ci-publish","permissions":["read","publish"], ...}
    

    "auth":"token" means it resolves against the tokens table β€” the path installs and publishes use. A 401 naming the signing key means the token predates the last JWT_SECRET rotation; re-issue it.

  2. Stop the exposure growing. Set DEFAULT_PACKAGE_VISIBILITY=private so newly published packages start private. Existing packages are unaffected.

  3. Migrate existing packages in batches, watching for install failures between batches:

    packr-cli visibility npm/@scope/package private
    

Existing packages are never changed implicitly, and a republish never re-opens a package you made private β€” visibility is applied on first publish only.