Files
starr-philproperties/SELF_HOSTING.md
T
2026-08-30 11:34:26 +08:00

9.0 KiB

Self-Hosting new_starr (Docker Compose) — Step-by-Step

Video tutorial soon

This stack bundles everything: PostgreSQL, Valkey (Redis), Garage (S3), the API, and the frontend. Battle-tested end-to-end on a completely fresh Debian machine on 2026-08-29 every gotcha below actually happened once and is now fixed or documented.


0. What you need

  • Docker Engine + Compose plugin
  • Node.js 22 + pnpm (only needed once, to build the frontend bundle)
  • Git access to this repo

1. Install Docker

This block auto-detects debian vs ubuntu from /etc/os-release and probes Docker's repo for your codename before adding it — if your codename is too new and 404s (interim/dev releases lag Docker's repo by months), it automatically falls back to the latest codename Docker actually publishes for your distro. No manual edits needed:

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings

. /etc/os-release
DOCKER_DISTRO="$ID"                      # debian or ubuntu
DOCKER_CODENAME="$VERSION_CODENAME"

# Fallback list, newest first, per distro — used only if the detected codename 404s
case "$DOCKER_DISTRO" in
  ubuntu) FALLBACKS="noble jammy focal" ;;
  debian) FALLBACKS="bookworm bullseye" ;;
  *) echo "Unsupported distro '$DOCKER_DISTRO' — Docker only publishes debian/ubuntu repos." >&2; exit 1 ;;
esac

if ! curl -fsSL -o /dev/null "https://download.docker.com/linux/${DOCKER_DISTRO}/dists/${DOCKER_CODENAME}/Release"; then
  echo "Docker has no repo for ${DOCKER_DISTRO} ${DOCKER_CODENAME} yet, falling back..." >&2
  for fb in $FALLBACKS; do
    if curl -fsSL -o /dev/null "https://download.docker.com/linux/${DOCKER_DISTRO}/dists/${fb}/Release"; then
      DOCKER_CODENAME="$fb"
      break
    fi
  done
  echo "Using ${DOCKER_DISTRO} ${DOCKER_CODENAME}" >&2
fi

curl -fsSL "https://download.docker.com/linux/${DOCKER_DISTRO}/gpg" | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${DOCKER_DISTRO} \
  ${DOCKER_CODENAME} stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

(The FALLBACKS lists above are current as of 2026-08 — bump them if Docker ships a newer stable codename by the time you read this.)

Let your user run Docker without sudo:

sudo usermod -aG docker $USER
newgrp docker
docker compose version   # sanity check

2. Clone & install

git clone git@github.com:rgrgogu/new_starr.git
cd new_starr
corepack enable && corepack prepare pnpm@11.3.0 --activate
pnpm install

3. Configure environment — three files, not two

apps/api/.env

cp apps/api/.env-production apps/api/.env

Fill in every CHANGE_ME. For a local bundled stack (as opposed to CockroachDB Cloud / managed S3), these specific values matter:

DB_HOST=postgres
DB_PORT=5432
DB_SSL=false                          # bundled Postgres has no TLS
CACHE_DRIVER=redis
REDIS_URL=redis://valkey:6379
S3_ENDPOINT=http://garage:3900
GARAGE_RPC_SECRET=<openssl rand -hex 32>

Generate every secret independently:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

apps/web/.env

cd apps/web && cp .env.example .env

Fill in VITE_API_URL, VITE_APP_URL, etc., then cd ../...

Root new_starr/.env — easy to miss, causes silent failures

docker-compose.yml's bundled postgres service reads ${DB_NAME}, ${DB_USER}, ${DB_PASSWORD} from a root-level .env — NOT from apps/api/.env. Create it separately, with exactly the same values as the DB block you just put in apps/api/.env:

DB_NAME=starr
DB_USER=starr
DB_PASSWORD=<same password as apps/api/.env>

⚠️ Postgres only reads these once, at first-ever container start, and bakes them into its data volume. If the two files ever drift apart afterward, you'll get password authentication failed even though everything "looks" configured — fix by making apps/api/.env match what's actually in the volume (the root .env value at the time Postgres first started), not the other way around.


