Zum Inhalt

Serverinstallation

Diese Anleitung richtet den Druckserver (tavletta serve) auf einem Linux-Server für eine Firma ein: Andere Programme, zum Beispiel ein Inventarprogramm, drucken darüber Etiketten im Netzwerk. Was der Server tut und wie Programme ihn verwenden: Druckserver und API-Referenz.

Überblick

  • Ein kleiner Dienst auf Port 8790: eine REST-API (und MCP für KI-Assistenten). Er zeichnet die Etiketten selbst und schickt sie über das Netzwerk an die Drucker; kein Desktop, kein Browser, keine Datenbank.
  • Die Drucker, die er verwenden darf, stehen in printers.toml; auf keiner anderen Adresse kann gedruckt werden.
  • Jedes Programm und jedes Gerät bekommt ein eigenes Zugangstoken mit einer Rolle (tokens.toml); die Datei speichert von jedem Token nur einen Fingerabdruck (Hash).
  • Die Etiketten sind .tavletta-Dateien in einem Etikettenordner, gestaltet in der Tavletta-App und dorthin kopiert.
  • Er lauscht nur auf 127.0.0.1. Andere Computer erreichen ihn über einen Reverse Proxy mit HTTPS (Traefik, nginx, Caddy).

Voraussetzungen

  • Ein Linux-Server auf x86-64 oder ARM64 (eine virtuelle Maschine oder ein Raspberry Pi 4/5 genügt).
  • Entweder Docker Engine mit dem Compose-Plugin (Variante A, empfohlen) oder Python 3.11 oder neuer mit PyGObject, Pango und Cairo aus der Distribution (Variante B).
  • Ressourcen: 1 CPU-Kern, 1 GB RAM reserviert (der Server braucht meist 50–150 MB; ein grosses Etikett ist auf 40 Megapixel begrenzt), 500 MB Speicherplatz für das Image oder die Installation, dazu die Etiketten.
  • Netzwerk vom Server zu den Druckern:
Drucker Port
cab, Brother QL, ZPL, EPL2, TSPL, ESC/POS, DYMO und andere, die "raw" drucken TCP 9100
Bürodrucker und CUPS-Warteschlangen (IPP) TCP 631
Brother VC-500W TCP 9100
  • Nur um Drucker automatisch zu finden (mDNS) oder für USB-Drucker: das Netzwerk des Hosts (Variante A mit compose.host.yaml) oder eine native Installation.
  • Schriften: Jede Schrift, die die Etiketten verwenden, muss auf dem Server installiert sein (siehe Schriften).

Variante A: Docker Compose

1. Dateien und Image holen

sudo git clone https://git.joshuahirsig.xyz/joshii/tavletta.git /opt/tavletta
cd /opt/tavletta
sudo git checkout v1.1.0      # the release to run: git tag lists them
cd packaging/docker
sudo docker compose pull      # ghcr.io/joshii-h/tavletta:1.1, for x86-64 and ARM64

Das Image (etwa 230 MB), gebaut und getestet von der CI des Projekts und signiert (siehe Server und Container), enthält Tavletta, Python, Pango und Cairo, die Schriften DejaVu, Source Sans 3, Liberation (Masse von Arial, Times New Roman und Courier New) und Carlito (Masse von Calibri). Es läuft als Benutzer ohne Rechte, schreibgeschützt, ohne Capabilities.

2. Ordner

Alles, was zu Ihrer Installation gehört, liegt in drei Ordnern neben compose.yaml (git ignoriert sie):

/opt/tavletta/packaging/docker/
├── compose.yaml
├── config/          printers.toml, tokens.toml
├── labels/          the label files (.tavletta) and their data files
└── fonts/           fonts of your own (.ttf, .otf), may stay empty
sudo mkdir -p config labels fonts
sudo chown -R 1000:1000 config labels fonts

Der Container läuft als Benutzer 1000 (TAVLETTA_UID und TAVLETTA_GID für einen anderen setzen): Er liest config/ und speichert Zähler in die Etikettendateien in labels/.

3. Drucker: printers.toml

config/printers.toml nennt jeden Drucker, den Programme verwenden dürfen. Ein Beispiel mit einem cab-Drucker für Inventaretiketten (Thermotransfer, um 180 Grad gedreht gedruckt), einem Brother QL am Empfang und einem Bürodrucker über IPP:

