portfolio-page/backend/deploy/DEPLOYMENT.md

5.8 KiB

Deploying the audit endpoint on the IONOS VPS

The website itself stays on the Bitpalast webspace (static files via FTP). Only this endpoint runs on the VPS, alongside the existing Git and Nextcloud services. Nothing here modifies their configuration.

Assumes Debian or Ubuntu with sudo. Adjust package commands for other distributions.


0. DNS

Create an A record api.sascha-bach.de pointing at the VPS IP address (and an AAAA record if the VPS has IPv6). Wait until it resolves:

dig +short api.sascha-bach.de

Nothing below works until this answers.


1. Survey the machine first

ssh <user>@<vps>

# Which web server is in front? The nginx config below assumes nginx.
systemctl is-active nginx apache2 2>/dev/null

# Is port 3001 free? Gitea commonly sits on 3000, so check before assuming.
sudo ss -tlnp | grep -E ':(3000|3001)\b' || echo "3001 frei"

# Node 20 or newer present?
node --version 2>/dev/null || echo "Node fehlt"

If Apache is in front instead of nginx, skip step 6 and set up an Apache ProxyPass for api.sascha-bach.de instead — the rest is unchanged.

If 3001 is taken, pick another free port and change it in three places: .env (PORT), the nginx proxy_pass lines, and nothing else.


2. Install Node (only if missing)

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version   # must be >= 20

3. Dedicated service user

The endpoint must not run as root and must not share the Git user's account.

sudo useradd --system --create-home --home-dir /var/lib/auditapi \
     --shell /usr/sbin/nologin auditapi

4. Fetch the code

sudo mkdir -p /opt/audit-endpoint
sudo chown auditapi:auditapi /opt/audit-endpoint

sudo -u auditapi git clone https://git.sascha-bach.de/saschabach/portfolio-page.git \
     /opt/audit-endpoint

If the repository is private, use a deploy token or an SSH key belonging to auditapi.

cd /opt/audit-endpoint/backend
sudo -u auditapi npm install --omit=dev

5. Configuration

sudo -u auditapi cp /opt/audit-endpoint/backend/.env.example \
                    /opt/audit-endpoint/backend/.env
sudo -u auditapi nano /opt/audit-endpoint/backend/.env

Fill in:

Variable Value
PORT 3001 (or the free port from step 1)
ALLOWED_ORIGINS https://sascha-bach.de,https://www.sascha-bach.de — the website origin, not the API origin
AUDIT_RECIPIENT where requests are delivered
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS mailbox credentials
SMTP_FROM an address on your own domain, so SPF and DMARC pass

Lock the file down — it holds the mailbox password:

sudo chmod 600 /opt/audit-endpoint/backend/.env
sudo chown auditapi:auditapi /opt/audit-endpoint/backend/.env

Smoke-test before wiring up systemd:

cd /opt/audit-endpoint/backend && sudo -u auditapi node server.js
# expect: Audit endpoint listening on 127.0.0.1:3001
# Ctrl+C

6. systemd service

sudo cp /opt/audit-endpoint/backend/deploy/audit-endpoint.service \
        /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now audit-endpoint

systemctl status audit-endpoint --no-pager
curl -s http://127.0.0.1:3001/health    # → {"status":"ok"}

7. nginx vhost

Adding a file and reloading never touches the running Git or Nextcloud vhosts.

sudo cp /opt/audit-endpoint/backend/deploy/nginx-api.sascha-bach.de.conf \
        /etc/nginx/sites-available/api.sascha-bach.de
sudo ln -s /etc/nginx/sites-available/api.sascha-bach.de \
           /etc/nginx/sites-enabled/

# Validates the WHOLE config. If this fails, nothing has changed yet.
sudo nginx -t

# reload, not restart - existing connections to git/nextcloud survive
sudo systemctl reload nginx

8. TLS certificate

sudo apt-get install -y certbot python3-certbot-nginx    # if missing
sudo certbot --nginx -d api.sascha-bach.de

certbot only edits the api.sascha-bach.de block. Verify renewal works:

sudo certbot renew --dry-run

9. Verify from outside

curl -sI https://api.sascha-bach.de/health        # → HTTP/2 200

curl -s -X POST https://api.sascha-bach.de/api/audit-request \
  -H 'Content-Type: application/json' \
  -H 'Origin: https://sascha-bach.de' \
  -d '{"name":"Test","company":"Test GmbH","email":"du@example.de",
       "url":"https://example.de","role":"client","motivation":"Test",
       "businessConfirmation":"true","gdprConsent":"true","website":""}'
# → {"success":true,...} and an email arrives

Then confirm the protections actually bite:

# Foreign origin → rejected
curl -s -X POST https://api.sascha-bach.de/api/audit-request \
  -H 'Content-Type: application/json' -H 'Origin: https://evil.example' \
  -d '{}' -o /dev/null -w '%{http_code}\n'

# Fourth request within 15 minutes → 429

10. Point the website at it

Back on the workstation, in the project root:

echo 'VITE_AUDIT_ENDPOINT=https://api.sascha-bach.de/api/audit-request' > .env
npm run deploy

Vite bakes the value in at build time, so this must happen before the upload. .env is gitignored.

The hostname must match in three places or the browser blocks the request:

  • public/.htaccessconnect-src 'self' https://api.sascha-bach.de
  • VITE_AUDIT_ENDPOINT
  • the nginx server_name and its certificate

Updating later

cd /opt/audit-endpoint
sudo -u auditapi git pull
cd backend && sudo -u auditapi npm install --omit=dev
sudo systemctl restart audit-endpoint

Logs

journalctl -u audit-endpoint -f

Request contents are never logged, only that a request was forwarded — see the note in services/emailService.js.