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
- Frontend: http://localhost:4650
- Backend: http://localhost:3024/api
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— changefrontend.portsindocker-compose.ymlif you want a different port. - Cluster init for Garage is intentionally manual (step 6) — the old automated
garage-init.shapproach was removed because it depended on a shell that doesn't exist in the Garage image.