← Alle Artikel
Zuletzt aktualisiert: 2026-03-30

Funktioniert lokal aber nicht live? So deployen Sie Ihre App in Produktion

Node.js- oder Python-App auf einem VPS deployen. Nginx Reverse Proxy, SSL, Umgebungsvariablen, systemd.

Kurzfassung (TL;DR)

Die App laeuft auf localhost:3000, aber auf dem Server nicht? Die haeufigsten Ursachen: fest eingebaute Ports und Hostnamen, fehlende Umgebungsvariablen, kein HTTPS, falsche CORS-Konfiguration und ein fehlender Prozessmanager. Dieser Artikel fuehrt Schritt fuer Schritt vom lokalen Projekt bis zum vollautomatischen Deployment mit Nginx, SSL-Zertifikat und CI/CD-Pipeline.

Voraussetzungen

  • Ein VPS (Ubuntu 22.04/24.04) mit Root- oder Sudo-Zugriff
  • Eine registrierte Domain mit A-Record auf die Server-IP
  • Eine lokal funktionstuechtige Anwendung
  • Grundkenntnisse in SSH und der Linux-Kommandozeile
  • Ein GitHub-Repository mit dem Projektcode

1. Localhost vs. Produktion — Was sich aendert

Wer versteht, warum etwas auf dem Server anders funktioniert, hat die Haelfte der Arbeit schon erledigt. Hier die wichtigsten Unterschiede:

Umgebungsvariablen

Lokal liegt oft eine .env-Datei im Projektverzeichnis, oder Werte stehen direkt im Code. In Produktion muessen diese Werte explizit und sicher gesetzt werden — und duerfen auf keinen Fall ins Repository gelangen.

# Lokale .env (NICHT deployen)
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=dev-key-12345
NODE_ENV=development

# Produktion — per systemd, PM2 oder Shell-Profil
DATABASE_URL=mongodb://db-server:27017/myapp?authSource=admin
API_KEY=prod-key-echtes-geheimnis
NODE_ENV=production

Ports und Binding

Lokal lauscht die App auf localhost:3000. In Produktion sollte sie auf 127.0.0.1 binden und hinter einem Reverse Proxy auf Port 80/443 erreichbar sein — nicht direkt oeffentlich.

CORS (Cross-Origin Resource Sharing)

Auf dem Entwicklungsrechner laufen Frontend und Backend meist unter der gleichen Adresse. Auf dem Server ist die API vielleicht unter api.example.com erreichbar, das Frontend unter example.com.

// Express.js — '*' ist in Produktion keine gute Idee
const cors = require('cors');
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'https://example.com',
  credentials: true
}));

HTTPS

Browser setzen fuer produktive Seiten strenge Sicherheitsrichtlinien durch. Funktionen wie Geolocation, Service Worker und sichere Cookies erfordern ein gueltiges SSL-Zertifikat. Daran fuehrt kein Weg vorbei.

Prozess-Lebenszyklus

Lokal drueckt man Strg+C und die App stoppt. In Produktion muss die Anwendung Neustarts, Abstuerze und Speicherlecks ueberleben. Dafuer braucht es einen Prozessmanager.

2. App fuer die Produktion vorbereiten

Konfiguration auslagern

Jeder Wert, der sich zwischen Entwicklung und Produktion unterscheidet, gehoert in eine Umgebungsvariable:

# Node.js
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
  console.error('DATABASE_URL ist nicht gesetzt. Beende Prozess.');
  process.exit(1);
}

# Python (Flask/FastAPI)
import os
PORT = int(os.environ.get('PORT', 8000))
DATABASE_URL = os.environ['DATABASE_URL']  # KeyError wenn nicht gesetzt

Health-Check-Endpunkt einrichten

// Express.js
app.get('/health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

# FastAPI
@app.get('/health')
def health():
    return {'status': 'ok'}

Produktions-Startskript definieren

In der package.json (Node.js):

{
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js"
  }
}

3. Deployment auf einem VPS

Server-Grundeinrichtung

# Per SSH auf den Server verbinden
ssh root@deine-server-ip

# Deploy-Benutzer anlegen (Apps nie als root ausfuehren)
adduser deploy
usermod -aG sudo deploy

# SSH-Key-Authentifizierung fuer den Deploy-Benutzer
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh

Laufzeitumgebung installieren

# Node.js (via NodeSource)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# Python
sudo apt-get install -y python3 python3-pip python3-venv

# Nginx
sudo apt-get install -y nginx

Variante A: Node.js mit PM2

# PM2 global installieren
sudo npm install -g pm2

# App klonen und einrichten
cd /home/deploy
git clone https://github.com/deinuser/deineapp.git
cd deineapp
npm ci --production

