-
Notifications
You must be signed in to change notification settings - Fork 0
IT Docker
Caution
Diese Seite richtet sich ausschliesslich an Informatikerinnen und Informatiker. Nicht für Laien geeignet: Falsch eingebundene Volumes führen zu Datenverlust beim nächsten Container-Neubau; falsche Proxy-Einstellungen sperren alle Anfragen oder hebeln Rate-Limits aus. Ohne Fachkenntnisse bitte Installation Schritt für Schritt verwenden.
Für wen ist diese Seite? Für Informatikerinnen und Informatiker, die SmallTime 2027 mit Docker oder Docker Compose betreiben. Grundlage sind backend/Dockerfile, backend/docker-entrypoint.sh, frontend/Dockerfile und frontend/nginx.conf im Repository.
- Die Images im Repository
- Hinweise zur mitgelieferten nginx.conf
- Pfade im Backend-Container
- Volumes
- Umgebungsvariablen
- Beispiel: docker-compose.yml
- Beispiel: nginx.conf für den Frontend-Container
- Betrieb
- Alternative: ein Container mit dem Betriebspaket
Beide Dockerfiles erwarten den Repository-Stamm als Build-Kontext (sie kopieren backend/… bzw. frontend/…):
docker build -f backend/Dockerfile -t smalltime-backend:1.0.0 .
docker build -f frontend/Dockerfile -t smalltime-frontend:1.0.0 .backend/Dockerfile (Multi-Stage):
| Stage | Basis | Inhalt |
|---|---|---|
build |
node:26.9.0-alpine |
npm ci, npm run build (esbuild → dist/server.js, dist/migrate.js) |
runtime |
node:26.9.0-alpine |
NODE_ENV=production, npm ci --omit=dev, dist/ und docker-entrypoint.sh, leere Ordner config/, data/, uploads/; EXPOSE 55000
|
docker-entrypoint.sh:
#!/bin/sh
set -eu
node dist/migrate.js
exec node dist/server.jsDie Migrationen laufen also vor dem Serverstart (und beim Start ein zweites Mal ohne Wirkung). exec sorgt dafür, dass Node PID 1 ist und SIGTERM erhält.
frontend/Dockerfile (Multi-Stage):
| Stage | Basis | Inhalt |
|---|---|---|
build |
node:26.9.0-alpine |
npm ci, npm run build; kopiert backend/config mit, weil vite.config.ts daraus Ports und Upload-Endungen liest |
runtime |
nginx:1.27-alpine |
frontend/nginx.conf, Build nach /usr/share/nginx/html; EXPOSE 80
|
Die mitgelieferte nginx.conf liefert das SPA aus (try_files … /index.html), setzt Sicherheitsheader und leitet /api an http://backend:55000 weiter. Der Backend-Service muss deshalb backend heissen.
Das Backend-Image enthält kein Frontend. Unter / antwortet es mit dem Hinweis «Kein gebautes Frontend gefunden». Im Zwei-Container-Betrieb liefert nginx das Frontend.
frontend/nginx.conf enthält seit Version 1.0.0:
-
Upgrade/Connection-Header für den WebSocket/api/live, -
location = /custom.css(Weiterleitung ans Backend fürdata/custom.css), -
frame-src 'self' blob:in der CSP (PDF-Vorschau), -
client_max_body_size 20m(Uploads).
Verbleibende Unterschiede:
| Punkt | Folge | Abhilfe |
|---|---|---|
CSP mit style-src 'unsafe-inline' statt Nonce |
schwächer als die CSP des Backends; der Platzhalter __CSP_NONCE__ in index.html wird nicht ersetzt |
hinnehmen oder Ein-Container-Variante |
X-Forwarded-For $proxy_add_x_forwarded_for hinter einem weiteren Proxy |
Backend (trust proxy = 1) sieht als Client-IP die IP des äusseren Proxys → alle teilen sich Rate-Limits |
real_ip-Modul, siehe Beispiel
|
WORKDIR ist /app/backend. Weil im Image weder frontend/ noch public/ existiert, greift die Fallback-Auflösung:
| Zweck | Pfad im Container |
|---|---|
| Konfiguration |
/app/backend/config (backend.json, security.json, optional frontend.json) |
Datenbank, Backups, Dokumente, custom.css
|
/app/backend/data |
| Uploads | /app/backend/uploads |
| Import aus SmallTime PHP | /app/convert |
.env (optional) |
/app/backend/.env |
Warning
Das Image enthält keine config/backend.json und config/security.json. Beim ersten Start schreibt das Backend security.json mit den Code-Standards: Länderfilter aus (allowedCountries: []), allowRegistration: false, allowLocalhost: true. Upload-Endungen ohne backend.json: nur .bmp .csv .gif .jpeg .jpg .json .md .png .txt .webp (kein PDF/Office). Deshalb vorbereitete Dateien ins Volume legen.
| Volume | Container-Pfad | Pflicht |
|---|---|---|
| Daten | /app/backend/data |
ja – Datenbank, tägliche Backups, Dokumente |
| Uploads | /app/backend/uploads |
ja |
| Konfiguration | /app/backend/config |
ja – sonst gehen Änderungen aus Administration → Sicherheit beim Neubau verloren |
| Import | /app/convert |
nur für die Übernahme aus SmallTime PHP |
Das Image läuft als root (kein USER im Dockerfile). Bind-Mounts gehören damit root; wer einen unprivilegierten Benutzer erzwingt (user: "1000:1000"), muss die Host-Verzeichnisse entsprechend besitzen lassen.
SQLite im WAL-Modus nicht auf Netzwerk-Volumes (NFS/SMB/CIFS) legen.
Vollständige Liste: Konfiguration. Für Docker relevant:
| Variable | Wert im Beispiel | Bemerkung |
|---|---|---|
NODE_ENV |
production |
im Image bereits gesetzt |
SESSION_SECRET |
aus .env des Compose-Projekts |
Pflicht, sonst Startabbruch |
FRONTEND_ORIGIN |
https://zeit.example.ch |
öffentliche Adresse; Lizenz-Domain |
TRUST_PROXY |
true |
nginx-Container ist der Proxy (ein Hop) |
SECURE_COOKIE |
true |
TLS endet vor dem nginx-Container; X-Forwarded-Proto ist dort http
|
ENABLE_COMPRESSION |
true |
Standard in production
|
BCRYPT_COST |
12 |
|
REDIS_URL |
redis://redis:6379 |
optional |
PORT |
– | Standard 55000; nginx erwartet 55000 |
SMALLTIME_AUTO_IMPORT |
– |
false schaltet den Import aus /app/convert ab |
Important
Beispiel, nicht Teil des Repositorys und nicht getestet. An die eigene Umgebung anpassen. Vorausgesetzt ist ein TLS-terminierender Reverse-Proxy auf dem Host (nginx, Caddy, Traefik …), der auf 127.0.0.1:8080 weiterleitet.
Verzeichnisstruktur neben dem Repository-Stamm:
SmallTime_2027.01/
├── docker-compose.yml
├── .env SESSION_SECRET=… (nur für Compose, nicht ins Image)
├── docker/nginx.conf siehe nächster Abschnitt
└── volumes/
├── config/backend.json Kopie von backend/config/backend.json
├── config/security.json vorbereitet, siehe unten
├── data/
├── uploads/
└── convert/ optional
# docker-compose.yml – BEISPIEL
name: smalltime
services:
backend:
build:
context: .
dockerfile: backend/Dockerfile
image: smalltime-backend:1.0.0
restart: unless-stopped
environment:
NODE_ENV: production
FRONTEND_ORIGIN: https://zeit.example.ch
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET fehlt in .env}
TRUST_PROXY: "true"
SECURE_COOKIE: "true"
ENABLE_COMPRESSION: "true"
BCRYPT_COST: "12"
# REDIS_URL: redis://redis:6379
volumes:
- ./volumes/config:/app/backend/config
- ./volumes/data:/app/backend/data
- ./volumes/uploads:/app/backend/uploads
- ./volumes/convert:/app/convert
expose:
- "55000"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:55000/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
stop_grace_period: 20s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
image: smalltime-frontend:1.0.0
restart: unless-stopped
depends_on:
backend:
condition: service_healthy
ports:
- "127.0.0.1:8080:80"
volumes:
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
# redis:
# image: redis:7-alpine
# restart: unless-stopped
# command: ["redis-server", "--save", "", "--appendonly", "no"]volumes/config/security.json (ohne Länder-Header vom Proxy):
{
"rateLimits": {
"login": { "limit": 5, "windowMinutes": 5 },
"passwordChange": { "limit": 5, "windowMinutes": 15 },
"upload": { "limit": 20, "windowMinutes": 10 }
},
"allowedCountries": [],
"countryHeaders": ["CF-IPCountry", "CloudFront-Viewer-Country", "X-Country-Code", "X-Geo-Country", "X-Country"],
"allowedIpRanges": [],
"blockedIpRanges": [],
"allowRegistration": false,
"allowLocalhost": false,
"bcryptCost": 12
}Note
Mit allowLocalhost: false und einem aktiven Länderfilter würde auch der Healthcheck (127.0.0.1) mit 403 abgewiesen. Mit leerer Länderliste ist der Healthcheck unproblematisch.
Start:
docker compose build
docker compose up -d
docker compose logs -f backendErstanmeldung: admin / admin1234 (Passwortwechsel erzwungen), sofern kein Import aus volumes/convert erfolgt ist.
Important
Beispiel, nicht getestet. Ersetzt frontend/nginx.conf per Volume, zusätzlich mit real_ip für einen äusseren Proxy. Die Adressbereiche bei set_real_ip_from an das Docker-Netz bzw. den äusseren Proxy anpassen.
# docker/nginx.conf – BEISPIEL
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 55m;
# Echte Client-IP vom äusseren Reverse-Proxy übernehmen (Host-Proxy kommt über das Docker-Gateway)
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self' blob:; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob:; connect-src 'self'; media-src 'self'; manifest-src 'self'" always;
location = /api/live {
proxy_pass http://backend:55000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 1h;
}
location /api {
proxy_pass http://backend:55000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
location = /custom.css {
proxy_pass http://backend:55000;
proxy_set_header Host $host;
}
location / {
try_files $uri $uri/ /index.html;
}
}X-Forwarded-Proto https ist fest gesetzt, weil TLS vor diesem Container endet. Der äussere Proxy muss Host durchreichen (Lizenzprüfung vergleicht den Hostnamen).
# Status / Logs
docker compose ps
docker compose logs --since 1h backend
# Online-Backup der Datenbank (sqlite3 ist im Image nicht enthalten → vom Host aus)
sqlite3 volumes/data/app.sqlite ".backup 'backup/app-$(date +%F).sqlite'"
# Update
git pull
docker compose build
docker compose up -d # Migrationen laufen im Entrypoint- Tägliche Backups (mit automatischer Rotation) schreibt die App nach
volumes/data/backups/(siehe Sicherheit und Betrieb). - Vor jedem Update Volumes sichern; Migrationen sind nur vorwärts.
- Nur eine Backend-Instanz betreiben (
deploy.replicas: 1, kein Scaling). - Import aus SmallTime PHP: Daten nach
volumes/convert/legen, bevor das Backend zum ersten Mal mit leeremvolumes/data/startet. Bericht danach involumes/convert/conversion-report.md.
Statt zweier Container kann das mit prod.cmd erzeugte Betriebspaket (dist/ im Repository-Stamm) in einem einzigen Node-Container laufen. Das Backend liefert dann Frontend, /custom.css und WebSocket selbst aus, mit der vollständigen CSP (Nonce, frame-src blob:). Davor genügt ein TLS-Proxy wie auf Reverse-Proxy und HTTPS beschrieben.
# Dockerfile.dist – BEISPIEL, nicht im Repository. Build-Kontext: dist/ aus prod.cmd
FROM node:26.9.0-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm install --omit=dev && npm cache clean --force
COPY app.cjs ./
COPY dist ./dist
COPY public ./public
COPY config ./config.default
RUN mkdir -p config data uploads
EXPOSE 55000
CMD ["node", "app.cjs"]Volumes dann: /app/config (mit dem Inhalt von config.default vorbefüllen), /app/data, /app/uploads, /app/convert. Die .env aus dem Paket nicht ins Image kopieren; Variablen per environment: übergeben.
Weiter mit: Konfiguration · Reverse-Proxy und HTTPS · Sicherheit und Betrieb
SmallTime 2027 · Version 1.0 · Zur Startseite · Fragen? Zuerst in den häufigen Fragen für Anwender oder Admins nachsehen.
👤 Anwender
🧑💼 Admins
- Erste Schritte
- Personen
- Arbeitsmodelle
- Ferien und Absenzen
- Saldo und Auszahlungen
- Monatsabschluss
- Gruppen und Rechte
- Stammdaten
- Einstellungen
- Terminals und Badges
- Protokoll und Sicherheit
- Häufige Fragen
🛠️ Einrichten
💻 Informatik (nur Fachleute)