[printers.it-cab]
backend = "cab"                          # cab EOS, SQUIX, MACH
host = "cab-it.example.com"              # name or IP address of the printer
description = "Asset labels"
location = "IT store room"
media = "57 x 32 mm polyester, resin ribbon"
default = true                           # used when a request names no printer
max_copies = 50

[printers.it-cab.settings]
method = "transfer"                      # thermal transfer with ribbon (direct: without)
upside_down = true                       # printed turned by 180 degrees

[printers.reception-ql]
backend = "brother_ql"                   # Brother QL-700, QL-8xx, QL-1100 …
host = "192.0.2.21"
description = "Visitor and sample labels"
location = "Reception"
media = "DK-22205, 62 mm continuous"
max_copies = 20
cut = "full"

[printers.reception-ql.settings]
quality = "high"

[printers.office]
backend = "system"                       # any IPP printer or CUPS queue
host = "ipp://office-printer.example.com/ipp/print"
description = "Label sheets, A4"
max_copies = 10
  • backend ist das Druckermodell: cab, brother_ql, brother_ql_diecut, ptouch, vc500w, zpl, epl, tspl, escpos, dymo, niimbot, system (tavletta list-printers zeigt sie).
  • host ist der Name oder die IP-Adresse des Druckers; für system eine IPP-Adresse. Geben Sie Druckern eine feste Adresse (DHCP-Reservierung oder DNS-Name).
  • Die Einstellungen eines Modells sind die der Druckereinstellungen der App; die vollständige Liste steht in docs/server.md.
  • Ein Fehler hält den Server mit der Datei, dem Schlüssel und dem Problem an, zum Beispiel printers.toml: printers.it-cab.settings.method: expected one of: printer, transfer, direct. Änderungen brauchen einen Neustart.

4. Zugangstoken: tokens.toml

Erstellen Sie ein Token pro Programm oder Gerät. Das Token wird einmal gezeigt; die Datei speichert nur seinen Hash.

sudo docker run --rm --user 1000:1000 -v ./config:/config --entrypoint tavletta \
    ghcr.io/joshii-h/tavletta:1.1 token new inventory-tool --role print --printer it-cab \
    --description "Inventory tool" \
    --tokens /config/tokens.toml --printers /config/printers.toml
tav_3f9c0a1b2c4d_Xq2…
tavletta: The token is shown only this once; /config/tokens.toml keeps only its hash.
  • Geben Sie das Token dem Administrator des Programms über einen sicheren Weg (einen Passwortmanager), nie per E-Mail.
  • Rollen: preview (nur ansehen), print (auch drucken, die eigenen Aufträge sehen), admin (jeden Auftrag sehen und abbrechen). Programme bekommen print, die Überwachung preview.
  • --printer (mehrfach möglich) beschränkt das Token auf diese Drucker, --expires 2027-12-31 auf ein Datum.
  • Geben Sie immer --printers an: Sobald ein Token auf Drucker beschränkt ist, lässt sich die Datei nur mit der Druckerliste prüfen.
  • Neue Tokens gelten sofort, ohne Neustart. Um eines zu widerrufen, löschen Sie seinen Eintrag in config/tokens.toml.

5. Etiketten

Gestalten Sie die Etiketten in der Tavletta-App und kopieren Sie die .tavletta-Dateien nach labels/ (Unterordner sind erlaubt). Programme nennen sie ohne Endung: labels/inventory/asset-tag.tavletta ist "label": "inventory/asset-tag".

sudo cp asset-tag.tavletta labels/
sudo chown 1000:1000 labels/asset-tag.tavletta

6. Starten

sudo docker compose up -d
sudo docker compose ps
curl http://127.0.0.1:8790/api/v1/health
{"status": "ok", "version": "1.1.0"}
  • Der Container startet nach einem Absturz oder einem Neustart des Servers von selbst wieder.
  • Die Zeitzone von {=date} und {=time} auf Etiketten stellen Sie mit TAVLETTA_TZ=Europe/Zurich in einer Datei .env neben compose.yaml ein (standardmässig UTC).
  • Token und Drucker prüfen:
TOKEN=tav_3f9c0a1b2c4d_Xq2…
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8790/api/v1/printers

Jeder Drucker wird mit seinem Zustand aufgeführt; UNREACHABLE mit einer Meldung, wenn der Server ihn nicht erreicht.

Host-Netzwerk: mDNS und USB

Um Drucker im Netzwerk per mDNS zu finden, auf CUPS-Warteschlangen des Hosts oder auf USB-Druckern zu drucken, starten Sie mit der Ergänzung compose.host.yaml:

sudo docker compose -f compose.yaml -f compose.host.yaml up -d