4. Build the frontend bundle before building images

apps/web/Dockerfile only copies a pre-built dist/ — it does not run the build itself:

pnpm --filter web build

5. Bring the stack up

docker compose up -d --build
docker compose ps       # all 5 services should show Up/healthy

6. One-time Garage (S3) cluster init

Garage's image ships only a static binary — no shell — so this can't be scripted as a container entrypoint; it's a manual one-time step:

docker compose exec garage /garage node id

Copy the ID (the part before the @host:port), then:

docker compose exec garage /garage layout assign <NODE_ID> -z dc1 -c 100G
docker compose exec garage /garage layout apply --version 1
docker compose exec garage /garage bucket create <S3_BUCKET>
docker compose exec garage /garage key import <S3_ACCESS_KEY> <S3_SECRET_KEY> -n starr-app --yes
docker compose exec garage /garage bucket allow <S3_BUCKET> --read --write --owner --key <S3_ACCESS_KEY>

(S3_BUCKET/S3_ACCESS_KEY/S3_SECRET_KEY are whatever you set in apps/api/.env.)


7. Run database migrations

docker compose exec backend sequelize-cli db:migrate

8. Verify

docker compose ps

If both respond, you're done with local testing.


9. (Optional) Put a real domain + HTTPS in front

For actually deploying this somewhere reachable, use the Caddyfile at the repo root as a template. It's a standalone Caddy install on the host (not part of docker-compose.yml) that reverse-proxies 3 domains to the 3 ports already published above (:4650, :3024, :3900) and handles Let's Encrypt certificates automatically. See the comments at the top of Caddyfile for exact steps. Not needed for local localhost:4650 testing — skip this until you're pointing a real domain at the machine.


Troubleshooting — real errors hit while writing this guide

Symptom Cause Fix
apt 404: .../linux/<distro> <codename> Release not found Step 1's install block already auto-detects and falls back — this only happens if you ran the old manual commands, or your codename is newer than every entry in FALLBACKS Re-run Step 1's block as-is, or add your distro's actual latest stable codename to FALLBACKS
permission denied ... docker.sock User not in the docker group sudo usermod -aG docker $USER && newgrp docker
WARN: "DB_NAME" variable is not set (and similar) Root new_starr/.env missing Create it (see step 3)
postgres container stuck Restarting POSTGRES_PASSWORD blank (same root .env issue) Same fix as above
exec: "/bin/sh": stat /bin/sh: no such file or directory Some images (like Garage's) ship no shell at all — never wrap their entrypoint in /bin/sh Use the image's default command; do cluster setup via docker exec <container> <binary> directly (no shell needed)
Garage crash-loops: rpc_secret value is missing GARAGE_RPC_SECRET not set in apps/api/.env Set it, then docker compose up -d --force-recreate garage
password authentication failed for user ... apps/api/.env and root .env DB passwords don't match exactly Make them match; Postgres only honors whatever was set at first boot
Fixed a file but the same error keeps happening Images bake in code at build time — there's no live volume mount docker compose up -d --build <service> after any source change
ERROR: Unknown constraint error during migrate Sequelize hides the real Postgres error behind this generic message Run the raw SQL directly via docker compose exec postgres psql -U <user> -d <db> to see the actual cause
Migration uses STRING as a raw SQL type CockroachDB alias, not valid in real Postgres Already fixed in this repo (swapped to TEXT, which works on both)
A service vanishes from docker compose ps A previous up was scoped to one service (e.g. up -d --build backend), which doesn't touch others Run a plain docker compose up -d (no service name) to restore everything
Browser: "Unable to connect" Either the container isn't actually running, or you're browsing from a different machine than the one running Docker Check docker compose ps first; localhost only resolves to the machine Docker is on

Notes

  • Frontend is served on :4650, not :80 — change frontend.ports in docker-compose.yml if you want a different port.
  • Cluster init for Garage is intentionally manual (step 6) — the old automated garage-init.sh approach was removed because it depended on a shell that doesn't exist in the Garage image.