Add deployment workflow to push code to Gitea and redeploy on Mac Mini

Configure Replit project for deployment to a self-hosted Gitea repository, including a `deploy.sh` script on a Mac Mini to pull changes, stop, rebuild, and restart Docker containers.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 1fa9329f-0cec-4a2f-80e8-e26dbae3142e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 29017a07-e519-4b14-bdf7-b913b959d38f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/4d696b13-86f2-4c9d-be0d-95b293430047/1fa9329f-0cec-4a2f-80e8-e26dbae3142e/ODGOKcj
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
Replit Agent
2026-06-06 10:11:36 +00:00
parent 838dde0d95
commit 9e602d53fa
9 changed files with 405 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# Keep the Docker build context lean, but DO NOT exclude:
# - attached_assets/ (the web app imports images via the @assets alias)
# - any workspace package.json (pnpm --frozen-lockfile needs them all)
**/node_modules
**/dist
**/.turbo
**/.vite
.git
.cache
.config
.local
.canvas
.agents
*.log
.DS_Store
Thumbs.db
# Docs / meta not needed inside images
.replitignore
+4
View File
@@ -31,6 +31,10 @@ externalPort = 8081
localPort = 8082
externalPort = 3003
[[ports]]
localPort = 9099
externalPort = 3002
[[ports]]
localPort = 18312
externalPort = 3000
+113
View File
@@ -0,0 +1,113 @@
# Deployment: Replit → Gitea → Mac Mini (Docker)
This project is developed on **Replit** and self-hosted on a **Mac Mini** using
Docker. **Gitea** is the central Git repository. Nothing deploys directly from
Replit, and GitHub is not used.
```
Replit (dev) ──push──▶ Gitea (central repo) ──pull──▶ Mac Mini ──▶ Docker redeploy
```
- **Default branch:** `main`
- **Git remote name (everywhere):** `gitea`
---
## What's in this repo
| File | Runs on | Purpose |
|------|---------|---------|
| `scripts/push-to-gitea.sh` | Replit | Push `main` to the `gitea` remote |
| `deploy.sh` | Mac Mini | Pull `main`, then rebuild & restart Docker |
| `docker-compose.yml` | Mac Mini | Defines the `web` + `api` services |
| `Dockerfile.web` | Mac Mini | Builds the SPA, serves it via nginx (+ `/api` proxy) |
| `Dockerfile.api` | Mac Mini | Builds & runs the Express API server |
| `docker/nginx.conf` | Mac Mini | Static serving + reverse proxy to the API |
---
## One-time setup
### 1. On Replit — add the `gitea` remote
The Gitea repo URL is provided later. Add it once (HTTPS with a token, or SSH):
```bash
# HTTPS (token embedded) — simplest for a headless push
git remote add gitea https://<user>:<token>@<gitea-host>/<owner>/<repo>.git
# …or SSH
git remote add gitea git@<gitea-host>:<owner>/<repo>.git
```
Verify:
```bash
git remote -v # should list 'gitea'
```
### 2. On the Mac Mini — clone the repo from Gitea
```bash
git clone -b main https://<gitea-host>/<owner>/<repo>.git ehsan
cd ehsan
# Make sure the remote is named 'gitea' (clone names it 'origin' by default):
git remote rename origin gitea # only if needed
chmod +x deploy.sh
```
Requirements on the Mac Mini: **Docker Desktop** (or Docker Engine) with the
`docker compose` plugin. On Apple Silicon, the images build for `linux/amd64`
and run under Rosetta/emulation automatically.
---
## Everyday workflow
### A. Push from Replit
Commit your changes (via the Replit Git pane), then:
```bash
./scripts/push-to-gitea.sh
```
(Or directly: `git push gitea main`.)
### B. Redeploy on the Mac Mini
```bash
./deploy.sh
```
`deploy.sh` will, in order:
1. `git pull gitea main`
2. `docker compose down` (stop current containers)
3. `docker compose build` (rebuild images)
4. `docker compose up -d` (start again)
It prints a clear **SUCCESS** message and the running containers, or an
**ERROR** and a non-zero exit code if any step fails.
After a successful deploy the app is available on the Mac Mini at
`http://localhost:8080` (override the host port with `WEB_PORT`, e.g.
`WEB_PORT=3000 ./deploy.sh`).
---
## Notes & constraints
- **Replit stays the development environment.** Its workflows/preview are
unchanged by this setup.
- **amd64 / glibc only.** The pnpm workspace strips every native binary that is
not `linux-x64-gnu`, so the Dockerfiles use `node:24-bookworm-slim` (not
alpine) and pin `platform: linux/amd64`. Do not switch the build base to
alpine or arm64 — the web build (rollup / tailwind oxide / lightningcss) will
fail to find its native binaries.
- **Web ↔ API.** The browser calls same-origin `/api/...`; nginx proxies that to
the `api` container, so no API URL needs to be configured in the frontend.
- **Data.** The API currently uses in-memory demo data, so no database service
is included. Restarting the `api` container resets it.
- **Secrets.** Do not commit the Gitea URL/token. Keep it in the local `gitea`
remote (or pass `GITEA_REMOTE_URL` at push time).
+34
View File
@@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1
#
# API server image (Express, esbuild bundle).
#
# NOTE: the pnpm-workspace `overrides` strip every native binary that is not
# linux-x64-gnu (no musl, no arm64, no darwin). The image therefore MUST be a
# glibc/amd64 image — use node:*-bookworm-slim (NOT alpine) and build for
# linux/amd64 (Docker on Apple Silicon runs this under emulation/Rosetta).
# ---- Build stage -----------------------------------------------------------
FROM --platform=linux/amd64 node:24-bookworm-slim AS build
ENV PNPM_HOME=/pnpm
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9 --activate
WORKDIR /repo
COPY . .
RUN pnpm install --frozen-lockfile
RUN NODE_ENV=production pnpm --filter @workspace/api-server run build
# ---- Runtime stage ---------------------------------------------------------
FROM --platform=linux/amd64 node:24-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
# esbuild produces a self-contained bundle (index.mjs + pino transport files).
COPY --from=build /repo/artifacts/api-server/dist ./dist
# PORT is supplied by docker-compose (defaults there to 8080).
EXPOSE 8080
CMD ["node", "--enable-source-maps", "dist/index.mjs"]
+32
View File
@@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1
#
# Web image: builds the Vite static bundle and serves it with nginx, which also
# reverse-proxies /api to the api service.
#
# NOTE: build MUST be glibc/amd64 (see Dockerfile.api for the reason). The
# vite.config.ts requires PORT and BASE_PATH to be set even for `build`.
# ---- Build stage -----------------------------------------------------------
FROM --platform=linux/amd64 node:24-bookworm-slim AS build
ENV PNPM_HOME=/pnpm
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9 --activate
WORKDIR /repo
COPY . .
RUN pnpm install --frozen-lockfile
# PORT + BASE_PATH are required by vite.config.ts at config load time.
RUN PORT=8080 BASE_PATH=/ NODE_ENV=production pnpm --filter @workspace/ehsan-poc run build
# ---- Runtime stage ---------------------------------------------------------
# The glibc/amd64 constraint applies only to the BUILD stage (node native deps).
# The runtime just serves static files, so the lightweight nginx:alpine is fine.
FROM --platform=linux/amd64 nginx:1.27-alpine AS runtime
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /repo/artifacts/ehsan-poc/dist/public /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Executable
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
#
# EHSAN — Mac Mini deployment script.
#
# Pulls the latest code from the Gitea repository, then rebuilds and restarts
# the Docker containers. Run this ON THE MAC MINI, from the repo checkout.
#
# Flow: Replit -> push to Gitea -> (this script) Mac Mini pulls -> Docker redeploys
#
# Usage:
# ./deploy.sh
#
# Optional overrides (environment variables):
# GIT_REMOTE Git remote to pull from (default: gitea)
# GIT_BRANCH Branch to deploy (default: main)
# WEB_PORT Host port for the web app (default: 8080)
set -euo pipefail
GIT_REMOTE="${GIT_REMOTE:-gitea}"
GIT_BRANCH="${GIT_BRANCH:-main}"
# Always operate from the directory this script lives in.
cd "$(dirname "$0")"
# --- Resolve the docker compose command (v2 plugin or legacy binary) --------
if docker compose version >/dev/null 2>&1; then
COMPOSE="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE="docker-compose"
else
echo "ERROR: Docker Compose was not found. Install Docker Desktop (includes the" >&2
echo " 'docker compose' plugin) or the standalone 'docker-compose' binary." >&2
exit 1
fi
echo "=================================================="
echo " EHSAN deploy | remote=${GIT_REMOTE} branch=${GIT_BRANCH}"
echo "=================================================="
echo ""
echo "==> [1/4] Pulling latest code from ${GIT_REMOTE}/${GIT_BRANCH}..."
if ! git pull "${GIT_REMOTE}" "${GIT_BRANCH}"; then
echo "ERROR: 'git pull ${GIT_REMOTE} ${GIT_BRANCH}' failed. Is the '${GIT_REMOTE}' remote" >&2
echo " configured and reachable? (git remote add ${GIT_REMOTE} <GITEA_URL>)" >&2
exit 1
fi
echo ""
echo "==> [2/4] Stopping current containers..."
if ! ${COMPOSE} down --remove-orphans; then
echo "ERROR: Failed to stop the existing containers." >&2
exit 1
fi
echo ""
echo "==> [3/4] Rebuilding containers (this can take a few minutes)..."
if ! ${COMPOSE} build --pull; then
echo "ERROR: Docker image build failed. See the build output above." >&2
exit 1
fi
echo ""
echo "==> [4/4] Starting containers..."
if ! ${COMPOSE} up -d; then
echo "ERROR: Failed to start the containers." >&2
exit 1
fi
echo ""
echo "=================================================="
echo " SUCCESS: Deployment complete."
echo "=================================================="
${COMPOSE} ps
echo ""
echo "The app should now be available on this machine at: http://localhost:${WEB_PORT:-8080}"
+45
View File
@@ -0,0 +1,45 @@
# Docker Compose for the EHSAN app on the Mac Mini.
#
# Two services:
# - api : Express API server (internal only, reached via the web proxy)
# - web : nginx serving the built SPA + reverse-proxying /api -> api
#
# Platform is pinned to linux/amd64 because the workspace strips all native
# binaries that are not linux-x64-gnu. On Apple Silicon this runs under
# emulation/Rosetta automatically.
services:
api:
build:
context: .
dockerfile: Dockerfile.api
platform: linux/amd64
environment:
NODE_ENV: production
PORT: "8080"
expose:
- "8080"
restart: unless-stopped
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://localhost:8080/api/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
web:
build:
context: .
dockerfile: Dockerfile.web
platform: linux/amd64
ports:
# Host port is configurable: WEB_PORT (default 8080) -> container :80
- "${WEB_PORT:-8080}:80"
depends_on:
api:
condition: service_healthy
restart: unless-stopped
+28
View File
@@ -0,0 +1,28 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip for static assets
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# Forward API calls to the api service (same Docker network).
# The prefix /api is preserved, which is what the API server expects.
location /api/ {
proxy_pass http://api:8080;
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;
}
# SPA fallback — let the client router handle unknown paths.
location / {
try_files $uri $uri/ /index.html;
}
}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
#
# Push this project to the central Gitea repository.
#
# Run this FROM REPLIT (the development environment). It pushes the `main`
# branch to the `gitea` remote so the Mac Mini can pull and redeploy.
#
# Flow: (this script) Replit -> push to Gitea -> Mac Mini pulls -> Docker redeploys
#
# One-time setup — add the gitea remote with your Gitea repo URL:
# git remote add gitea https://<user>:<token>@<gitea-host>/<owner>/<repo>.git
# # or via SSH:
# git remote add gitea git@<gitea-host>:<owner>/<repo>.git
#
# After that, just run:
# ./scripts/push-to-gitea.sh
#
# Optional overrides (environment variables):
# GIT_REMOTE Remote name (default: gitea)
# GIT_BRANCH Branch to push (default: main)
# GITEA_REMOTE_URL If set and the remote does not exist, it is added first.
set -euo pipefail
GIT_REMOTE="${GIT_REMOTE:-gitea}"
GIT_BRANCH="${GIT_BRANCH:-main}"
# Add the remote automatically if a URL was provided and it is missing.
if ! git remote get-url "${GIT_REMOTE}" >/dev/null 2>&1; then
if [ -n "${GITEA_REMOTE_URL:-}" ]; then
echo "==> Adding '${GIT_REMOTE}' remote -> ${GITEA_REMOTE_URL}"
git remote add "${GIT_REMOTE}" "${GITEA_REMOTE_URL}"
else
echo "ERROR: Git remote '${GIT_REMOTE}' is not configured." >&2
echo " Add it once with:" >&2
echo " git remote add ${GIT_REMOTE} <GITEA_REPO_URL>" >&2
echo " Or re-run with the URL inline:" >&2
echo " GITEA_REMOTE_URL=<GITEA_REPO_URL> ./scripts/push-to-gitea.sh" >&2
exit 1
fi
fi
# NOTE: never echo the remote URL — it may contain an embedded token.
echo "==> Pushing '${GIT_BRANCH}' to '${GIT_REMOTE}'..."
if git push "${GIT_REMOTE}" "${GIT_BRANCH}"; then
echo "SUCCESS: Pushed ${GIT_BRANCH} to ${GIT_REMOTE}."
echo "Next: on the Mac Mini run ./deploy.sh to pull and redeploy."
else
echo "ERROR: Push to ${GIT_REMOTE}/${GIT_BRANCH} failed." >&2
exit 1
fi