Der Container teilt dann das Netzwerk des Hosts und lauscht auf 127.0.0.1:8790 des Hosts. CUPS-Warteschlangen des Hosts sind ipp://localhost:631/printers/<queue> in printers.toml.

Protokoll

sudo docker compose logs -f

Jede Anfrage wird mit der Adresse des Clients, dem Namen des Tokens, dem Pfad und dem Status protokolliert; jeder Auftrag mit Etikett, Drucker und Ergebnis. Tokens und Etiketteninhalte werden nie protokolliert.

Aktualisieren

cd /opt/tavletta
sudo git fetch --tags
sudo git checkout v1.2.0         # the new release
cd packaging/docker
sudo docker compose pull
sudo docker compose up -d

Lesen Sie zuerst die Versionshinweise. Wartende Aufträge werden abgebrochen, wenn der Server anhält; ein Etikett, das gerade gedruckt wird, wird fertig gedruckt. config/, labels/ und fonts/ bleiben erhalten.

Sicherung

Sichern Sie die drei Ordner: config/ (die Druckerliste, die Token-Hashes), labels/ (die Etiketten und ihre Zähler: Die nächste Nummer jedes Etiketts mit {=counter} steht in seiner Datei) und fonts/. Das Image lässt sich jederzeit neu herunterladen.

Deinstallieren

cd /opt/tavletta/packaging/docker
sudo docker compose down
sudo docker image rm ghcr.io/joshii-h/tavletta:1.1
sudo rm -r /opt/tavletta     # also deletes config, labels and fonts

Variante B: nativ mit systemd

Für einen Server ohne Docker. Getestet auf Debian 13, Ubuntu 24.04 und Fedora 44. Die Installation braucht Internetzugang (Python-Pakete von PyPI, eines von GitHub).

1. Systempakete

Debian, Ubuntu:

sudo apt install python3 python3-venv python3-gi python3-gi-cairo gir1.2-pango-1.0 \
    gettext git fontconfig fonts-dejavu-core

Fedora:

sudo dnf install python3 python3-gobject gobject-introspection pango gettext git-core

GTK ist nicht nötig. gettext und git braucht es nur während der Installation.

2. Tavletta installieren

In eine eigene Python-Umgebung in /opt/tavletta, die das PyGObject des Systems verwendet:

sudo python3 -m venv --system-site-packages /opt/tavletta
sudo /opt/tavletta/bin/pip install "git+https://git.joshuahirsig.xyz/joshii/tavletta.git@v1.1.0"
/opt/tavletta/bin/tavletta --version

3. Dienstbenutzer und Ordner

sudo useradd --system --home-dir /var/lib/tavletta --create-home \
    --shell /usr/sbin/nologin tavletta
sudo install -d -o tavletta -g tavletta -m 0750 /etc/tavletta /var/lib/tavletta/labels
Pfad Inhalt Besitzer, Rechte
/etc/tavletta/printers.toml die Drucker (wie oben) tavletta, 600
/etc/tavletta/tokens.toml die Token-Hashes tavletta, 600
/var/lib/tavletta/labels/ die Etikettendateien tavletta, 750
/usr/local/share/fonts/ eigene Schriften root, für alle lesbar

Erstellen Sie die Tokens als Dienstbenutzer, damit die Datei ihm gehört:

sudo -u tavletta /opt/tavletta/bin/tavletta token new inventory-tool --role print \
    --printer it-cab --tokens /etc/tavletta/tokens.toml --printers /etc/tavletta/printers.toml

Der Server weist eine tokens.toml ab, die jeder Benutzer ändern darf, und warnt bei einer, die andere lesen dürfen.

4. Der Dienst

packaging/systemd/tavletta-server.service führt den Server als tavletta aus, abgeschottet (System schreibgeschützt, keine Home-Ordner, keine Capabilities, 1 GB Speicher):

sudo curl -fsSLo /etc/systemd/system/tavletta-server.service \
    https://git.joshuahirsig.xyz/joshii/tavletta/raw/tag/v1.1.0/packaging/systemd/tavletta-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now tavletta-server
systemctl status tavletta-server
curl http://127.0.0.1:8790/api/v1/health
  • sudo systemctl reload tavletta-server liest tokens.toml neu ein (SIGHUP); die Datei wird auch bei jeder Änderung neu gelesen.
  • sudo systemctl restart tavletta-server nach Änderungen an printers.toml oder nach dem Installieren von Schriften.
  • Protokoll: journalctl -u tavletta-server -f.
  • Die Zeitzone ist die des Servers (timedatectl).