# PM2-Konfiguration erstellen
cat > ecosystem.config.js << 'EOF'
module.exports = {
  apps: [{
    name: 'meineapp',
    script: 'src/server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      DATABASE_URL: 'mongodb://localhost:27017/meineapp'
    },
    max_memory_restart: '500M',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
  }]
};
EOF

# App starten
pm2 start ecosystem.config.js

# Prozessliste speichern und Autostart aktivieren
pm2 save
pm2 startup systemd -u deploy --hp /home/deploy

Variante B: Node.js mit systemd

# systemd-Service erstellen
sudo cat > /etc/systemd/system/meineapp.service << 'EOF'
[Unit]
Description=Meine Node.js App
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/deineapp
ExecStart=/usr/bin/node src/server.js
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/home/deploy/deineapp/.env.production
StandardOutput=journal
StandardError=journal
SyslogIdentifier=meineapp

[Install]
WantedBy=multi-user.target
EOF

# Aktivieren und starten
sudo systemctl daemon-reload
sudo systemctl enable meineapp
sudo systemctl start meineapp
sudo systemctl status meineapp

Variante C: Python mit Gunicorn und systemd

# Virtuelle Umgebung einrichten
cd /home/deploy/deineapp
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install gunicorn

# systemd-Service erstellen
sudo cat > /etc/systemd/system/meineapp.service << 'EOF'
[Unit]
Description=Meine Python App (Gunicorn)
After=network.target

[Service]
Type=notify
User=deploy
WorkingDirectory=/home/deploy/deineapp
ExecStart=/home/deploy/deineapp/venv/bin/gunicorn \
    --workers 3 \
    --bind 127.0.0.1:8000 \
    --access-logfile /var/log/meineapp/access.log \
    --error-logfile /var/log/meineapp/error.log \
    app:app
Restart=on-failure
RestartSec=5
EnvironmentFile=/home/deploy/deineapp/.env.production

[Install]
WantedBy=multi-user.target
EOF

# Log-Verzeichnis anlegen und starten
sudo mkdir -p /var/log/meineapp
sudo chown deploy:deploy /var/log/meineapp
sudo systemctl daemon-reload
sudo systemctl enable meineapp
sudo systemctl start meineapp

4. Nginx als Reverse Proxy

Nginx steht vor der Anwendung und uebernimmt SSL-Terminierung, statische Dateien und Lastverteilung.

# /etc/nginx/sites-available/meineapp
server {
    listen 80;
    server_name example.com www.example.com;

    # HTTP auf HTTPS weiterleiten (nach SSL-Einrichtung)
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # SSL-Zertifikate (von Certbot verwaltet — siehe naechster Abschnitt)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Sicherheitsheader
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Anfragen an die App weiterleiten
    location / {
        proxy_pass http://127.0.0.1:3000;
        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_cache_bypass $http_upgrade;
        proxy_read_timeout 90s;
    }

    # Statische Dateien direkt ausliefern (optional)
    location /static/ {
        alias /home/deploy/deineapp/public/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Health Check ohne Logging
    location /health {
        proxy_pass http://127.0.0.1:3000;
        access_log off;
    }
}
# Seite aktivieren und testen
sudo ln -s /etc/nginx/sites-available/meineapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Wichtig: Ein 502 Bad Gateway bedeutet, dass Nginx die App nicht erreichen kann. Pruefen mit: curl -v http://127.0.0.1:3000/health

5. SSL mit Let's Encrypt

# Certbot installieren
sudo apt-get install -y certbot python3-certbot-nginx

# Zertifikat beziehen (Certbot konfiguriert Nginx automatisch)
sudo certbot --nginx -d example.com -d www.example.com

# Automatische Verlaengerung pruefen
sudo certbot renew --dry-run

# Certbot richtet einen systemd-Timer ein:
sudo systemctl status certbot.timer

Certbot ergaenzt die Nginx-Konfiguration automatisch um die SSL-Direktiven. Die Zertifikate werden alle 60-90 Tage automatisch erneuert.

Tipp: Falls der DNS-Eintrag noch nicht auf den Server zeigt, schlaegt Certbot fehl. Vorher pruefen:

dig +short example.com
# Sollte die IP des Servers zurueckgeben

6. CI/CD mit GitHub Actions

Automatisierte Deployments ersparen das manuelle Einloggen und git pull.

SSH Deploy Key einrichten

# Auf dem lokalen Rechner einen Deploy Key erzeugen
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -N ""

# Oeffentlichen Schluessel auf den Server kopieren
cat ~/.ssh/deploy_key.pub | ssh deploy@deine-server-ip \
  "cat >> ~/.ssh/authorized_keys"

# Privaten Schluessel als GitHub Secret hinterlegen:
# Repository > Settings > Secrets > DEPLOY_SSH_KEY

GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Per SSH deployen
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_IP }}
          username: deploy
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /home/deploy/deineapp
            git pull origin main
            npm ci --production
            pm2 restart meineapp --update-env
            echo "Deployed am $(date)"

