From d70ee83a63acce60995c7d155bca93f356cc8180 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 13 May 2026 19:47:05 +0000 Subject: [PATCH] Set up Docker environment for local development and deployment Configure Docker Compose, Dockerfiles for API and web servers, and a startup script to enable local development and deployment of the application. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9b39e228-74f1-466d-ab5c-87a1e5e7ed07 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu Replit-Helium-Checkpoint-Created: true --- .dockerignore | 28 ++++++ DOCKER.md | 83 ++++++++++++++++ docker-compose.yml | 179 +++++++++-------------------------- docker/api-server.Dockerfile | 57 +++++++++++ docker/migrate.sh | 27 ++++++ docker/nginx.conf | 95 +++++++++---------- docker/tx-os.Dockerfile | 33 +++++++ start.sh | 86 +++++++++++++++++ 8 files changed, 403 insertions(+), 185 deletions(-) create mode 100644 .dockerignore create mode 100644 DOCKER.md create mode 100644 docker/api-server.Dockerfile create mode 100755 docker/migrate.sh create mode 100644 docker/tx-os.Dockerfile create mode 100755 start.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6cc05a9f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +node_modules +**/node_modules +**/dist +**/build +**/.next +**/.turbo +**/.cache +**/coverage +**/playwright-report +**/test-results +**/.vite +**/.expo +.git +.gitignore +.local +.replit +.cache +attached_assets +storage +**/storage +.env +.env.* +!.env.docker.example +*.log +**/*.log +.DS_Store +artifacts/mockup-sandbox +docker-compose.override.yml diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 00000000..141dc59d --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,83 @@ +# تشغيل Tx OS عبر Docker + +دليل سريع لتشغيل المشروع بالكامل على جهازك (ماك / لينكس) بأمر واحد، بدون الحاجة لتثبيت Node أو Postgres يدوياً. + +## المتطلبات + +- **Docker Desktop** (الإصدار 4.x أو أحدث) — حمّله من . + - يدعم Apple Silicon (M1/M2/M3/M4) و Intel. + - تأكد إنه شغّال بعد التثبيت (تشوف أيقونة الحوت بأعلى الشاشة). +- **Git** — موجود غالباً، تأكد بـ `git --version`. + +> لا تحتاج تثبيت Node أو pnpm أو Postgres على الماك — كله يشتغل داخل الحاويات. + +## التشغيل لأول مرة + +```bash +git clone https://desktop-11cj93j.tail866923.ts.net/rafraa/TX.git +cd TX +./start.sh +``` + +السكربت يعمل تلقائياً: + +1. ينشئ ملف `.env` من `.env.docker.example` ويولّد `SESSION_SECRET` و `POSTGRES_PASSWORD` عشوائيين. +2. يبني صور Docker (أول مرة يأخذ 5–10 دقائق — Node, Chromium, dependencies). +3. يشغّل Postgres ثم يدفع جداول قاعدة البيانات (drizzle-kit push) ثم يزرع البيانات الأولية (seed). +4. يشغّل API server والواجهة خلف Nginx. + +بعدها افتح: **http://localhost:3000** + +## الأوامر اليومية + +| الأمر | الوظيفة | +|---|---| +| `./start.sh` | تشغيل (build إذا لزم) | +| `./start.sh logs` | متابعة سجلّات الحاويات | +| `./start.sh stop` | إيقاف بدون حذف (البيانات تبقى) | +| `./start.sh down` | حذف الحاويات (البيانات تبقى في الـ volumes) | +| `./start.sh rebuild` | إعادة بناء الصور من الصفر | +| `./start.sh reset` | **خطر**: حذف قاعدة البيانات والملفات المرفوعة | + +## البنية + +``` +http://localhost:3000 → nginx (web container) + ├── / → ملفات React الثابتة + └── /api/* → http://api:8080 (Node Express + Socket.IO) + └── postgres:5432 + └── /app/storage (volume للملفات) +``` + +ثلاث حاويات + قاعدة بيانات: + +- **postgres** — Postgres 16، البيانات في volume اسمه `tx-os_postgres_data`. +- **migrate** — يشتغل مرة وحدة عند كل تشغيل، يدفع schema ويزرع البيانات. +- **api** — Node 24 + Chromium (لتوليد PDF) + التخزين المحلي. +- **web** — Nginx يقدّم الواجهة ويعمل reverse proxy للـ API. + +## تخصيص + +عدّل `.env` بعد ما يُنشأ. أهم القيم: + +- `APP_PORT=3000` — منفذ المتصفح (غيّره مثلاً إلى 8080 لو 3000 محجوز). +- `SESSION_SECRET` — سر توقيع الجلسات (مولّد تلقائياً، لا تشاركه). +- `POSTGRES_PASSWORD` — كلمة مرور قاعدة البيانات. +- `STORAGE_DRIVER=local` — التخزين على القرص داخل الحاوية (افتراضي ومناسب لمعظم الحالات). +- `SMTP_*` — إعدادات البريد لإشعارات اجتماعات الإدارة (اختياري). + +بعد تعديل `.env` نفّذ `./start.sh stop` ثم `./start.sh` لإعادة التحميل. + +## استكشاف الأخطاء + +- **المنفذ 3000 محجوز**: عدّل `APP_PORT` في `.env`. +- **الواجهة ما تفتح**: راجع `./start.sh logs`، خصوصاً سجل `api` و `migrate`. +- **بحاجة للوصول لقاعدة البيانات**: `docker compose exec postgres psql -U tx tx`. +- **مساحة القرص**: `docker system prune` يحذف الصور غير المستخدمة (لا يلمس الـ volumes). + +## التحديث من Gitea + +```bash +git pull +./start.sh rebuild +``` diff --git a/docker-compose.yml b/docker-compose.yml index e75a876f..ac4caf16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,175 +1,82 @@ -# Tx OS — production-style compose stack for a single VPS. -# -# Bring up: docker compose up -d -# Migrate DB: docker compose run --rm migrate -# Logs: docker compose logs -f api -# Tear down: docker compose down -# -# All secrets (passwords, session secret, MinIO credentials) live in `.env`. -# Copy `.env.example` to `.env` and edit before first boot. - name: tx-os services: - db: + postgres: image: postgres:16-alpine restart: unless-stopped environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER:-tx} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tx_dev_password} + POSTGRES_DB: ${POSTGRES_DB:-tx} volumes: - - db_data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 10s + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tx} -d ${POSTGRES_DB:-tx}"] + interval: 5s timeout: 5s - retries: 10 - networks: [internal] + retries: 20 - minio: - image: minio/minio:RELEASE.2025-01-20T14-49-07Z - restart: unless-stopped - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY} - volumes: - - minio_data:/data - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 15s - timeout: 5s - retries: 5 - networks: [internal] - - # One-shot: create the two buckets on first boot. Idempotent. - minio-init: - image: minio/mc:RELEASE.2025-01-17T23-25-50Z + migrate: + build: + context: . + dockerfile: docker/api-server.Dockerfile + image: tx-os/api-server:latest depends_on: - minio: + postgres: condition: service_healthy - entrypoint: > - /bin/sh -c " - mc alias set local http://minio:9000 $${S3_ACCESS_KEY_ID} $${S3_SECRET_ACCESS_KEY} && - mc mb --ignore-existing local/${S3_BUCKET_PRIVATE} && - mc mb --ignore-existing local/${S3_BUCKET_PUBLIC} && - mc anonymous set download local/${S3_BUCKET_PUBLIC} || - true - " environment: - S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} - S3_BUCKET_PRIVATE: ${S3_BUCKET_PRIVATE} - S3_BUCKET_PUBLIC: ${S3_BUCKET_PUBLIC} - networks: [internal] + NODE_ENV: production + DATABASE_URL: postgres://${POSTGRES_USER:-tx}:${POSTGRES_PASSWORD:-tx_dev_password}@postgres:5432/${POSTGRES_DB:-tx} + user: root + entrypoint: ["/usr/bin/tini", "--"] + command: ["/bin/bash", "/usr/local/bin/migrate.sh"] restart: "no" api: build: context: . - target: api - image: tx-os/api:latest + dockerfile: docker/api-server.Dockerfile + image: tx-os/api-server:latest restart: unless-stopped depends_on: - db: - condition: service_healthy - minio: + postgres: condition: service_healthy + migrate: + condition: service_completed_successfully environment: NODE_ENV: production - PORT: "8080" - DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - SESSION_SECRET: ${SESSION_SECRET} - ALLOWED_ORIGINS: ${ALLOWED_ORIGINS} + PORT: 8080 + DATABASE_URL: postgres://${POSTGRES_USER:-tx}:${POSTGRES_PASSWORD:-tx_dev_password}@postgres:5432/${POSTGRES_DB:-tx} + SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required — copy .env.docker.example to .env and edit it} + LOCAL_STORAGE_SIGNING_SECRET: ${LOCAL_STORAGE_SIGNING_SECRET:-${SESSION_SECRET}} + LOCAL_STORAGE_ROOT: /app/storage + STORAGE_DRIVER: ${STORAGE_DRIVER:-local} + PRIVATE_OBJECT_DIR: ${PRIVATE_OBJECT_DIR:-/app/storage/private} + PUBLIC_OBJECT_SEARCH_PATHS: ${PUBLIC_OBJECT_SEARCH_PATHS:-/app/storage/public} + DEFAULT_OBJECT_STORAGE_BUCKET_ID: ${DEFAULT_OBJECT_STORAGE_BUCKET_ID:-tx-local} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:${APP_PORT:-3000}} + PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:${APP_PORT:-3000}} LOG_LEVEL: ${LOG_LEVEL:-info} - PUBLIC_BASE_URL: ${PUBLIC_BASE_URL} - # Storage — S3 driver against MinIO. - STORAGE_DRIVER: s3 - S3_ENDPOINT: http://minio:9000 - S3_REGION: ${S3_REGION:-us-east-1} - S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} - S3_FORCE_PATH_STYLE: "true" - PRIVATE_OBJECT_DIR: /${S3_BUCKET_PRIVATE}/private - PUBLIC_OBJECT_SEARCH_PATHS: /${S3_BUCKET_PUBLIC}/public - # Email (optional — leave SMTP_HOST blank to disable mail). SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} - SMTP_SECURE: ${SMTP_SECURE:-} SMTP_FROM: ${SMTP_FROM:-} - ports: - # API — exposed on host :8080 for direct curl/integration access. In - # a public-facing deployment this should be bound to 127.0.0.1 only - # (e.g. "127.0.0.1:8080:8080") so only the edge proxy reaches it. - - "${API_PORT:-8080}:8080" - networks: [internal] - healthcheck: - test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:8080/api/healthz', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))\""] - interval: 15s - timeout: 5s - retries: 10 + SMTP_SECURE: ${SMTP_SECURE:-} + volumes: + - app_storage:/app/storage web: build: context: . - target: web + dockerfile: docker/tx-os.Dockerfile image: tx-os/web:latest restart: unless-stopped depends_on: - api: - condition: service_healthy + - api ports: - # SPA — exposed on host :3000. Front this with an operator-managed - # TLS terminator (Caddy / Nginx / Traefik) for production HTTPS. - - "${WEB_PORT:-3000}:80" - networks: [internal] - - # Component preview server. Dev-only — used to iterate on isolated - # React components in an iframe-friendly preview at /__mockup. Not part - # of the production user surface; kept under the `dev` profile so a - # normal `docker compose up -d` does NOT start it. Run with: - # docker compose --profile dev up -d mockup-sandbox - mockup-sandbox: - build: - context: . - target: mockup - image: tx-os/mockup:latest - profiles: ["dev"] - environment: - NODE_ENV: development - PORT: "8081" - BASE_PATH: /__mockup - ports: - - "${MOCKUP_PORT:-8081}:8081" - networks: [internal] - - # One-shot DB migrate + seed. Run on first boot: - # docker compose run --rm migrate - migrate: - build: - context: . - target: migrate - image: tx-os/migrate:latest - profiles: ["tools"] - depends_on: - db: - condition: service_healthy - environment: - DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - NODE_ENV: production - SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD} - SEED_USER_PASSWORD: ${SEED_USER_PASSWORD} - # Single canonical entrypoint — wraps `pnpm --filter db push` + seed. - command: ["pnpm", "--filter", "@workspace/api-server", "run", "migrate"] - networks: [internal] + - "${APP_PORT:-3000}:80" volumes: - db_data: - minio_data: - -networks: - internal: - driver: bridge + postgres_data: + app_storage: diff --git a/docker/api-server.Dockerfile b/docker/api-server.Dockerfile new file mode 100644 index 00000000..67d5fcd4 --- /dev/null +++ b/docker/api-server.Dockerfile @@ -0,0 +1,57 @@ +FROM node:24-bookworm-slim AS base +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable && corepack prepare pnpm@10.26.1 --activate + +RUN apt-get update && apt-get install -y --no-install-recommends \ + chromium \ + fonts-noto \ + fonts-noto-color-emoji \ + fonts-noto-cjk \ + ca-certificates \ + curl \ + tini \ + && rm -rf /var/lib/apt/lists/* + +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + +WORKDIR /app + +FROM base AS deps +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ +COPY artifacts/api-server/package.json artifacts/api-server/ +COPY artifacts/tx-os/package.json artifacts/tx-os/ +COPY lib/api-client-react/package.json lib/api-client-react/ +COPY lib/api-spec/package.json lib/api-spec/ +COPY lib/api-zod/package.json lib/api-zod/ +COPY lib/db/package.json lib/db/ +COPY lib/object-storage-web/package.json lib/object-storage-web/ +COPY scripts/package.json scripts/ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile --ignore-scripts + +FROM deps AS build +COPY tsconfig.base.json* tsconfig.json* ./ +COPY lib lib +COPY scripts scripts +COPY artifacts/api-server artifacts/api-server +RUN pnpm --filter @workspace/api-server run build + +FROM base AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/pnpm-workspace.yaml /app/pnpm-lock.yaml /app/package.json ./ +COPY --from=build /app/lib ./lib +COPY --from=build /app/scripts ./scripts +COPY --from=build /app/artifacts/api-server/package.json ./artifacts/api-server/package.json +COPY --from=build /app/artifacts/api-server/dist ./artifacts/api-server/dist +COPY --from=build /app/artifacts/api-server/assets ./artifacts/api-server/assets +COPY --from=build /app/artifacts/api-server/node_modules ./artifacts/api-server/node_modules +COPY docker/migrate.sh /usr/local/bin/migrate.sh +RUN chmod +x /usr/local/bin/migrate.sh +RUN mkdir -p /app/storage && chown -R node:node /app/storage +USER node +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["node", "--enable-source-maps", "/app/artifacts/api-server/dist/index.mjs"] diff --git a/docker/migrate.sh b/docker/migrate.sh new file mode 100755 index 00000000..a9499ce0 --- /dev/null +++ b/docker/migrate.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[migrate] waiting for postgres at $DATABASE_URL ..." +for i in {1..60}; do + if node -e " + import('pg').then(({ default: pg }) => { + const c = new pg.Client({ connectionString: process.env.DATABASE_URL }); + c.connect().then(() => c.end()).then(() => process.exit(0)).catch(() => process.exit(1)); + }); + " 2>/dev/null; then + echo "[migrate] postgres is ready." + break + fi + sleep 1 +done + +echo "[migrate] pushing schema with drizzle-kit ..." +cd /app +pnpm --filter @workspace/db run push-force + +echo "[migrate] seeding database ..." +pnpm --filter @workspace/scripts run seed || { + echo "[migrate] seed reported a non-zero exit (often safe if data already exists). Continuing." +} + +echo "[migrate] done." diff --git a/docker/nginx.conf b/docker/nginx.conf index 3cb1f950..bac6c3a7 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,56 +1,53 @@ -# Tx OS web — serves the built React/Vite SPA and reverse-proxies the -# API + Socket.IO traffic to the api container on the private network. upstream tx_api { - server api:8080; + server api:8080; } server { - listen 80; - server_name _; + listen 80; + server_name _; + + client_max_body_size 50m; + + # WebSocket / Socket.IO upgrade for /api/socket.io + location /api/socket.io/ { + proxy_pass http://tx_api; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Forward all /api requests to the Node API server + location /api/ { + proxy_pass http://tx_api; + 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_read_timeout 300s; + proxy_send_timeout 300s; + } + + # Static SPA assets with hashed filenames — cache aggressively + location /assets/ { + root /usr/share/nginx/html; + expires 1y; + access_log off; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # SPA fallback — every unknown path returns index.html + location / { root /usr/share/nginx/html; index index.html; - - # Health endpoint for compose / load balancer probes. - location = /healthz { - access_log off; - return 200 "ok\n"; - add_header Content-Type text/plain; - } - - # Socket.IO upgrade — must come before the generic /api/ block so the - # WebSocket handshake gets the upgrade headers it needs. - location /api/socket.io/ { - proxy_pass http://tx_api; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - 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_read_timeout 1h; - } - - # All other API calls. - location /api/ { - proxy_pass http://tx_api; - 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; - client_max_body_size 25m; - } - - # SPA: serve hashed assets with long-cache; fall back to index.html. - location ~* \.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico|webp|map)$ { - expires 30d; - add_header Cache-Control "public, immutable"; - try_files $uri =404; - } - - location / { - try_files $uri $uri/ /index.html; - add_header Cache-Control "no-cache"; - } + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache"; + } } diff --git a/docker/tx-os.Dockerfile b/docker/tx-os.Dockerfile new file mode 100644 index 00000000..e63f12f6 --- /dev/null +++ b/docker/tx-os.Dockerfile @@ -0,0 +1,33 @@ +FROM node:24-bookworm-slim AS base +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable && corepack prepare pnpm@10.26.1 --activate +WORKDIR /app + +FROM base AS deps +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ +COPY artifacts/api-server/package.json artifacts/api-server/ +COPY artifacts/tx-os/package.json artifacts/tx-os/ +COPY lib/api-client-react/package.json lib/api-client-react/ +COPY lib/api-spec/package.json lib/api-spec/ +COPY lib/api-zod/package.json lib/api-zod/ +COPY lib/db/package.json lib/db/ +COPY lib/object-storage-web/package.json lib/object-storage-web/ +COPY scripts/package.json scripts/ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile --ignore-scripts + +FROM deps AS build +COPY tsconfig.base.json* tsconfig.json* ./ +COPY lib lib +COPY artifacts/tx-os artifacts/tx-os +ENV PORT=80 +ENV BASE_PATH=/ +RUN pnpm --filter @workspace/tx-os run build + +FROM nginx:1.27-alpine AS runtime +RUN rm -rf /usr/share/nginx/html/* /etc/nginx/conf.d/default.conf +COPY --from=build /app/artifacts/tx-os/dist/public /usr/share/nginx/html +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/start.sh b/start.sh new file mode 100755 index 00000000..7d13da98 --- /dev/null +++ b/start.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# Tx OS — one-shot launcher for Docker on macOS / Linux. +# Usage: +# ./start.sh # build (if needed) and start in the background +# ./start.sh logs # tail container logs +# ./start.sh stop # stop containers (data is preserved) +# ./start.sh down # stop AND remove containers (data still preserved) +# ./start.sh reset # DANGEROUS: also wipe database + uploaded files +# ./start.sh rebuild # force rebuild images then start +# ----------------------------------------------------------------------------- +set -euo pipefail + +cd "$(dirname "$0")" + +if ! command -v docker >/dev/null 2>&1; then + echo "Docker not found. Install Docker Desktop from https://www.docker.com/products/docker-desktop/" + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "Docker Compose v2 not found. Update Docker Desktop." + exit 1 +fi + +if [ ! -f .env ]; then + echo "No .env file found. Creating one from .env.docker.example ..." + cp .env.docker.example .env + if command -v openssl >/dev/null 2>&1; then + SECRET="$(openssl rand -hex 32)" + PG_PW="$(openssl rand -hex 16)" + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^SESSION_SECRET=.*|SESSION_SECRET=${SECRET}|" .env + sed -i '' "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${PG_PW}|" .env + else + sed -i "s|^SESSION_SECRET=.*|SESSION_SECRET=${SECRET}|" .env + sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${PG_PW}|" .env + fi + echo "Generated random SESSION_SECRET and POSTGRES_PASSWORD in .env." + else + echo "openssl not available — please open .env and set SESSION_SECRET + POSTGRES_PASSWORD before running again." + exit 1 + fi +fi + +action="${1:-up}" + +case "$action" in + up) + echo "==> Building images (first time can take 5-10 minutes) ..." + docker compose build + echo "==> Starting containers ..." + docker compose up -d + APP_PORT="$(grep -E '^APP_PORT=' .env | cut -d= -f2 | tr -d '\r' || echo 3000)" + echo + echo "Tx OS is starting. Open http://localhost:${APP_PORT:-3000}" + echo "Tail logs with: ./start.sh logs" + ;; + rebuild) + docker compose build --no-cache + docker compose up -d + ;; + logs) + docker compose logs -f --tail=200 + ;; + stop) + docker compose stop + ;; + down) + docker compose down + ;; + reset) + read -r -p "This will DELETE the database and uploaded files. Type 'yes' to confirm: " ans + if [ "$ans" = "yes" ]; then + docker compose down -v + echo "All data wiped." + else + echo "Aborted." + fi + ;; + *) + echo "Unknown action: $action" + echo "Usage: ./start.sh [up|rebuild|logs|stop|down|reset]" + exit 1 + ;; +esac