Für einen einzelnen Benutzer auf einem Desktop gibt es stattdessen den Benutzerdienst packaging/systemd/tavletta.service.

Aktualisieren

sudo /opt/tavletta/bin/pip install --upgrade "git+https://git.joshuahirsig.xyz/joshii/tavletta.git@v1.2.0"
sudo curl -fsSLo /etc/systemd/system/tavletta-server.service \
    https://git.joshuahirsig.xyz/joshii/tavletta/raw/tag/v1.2.0/packaging/systemd/tavletta-server.service
sudo systemctl daemon-reload
sudo systemctl restart tavletta-server

Sichern Sie /etc/tavletta, /var/lib/tavletta/labels und Ihre Schriften.

Reverse Proxy und HTTPS

Der Server spricht einfaches HTTP auf 127.0.0.1. Andere Computer erreichen ihn über einen Reverse Proxy, der HTTPS hinzufügt. Geben Sie dem Server die Adresse des Proxys mit --trusted-proxy, damit das Protokoll und die Grenzen pro Client den echten Client sehen, nicht den Proxy.

Traefik (Docker)

Traefik und Tavletta in einem Docker-Netzwerk, Traefik mit einer festen Adresse. Entfernen Sie in compose.yaml den Eintrag ports: (der Server ist dann nur über Traefik erreichbar) und ergänzen Sie:

services:
  tavletta:
    # ... as in compose.yaml, without ports:
    networks: [proxy]
    command:
      - "--mcp"
      - "--printers"
      - "/config/printers.toml"
      - "--tokens"
      - "/config/tokens.toml"
      - "--trusted-proxy"
      - "172.30.0.10/32"          # Traefik's address in the network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.tavletta.rule=Host(`labels.example.com`)"
      - "traefik.http.routers.tavletta.entrypoints=websecure"
      - "traefik.http.routers.tavletta.tls.certresolver=letsencrypt"
      - "traefik.http.services.tavletta.loadbalancer.server.port=8790"

networks:
  proxy:
    external: true                # Traefik's network, Traefik at 172.30.0.10

nginx

Der Server auf 127.0.0.1 beantwortet nur Anfragen für einen lokalen Hostnamen (gegen DNS-Rebinding): nginx darf den öffentlichen Namen nicht weitergeben. Seine Voreinstellung (127.0.0.1:8790) ist richtig; fügen Sie kein proxy_set_header Host $host hinzu.

server {
    listen 443 ssl;
    server_name labels.example.com;
    ssl_certificate     /etc/ssl/certs/labels.example.com.pem;
    ssl_certificate_key /etc/ssl/private/labels.example.com.key;

    client_max_body_size 4m;           # as the server's --max-body
    location / {
        proxy_pass http://127.0.0.1:8790;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 330s;       # print requests may wait up to 300 s
    }
}

Starten Sie den Server mit --trusted-proxy 127.0.0.1/32 (an ExecStart im Dienst oder an command: in compose.yaml anhängen).

Caddy

Caddy gibt standardmässig den öffentlichen Hostnamen weiter; setzen Sie ihn auf den des Servers:

labels.example.com {
    reverse_proxy 127.0.0.1:8790 {
        header_up Host {upstream_hostport}
    }
}

Caddy holt sich selbst ein Zertifikat. Starten Sie den Server mit --trusted-proxy 127.0.0.1/32.

Firewall

  • Öffnen Sie nur den Port 443 des Proxys, und nur für die Netzwerke, die ihn brauchen (den Server des Inventarprogramms, das VLAN der Clients).
  • Port 8790 bleibt geschlossen: Der Server lauscht auf 127.0.0.1 (Docker veröffentlicht ihn nur auf 127.0.0.1).
  • Ausgehend vom Server zu den Druckern: TCP 9100 und 631.
  • Machen Sie den Server nie aus dem Internet erreichbar.

Schriften

