Files
marketing-app/DEPLOYMENT_PLAN.md
2026-02-23 11:57:32 +03:00

8.0 KiB

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)

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
sudo systemctl restart sshd

Test in a new terminal before closing your current session.

0.3 Firewall (ufw)

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

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Select Yes.

0.5 Fail2ban

sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

0.6 Disable unused services

sudo ss -tlnp
# Disable anything unexpected:
# sudo systemctl disable --now <service-name>

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

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

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=<token-from-step-3>
NOCODB_BASE_ID=<base-id-from-step-3>
SESSION_SECRET=<generate-with: openssl rand -hex 32>
CORS_ORIGIN=https://marketing.yourdomain.com
NODE_ENV=production

2.4 Install and register Gitea runner

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 <token-from-phase-1-step-5> \
  --name server-a \
  --labels ubuntu-latest:host

2.5 systemd service for the runner

# /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
sudo systemctl enable --now gitea-runner

2.6 systemd service for marketing-app

# /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
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

# /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;
}
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

# .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

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