Tests vor dem Deployment

# .github/workflows/deploy.yml (erweitert)
name: Testen & Deployen

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Per SSH deployen
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_IP }}
          username: deploy
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /home/deploy/deineapp
            git pull origin main
            npm ci --production
            pm2 restart meineapp --update-env

7. Logs und Debugging

Wenn in Produktion etwas schiefgeht, sind Logs die erste Anlaufstelle.

# PM2-Logs
pm2 logs meineapp --lines 100
pm2 logs meineapp --err           # Nur Fehlerausgabe

# systemd-Journal
sudo journalctl -u meineapp -f              # Live mitverfolgen
sudo journalctl -u meineapp --since "1 hour ago"
sudo journalctl -u meineapp --since "2025-01-15 14:00"

# Nginx-Logs
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

# Gunicorn-Logs
sudo tail -f /var/log/meineapp/error.log

# Welcher Prozess lauscht auf einem Port?
sudo ss -tlnp | grep :3000

# App direkt testen (ohne Nginx)
curl -v http://127.0.0.1:3000/health

# Ueber Nginx testen
curl -v https://example.com/health

Fehlersuche

502 Bad Gateway

Nginx kann die App nicht erreichen. Pruefen:

  • Laeuft die App? sudo systemctl status meineapp oder pm2 status
  • Lauscht sie auf dem richtigen Port? sudo ss -tlnp | grep :3000
  • Stimmt der Port in der Nginx-Konfiguration?

CORS-Fehler in der Browserkonsole

Die API liefert keinen passenden Access-Control-Allow-Origin-Header.

  • CORS-Middleware muss die produktive Frontend-URL enthalten, nicht localhost
  • Preflight-Anfragen (OPTIONS) muessen behandelt werden
  • Bei Cookies darf Access-Control-Allow-Origin nicht * sein

Mixed-Content-Warnungen

Die Seite wird per HTTPS geladen, aber API-Aufrufe gehen an HTTP-URLs.

  • Alle API-URLs im Frontend auf https:// umstellen
  • Relative URLs verwenden (/api/data) oder aus window.location.origin zusammenbauen

App stuerzt beim Start ab

  • Fehlende Umgebungsvariablen — journalctl -u meineapp zeigt den Fehler
  • Port bereits belegt — sudo ss -tlnp | grep :3000
  • Fehlende Abhaengigkeiten — wurde npm ci oder pip install -r requirements.txt ausgefuehrt?

SSL-Zertifikat funktioniert nicht

  • DNS zeigt nicht auf den Server — dig +short example.com
  • Port 80 von Firewall blockiert — sudo ufw allow 80
  • Certbot-Timer laeuft nicht — sudo systemctl status certbot.timer

Aenderungen nach dem Deployment nicht sichtbar

  • Browser-Cache — harter Reload mit Strg+Umschalt+R
  • CDN- oder Nginx-Cache — waehrend der Fehlersuche deaktivieren
  • App nicht neu gestartet — pm2 restart meineapp oder sudo systemctl restart meineapp

Praevention

Vor jedem Deployment diese Punkte abarbeiten:

  • Alle Konfigurationswerte kommen aus Umgebungsvariablen (keine fest eingebauten URLs, Ports oder Geheimnisse)
  • .env-Dateien stehen in der .gitignore
  • App bindet an 127.0.0.1, nicht an 0.0.0.0 (Nginx uebernimmt den oeffentlichen Verkehr)
  • CORS ist mit der exakten Produktions-Origin konfiguriert
  • Prozessmanager (PM2/systemd) ist eingerichtet mit automatischem Neustart
  • Nginx-Konfiguration ist mit nginx -t getestet
  • SSL-Zertifikat ist gueltig und wird automatisch erneuert
  • Health-Check-Endpunkt existiert und wird ueberwacht
  • CI/CD-Pipeline fuehrt Tests vor dem Deployment aus
  • Firewall gibt nur die Ports 22, 80 und 443 frei
  • Log-Rotation ist eingerichtet, damit die Festplatte nicht volllaeuft
  • Ein Rollback wurde getestet — laesst sich im Notfall schnell zurueckrollen?

Experten-Hilfe gebraucht?

Immer noch fest? Ich deploye live per Screensharing. €49, 30 Min.

Jetzt buchen — €49

100% Geld-zurück-Garantie

HR

Harald Roessler

Infrastructure Engineer mit 20+ Jahren Erfahrung. Gründer der DSNCON GmbH.