Ein Etikett wird mit den Schriften des Computers gezeichnet, der es druckt. Ein Text in einer Schrift, die der Server nicht hat, wird in einer anderen gezeichnet; die Vorschau und der Druckauftrag warnen dann mit font_missing (zum Beispiel Element 2: the font Source Sans 3 is not installed – another font is used).

  • Docker: Das Image hat DejaVu, Source Sans 3, Liberation und Carlito. Kopieren Sie andere Schriften (.ttf, .otf, z. B. die Hausschrift) nach fonts/ und starten Sie neu: sudo docker compose restart. Weitere Schriftpakete von Debian: das Image selbst bauen, sudo docker compose -f compose.yaml -f compose.build.yaml build --build-arg FONTS="fonts-dejavu-core fonts-adobe-sourcesans3 fonts-liberation fonts-crosextra-carlito fonts-noto-core", danach up -d mit denselben zwei Dateien.
  • Nativ: Installieren Sie die Schriftpakete der Distribution (Debian, Ubuntu: fonts-adobe-sourcesans3; Fedora: adobe-source-sans-pro-fonts) oder kopieren Sie die Schriftdateien nach /usr/local/share/fonts/, dann sudo fc-cache -f und den Dienst neu starten.
  • Prüfen, was der Server hat: fc-list : family (im Container: sudo docker compose exec tavletta fc-list : family).
  • Schriften brauchen für die Verwendung auf einem Server unter Umständen eine Lizenz; Source Sans 3, Liberation, Carlito, DejaVu und Noto sind frei.

Sicherheits-Checkliste

  • Ein Token pro Programm oder Gerät, mit der kleinsten Rolle (print für Programme, preview für die Überwachung) und beschränkt auf seine Drucker (--printer); ein Ablaufdatum für befristete.
  • Tokens in einem Passwortmanager aufbewahrt; tokens.toml mit Rechten 600.
  • printers.toml in Gebrauch: Programme können nur auf diesen Druckern drucken.
  • max_copies pro Drucker auf ein sinnvolles Mass gesetzt.
  • Der Server nur über den HTTPS-Proxy erreichbar, nur aus den Netzwerken, die ihn brauchen; nie aus dem Internet.
  • --trusted-proxy auf die eigene Adresse des Proxys gesetzt, nichts Weiteres.
  • Aktualisierungen: die Versionen verfolgen und innert Wochen aktualisieren; das Image jeder Version bringt die Sicherheitsaktualisierungen von Debian ihres Tages mit (docker compose pull).
  • Sicherung von config/ (oder /etc/tavletta) und der Etiketten.

Die Einzelheiten jedes Schutzes (Grenzen, Sperre bei falschen Tokens, Dateizugriff, Webseiten): docs/server.md.

Fehlerbehebung

Symptom Ursache und Abhilfe
Ein Drucker ist UNREACHABLE (printer_timeout, printer_not_found) Der Server erreicht ihn nicht: vom Server aus mit nc -vz <printer> 9100 testen (631 für IPP); Adresse und Firewall prüfen, und ob der Drucker eingeschaltet ist. In Docker muss der Container das Netzwerk des Druckers erreichen.
401 unauthorized Token fehlt, ist falsch geschrieben, abgelaufen oder aus tokens.toml entfernt. Der Header lautet Authorization: Bearer <token>.
429 too_many_requests Zehn falsche Tokens von dieser Adresse innerhalb einer Minute: das Token korrigieren, eine Minute warten.
429 queue_full 100 Aufträge (20 pro Token) warten: Der Drucker ist langsam oder steht; ihn prüfen, später wiederholen.
503 busy Alle Anfrage- oder Zeichenplätze sind gerade belegt: nach ein, zwei Sekunden wiederholen.
404 printer_not_found Nicht in printers.toml, oder das Token darf ihn nicht verwenden (--printer).
413 label_too_large Das Etikett hat zu viele Elemente, zu lange Texte oder wird grösser als 40 Megapixel: es vereinfachen oder weniger Datenzeilen pro Anfrage schicken.
421 wrong_host Ein Reverse Proxy gibt den öffentlichen Hostnamen an einen Server auf 127.0.0.1 weiter: siehe nginx und Caddy.
font_missing in den Warnungen, der Text sieht falsch aus Die Schrift ist auf dem Server nicht installiert: siehe Schriften.
409 counter_not_saved Die Etikettendatei ist für den Benutzer des Servers nicht beschreibbar: mit chown diesem Benutzer geben (Docker: 1000, nativ: tavletta).
Datum oder Uhrzeit auf Etiketten ein, zwei Stunden daneben Der Container läuft auf UTC: TAVLETTA_TZ=Europe/Zurich in .env setzen, dann docker compose up -d.
Der Server startet nicht docker compose logs oder journalctl -u tavletta-server: Ein Fehler in printers.toml oder tokens.toml wird mit seinem Schlüssel genannt.
Zugriff auf die Ordner verweigert (Fedora, RHEL mit SELinux und Podman) :Z an die Volumes in compose.yaml anhängen.