# Marketing App — Deployment Plan ## Architecture | Server | Role | What runs | |--------|------|-----------| | **Server A** (Hetzner, Ubuntu 24.04) | Frontends + Express backends | Nginx, act_runner, marketing-app, hihala-dashboard, other apps | | **Server B** (Hetzner, Cloudron) | Data layer + Git | Cloudron, NocoDB, Gitea | ``` Server B (Cloudron) Server A (Ubuntu 24.04) ┌──────────────────────┐ ┌──────────────────────────────┐ │ Gitea (git repos) │◄── register ─│ act_runner (Gitea runner) │ │ NocoDB (databases) │ │ Nginx │ │ │── webhook ──►│ marketing-app (systemd) │ │ │ │ hihala-dashboard (systemd) │ └──────────────────────┘ └──────────────────────────────┘ ``` Flow: `local dev → git push → Gitea (Server B) → triggers runner on Server A → build + deploy` --- ## Phase 0 — Secure Server A (Ubuntu 24.04) ### 0.1 Create non-root user (if still logging in as root) ```bash adduser fahed usermod -aG sudo fahed mkdir -p /home/fahed/.ssh cp /root/.ssh/authorized_keys /home/fahed/.ssh/authorized_keys chown -R fahed:fahed /home/fahed/.ssh chmod 700 /home/fahed/.ssh chmod 600 /home/fahed/.ssh/authorized_keys ``` Test in a **new terminal** before continuing: `ssh fahed@server-a` ### 0.2 Lock down SSH Edit `/etc/ssh/sshd_config`: ``` PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes MaxAuthTries 3 AllowUsers fahed ``` ```bash sudo systemctl restart sshd ``` Test in a **new terminal** before closing your current session. ### 0.3 Firewall (ufw) ```bash sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP (certbot + redirect) sudo ufw allow 443/tcp # HTTPS sudo ufw enable sudo ufw status ``` ### 0.4 Automatic security updates ```bash sudo apt install -y unattended-upgrades sudo dpkg-reconfigure -plow unattended-upgrades ``` Select **Yes**. ### 0.5 Fail2ban ```bash sudo apt install -y fail2ban sudo systemctl enable --now fail2ban ``` ### 0.6 Disable unused services ```bash sudo ss -tlnp # Disable anything unexpected: # sudo systemctl disable --now ``` Server B (Cloudron) handles its own security. --- ## Phase 1 — Server B (Cloudron) 1. Install **Gitea** from Cloudron app store → `https://gitea.yourdomain.com` 2. Install **NocoDB** from Cloudron app store → `https://nocodb.yourdomain.com` 3. On NocoDB: create a base for marketing-app, generate an API token 4. On Gitea: create a repo `marketing-app`, push your local code to it 5. On Gitea: go to **Site Admin → Runners** and copy the runner registration token --- ## Phase 2 — Server A (Ubuntu 24.04) ### 2.1 Install dependencies ```bash curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash - sudo apt install -y nodejs nginx certbot python3-certbot-nginx ``` ### 2.2 Create app directory ```bash sudo mkdir -p /opt/apps/marketing-app sudo chown fahed:fahed /opt/apps/marketing-app ``` ### 2.3 Create .env file (set once, never touched by deploys) ``` # /opt/apps/marketing-app/server/.env NOCODB_URL=https://nocodb.yourdomain.com NOCODB_TOKEN= NOCODB_BASE_ID= SESSION_SECRET= CORS_ORIGIN=https://marketing.yourdomain.com NODE_ENV=production ``` ### 2.4 Install and register Gitea runner ```bash sudo wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64 -O /usr/local/bin/act_runner sudo chmod +x /usr/local/bin/act_runner mkdir -p /opt/gitea-runner && cd /opt/gitea-runner act_runner register \ --instance https://gitea.yourdomain.com \ --token \ --name server-a \ --labels ubuntu-latest:host ``` ### 2.5 systemd service for the runner ```ini # /etc/systemd/system/gitea-runner.service [Unit] Description=Gitea Actions Runner After=network.target [Service] Type=simple User=fahed WorkingDirectory=/opt/gitea-runner ExecStart=/usr/local/bin/act_runner daemon Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl enable --now gitea-runner ``` ### 2.6 systemd service for marketing-app ```ini # /etc/systemd/system/marketing-app.service [Unit] Description=Marketing App After=network.target [Service] Type=simple User=fahed WorkingDirectory=/opt/apps/marketing-app/server ExecStart=/usr/bin/node server.js Restart=always RestartSec=5 EnvironmentFile=/opt/apps/marketing-app/server/.env [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl enable marketing-app ``` ### 2.7 Allow passwordless restart for deploys ``` # /etc/sudoers.d/gitea-runner fahed ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart marketing-app fahed ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart hihala-dashboard ``` ### 2.8 DNS Point `marketing.yourdomain.com` to Server A's IP. ### 2.9 Nginx vhost ```nginx # /etc/nginx/sites-available/marketing-app server { listen 443 ssl; server_name marketing.yourdomain.com; ssl_certificate /etc/letsencrypt/live/marketing.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/marketing.yourdomain.com/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:3001; 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 50M; } location / { root /opt/apps/marketing-app/client/dist; try_files $uri $uri/ /index.html; } } server { listen 80; server_name marketing.yourdomain.com; return 301 https://$host$request_uri; } ``` ```bash sudo ln -s /etc/nginx/sites-available/marketing-app /etc/nginx/sites-enabled/ sudo certbot --nginx -d marketing.yourdomain.com sudo nginx -t && sudo systemctl reload nginx ``` --- ## Phase 3 — CI/CD Pipeline ### 3.1 Add workflow file to repo ```yaml # .gitea/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Build frontend run: cd client && npm ci && npm run build - name: Deploy to app directory run: | rsync -a --delete client/dist/ /opt/apps/marketing-app/client/dist/ rsync -a server/ /opt/apps/marketing-app/server/ \ --exclude node_modules --exclude '*.db' --exclude uploads --exclude .env - name: Install production deps run: cd /opt/apps/marketing-app/server && npm ci --production - name: Restart service run: sudo systemctl restart marketing-app ``` ### 3.2 Add Gitea remote and push ```bash cd ~/clawd/projects/marketing-app git remote add gitea https://gitea.yourdomain.com/fahed/marketing-app.git git push gitea main ``` --- ## Phase 4 — Verify 1. Watch the runner: `journalctl -u gitea-runner -f` 2. Watch the app: `journalctl -u marketing-app -f` 3. Open `https://marketing.yourdomain.com` 4. Test: login, create an artefact, upload a file --- ## Code changes already made (in server.js) - Session secret reads from `process.env.SESSION_SECRET` with `'dev-fallback-secret'` fallback - CORS locks to `process.env.CORS_ORIGIN` in production, open locally - Cookie `secure: true` when `NODE_ENV=production` - `trust proxy` enabled when `NODE_ENV=production` - Static file serving of `client/dist/` + SPA fallback when `NODE_ENV=production` --- ## Adding another app later (e.g. hihala-dashboard) Repeat phases 1.3-1.4, 2.2-2.3, 2.6, 2.8-2.9, 3.1-3.2 with: - Different port (3002) - Different subdomain (hihala.yourdomain.com) - Different systemd service name (hihala-dashboard) - Same runner handles all repos