Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ BETTER_AUTH_URL=http://localhost:4000
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
INTERNAL_TOKEN=change-me-32-byte-random-hex

# ─── First admin (Docker Compose only, optional) ──────────
# On the CLI (`openship`) the setup wizard creates the first admin. Docker has no
# wizard, so set these to have the one-shot `init-admin` service create the first
# admin on `docker compose up` (issue #138). Leave unset to create it another way
# (CLI, or a first sign-in you enable manually). Public sign-up stays disabled —
# this calls the same internal-token-gated bootstrap endpoint the CLI uses, and
# is idempotent (it won't touch an admin that already exists). Password: 8-128 chars.
# OPENSHIP_ADMIN_NAME=Admin
# OPENSHIP_ADMIN_EMAIL=admin@example.com
# OPENSHIP_ADMIN_PASSWORD=

# ─── OAuth login (optional) ───────────────────────────────
# GITHUB_CLIENT_ID=
# GITHUB_CLIENT_SECRET=
Expand Down
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ services:
api:
condition: service_healthy

# ─── First-admin bootstrap (one-shot, optional) ───────
# Creates the first administrator declaratively so a fresh `docker compose up`
# is usable without the CLI wizard (issue #138). No-op unless OPENSHIP_ADMIN_*
# is set in .env; idempotent (bootstrap-admin is one-shot — 409 once an admin
# exists). Does NOT enable public sign-up — it calls the same internal-token-
# gated endpoint the CLI uses. Runs after the API is healthy, then exits.
init-admin:
image: curlimages/curl:8.11.1
restart: "no"
env_file:
- .env
environment:
OPENSHIP_API_URL: http://api:4000
volumes:
- ./scripts/docker-init-admin.sh:/docker-init-admin.sh:ro
entrypoint: ["sh", "/docker-init-admin.sh"]
depends_on:
api:
condition: service_healthy

# ─── Web / Landing ────────────────────────────────────
web:
build:
Expand Down
15 changes: 14 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,24 @@ cp .env.example .env
docker compose up -d
```

**Create the first admin.** Docker has no interactive setup wizard, so declare the
first administrator in `.env` before starting — the one-shot `init-admin` service
creates it once the API is healthy (public sign-up stays disabled):

```bash
OPENSHIP_ADMIN_NAME=Admin
OPENSHIP_ADMIN_EMAIL=admin@example.com
OPENSHIP_ADMIN_PASSWORD=a-strong-password # 8-128 chars
```

It's idempotent — re-running `docker compose up` never changes an existing admin.
Leaving these unset skips the step; you can still create the admin another way.

---

## Users & access (self-hosted)

- **Public signup is disabled.** The first admin is created by `openship up`'s setup wizard.
- **Public signup is disabled.** On the CLI the first admin is created by `openship up`'s setup wizard; on Docker, set `OPENSHIP_ADMIN_*` in `.env` (see [Docker](#docker) above).
- **Invite teammates** from **Settings → Team**. They get an accept link at your instance's URL (so the instance needs to be reachable — a public URL or your LAN).
- **Lost the admin password?** Run `openship reset-admin-password` on the box (no sign-in needed; uses the local internal token).

Expand Down
76 changes: 76 additions & 0 deletions scripts/docker-init-admin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/sh
# ─────────────────────────────────────────────────────────────────────────────
# Declarative first-admin bootstrap for the Docker Compose stack.
#
# Compose has no equivalent of `openship up`'s interactive setup wizard, so a
# fresh `docker compose up` used to leave the instance with no way to create the
# first administrator through the web flow (public sign-up is disabled, and the
# loopback bootstrap shortcut never fires because the request reaches the API
# from a Docker-bridge IP, not 127.0.0.1). See issue #138.
#
# This one-shot service closes that gap WITHOUT enabling public sign-up: it
# calls the existing internal-token-gated `POST /api/system/bootstrap-admin`
# endpoint — the same endpoint the CLI wizard uses — with the credentials the
# operator declared in `.env`.
#
# It is a no-op unless OPENSHIP_ADMIN_EMAIL and OPENSHIP_ADMIN_PASSWORD are set,
# and it is idempotent: bootstrap-admin refuses (409) once an admin exists, so
# re-running `docker compose up` never changes an existing admin. Operators who
# prefer the CLI or a manual first sign-in can simply leave the vars unset.
# ─────────────────────────────────────────────────────────────────────────────
set -eu

API_URL="${OPENSHIP_API_URL:-http://api:4000}"
ADMIN_NAME="${OPENSHIP_ADMIN_NAME:-Admin}"

if [ -z "${OPENSHIP_ADMIN_EMAIL:-}" ] || [ -z "${OPENSHIP_ADMIN_PASSWORD:-}" ]; then
echo "[init-admin] OPENSHIP_ADMIN_EMAIL / OPENSHIP_ADMIN_PASSWORD not set — skipping declarative admin bootstrap."
echo "[init-admin] Create the first admin with the CLI, or set those vars in .env and re-run \`docker compose up\`."
exit 0
fi

if [ -z "${INTERNAL_TOKEN:-}" ]; then
echo "[init-admin] INTERNAL_TOKEN is not set. The API requires it and so does this bootstrap. Aborting." >&2
exit 1
fi

# Build the JSON body with a tiny Python helper if available; otherwise fall back
# to a manual escape. curl images ship neither jq nor python, so escape inline.
escape() {
# Escape backslash, double-quote and control chars for a JSON string value.
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}

body="{\"name\":\"$(escape "$ADMIN_NAME")\",\"email\":\"$(escape "$OPENSHIP_ADMIN_EMAIL")\",\"password\":\"$(escape "$OPENSHIP_ADMIN_PASSWORD")\"}"

echo "[init-admin] Bootstrapping first admin for ${OPENSHIP_ADMIN_EMAIL} via ${API_URL} …"

status="$(
curl -sS -o /tmp/init-admin-response -w '%{http_code}' \
-X POST "${API_URL}/api/system/bootstrap-admin" \
-H 'Content-Type: application/json' \
-H "X-Internal-Token: ${INTERNAL_TOKEN}" \
--data "${body}"
)" || {
echo "[init-admin] Could not reach the API at ${API_URL}. Is the api service healthy?" >&2
exit 1
}

case "${status}" in
200)
echo "[init-admin] First admin created for ${OPENSHIP_ADMIN_EMAIL}. Sign in at the dashboard."
;;
409)
echo "[init-admin] An admin already exists — nothing to do (bootstrap is one-shot)."
;;
401)
echo "[init-admin] API rejected the internal token (401). Ensure INTERNAL_TOKEN matches the api service." >&2
exit 1
;;
*)
echo "[init-admin] bootstrap-admin failed (HTTP ${status}):" >&2
cat /tmp/init-admin-response >&2 2>/dev/null || true
echo >&2
exit 1
;;
esac