Skip to content

Shopee Print (CT 108)

Status: Deployed ✅ Deployed: 2026-05-16 Purpose: Runs the shopee-auto-print service — polls Shopee for ready-to-ship orders, fetches waybill PDFs, and dispatches them to a Fujun 9250 thermal label printer via USB passthrough.


Overview

Property Value
CT ID 108
Hostname shopee-print
LAN IP 192.168.0.58
OS Debian 12
Node.js 22.22.2
Service port 8787 (localhost only — proxied via NPM)
Public endpoint https://shopee-print.exzentcg.com (pending — see Manual Steps below)
Service unit shopee-auto-print.service
Deploy path /opt/shopee-auto-print/
CPU cores 2
RAM 1024 MB
Disk 8 GB (local-lvm)
Unprivileged yes

The service listens on 127.0.0.1:8787 inside the CT. External access goes through Nginx Proxy Manager (CT 101) via the existing exzentcg-homelab Cloudflare Tunnel, following the same pattern as n8n.exzentcg.com.


Prerequisites

Before provisioning CT 108, the following must be in place:

  • USB printer on the PVE host. The Fujun 9250 (Winbond USB VID:PID 0416:5011) must be connected and visible at /dev/usb/lp0 on the Proxmox host.
  • udev rule on PVE host. The printer device needs MODE="0666" so the LXC passthrough can write to it without root.
  • Built deployment archive. The service is built on CT 107 (dev-shell) and tarred to CT 108. Build the dist before provisioning so the archive is ready to copy.
  • MASTER_KEY generated. A 64-hex-character key must be generated before first boot. This key encrypts all Shopee credentials at rest in the service's SQLite database. It lives in /opt/shopee-auto-print/.env and is backed up in 1Password.

USB Passthrough Setup

The Fujun 9250 appears on the PVE host as a character device at c 180:0 (/dev/usb/lp0). The passthrough uses LXC cgroup allowlisting and a bind-mount.

Step 1 — Add udev rule on PVE host

# On the PVE host (ssh root@pve)
cat > /etc/udev/rules.d/99-shopee-printer.rules <<'EOF'
SUBSYSTEM=="usb", ATTRS{idVendor}=="0416", ATTRS{idProduct}=="5011", MODE="0666"
EOF

udevadm control --reload-rules
udevadm trigger
ls -la /dev/usb/lp0   # expect: crw-rw-rw- ... 180, 0 /dev/usb/lp0

Step 2 — Add cgroup allowlist and bind-mount to CT 108 config

After creating CT 108 (see Provisioning below), stop it and append to /etc/pve/lxc/108.conf:

pct stop 108
cat >> /etc/pve/lxc/108.conf <<'EOF'
lxc.cgroup2.devices.allow: c 180:* rwm
lxc.mount.entry: /dev/usb/lp0 dev/usb/lp0 none bind,optional,create=file
EOF
pct start 108

The optional flag is critical

Using bind,optional,create=file (not just bind) means the CT starts cleanly even when the printer is powered off or disconnected. Without optional, a missing /dev/usb/lp0 on the host prevents the CT from starting at all.

After a printer power cycle

The udev rule re-triggers automatically on reconnect, but the bind-mount inside the CT is established at CT start time. If /dev/usb/lp0 disappears and reappears while the CT is running, the device inode inside the CT may become stale. See the Ops Runbook for the recovery procedure.


Provisioning Steps

Step 1 — Create LXC

From the Proxmox host:

pct create 108 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst \
  --hostname shopee-print \
  --cores 2 \
  --memory 1024 \
  --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.0.58/24,gw=192.168.0.1,firewall=1 \
  --nameserver 1.1.1.1 \
  --onboot 1 \
  --unprivileged 1 \
  --ssh-public-keys /root/.ssh/authorized_keys \
  --start 1

Step 2 — Install runtime dependencies

pct exec 108 -- bash -c '
  apt-get update -qq
  apt-get install -y -qq curl ca-certificates gnupg sudo vim htop
  # Node 22
  curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
  apt-get install -y -qq nodejs
  # poppler-utils: provides pdftoppm, used by the PDF→PNG pipeline
  apt-get install -y -qq poppler-utils
'

Verify:

pct exec 108 -- node --version    # expect: v22.x.x
pct exec 108 -- pdftoppm -v       # expect: Poppler version line

Step 3 — Add cgroup passthrough (USB)

Stop the CT, edit config, restart (see USB Passthrough Setup above):

pct stop 108
cat >> /etc/pve/lxc/108.conf <<'EOF'
lxc.cgroup2.devices.allow: c 180:* rwm
lxc.mount.entry: /dev/usb/lp0 dev/usb/lp0 none bind,optional,create=file
EOF
pct start 108

# Verify the device is visible inside the CT
pct exec 108 -- ls -la /dev/usb/lp0

Step 4 — Deploy the service archive

Build the archive on dev-shell (CT 107), then copy to CT 108. The following assumes the source repo is checked out at ~/Documents/shopee-auto-print/ on dev-shell:

# On dev-shell (CT 107)
cd ~/Documents/shopee-auto-print
yarn build            # compiles src/ → dist/
cd web && yarn build  # compiles web/ → web/dist/
cd ..

# Package everything needed for production
tar czf /tmp/shopee-auto-print.tar.gz \
  dist/ \
  web/dist/ \
  node_modules/ \
  package.json

# Copy to CT 108 via the PVE host
scp /tmp/shopee-auto-print.tar.gz root@pve:/tmp/
# On PVE host
pct exec 108 -- mkdir -p /opt/shopee-auto-print
pct push 108 /tmp/shopee-auto-print.tar.gz /tmp/shopee-auto-print.tar.gz
pct exec 108 -- bash -c '
  tar xzf /tmp/shopee-auto-print.tar.gz -C /opt/shopee-auto-print
  rm /tmp/shopee-auto-print.tar.gz
'

Step 5 — Create the .env file

Generate MASTER_KEY (64 hex chars) and write it into the env file. Store it in 1Password immediately — if it is lost, all encrypted credentials in the SQLite DB are unrecoverable and Shopee OAuth must be re-done from scratch.

# Generate key
openssl rand -hex 32   # outputs 64 hex chars

# Write .env into CT 108
pct exec 108 -- bash -c 'cat > /opt/shopee-auto-print/.env <<EOF
NODE_ENV=production
PORT=8787
HOST=127.0.0.1
MASTER_KEY=<paste-64-hex-key-here>
EOF
chmod 600 /opt/shopee-auto-print/.env'

MASTER_KEY is not recoverable

The encrypted partner_key, access_token, and refresh_token in the SQLite DB can only be decrypted with this key. If the key is lost, delete the settings row and re-enter Shopee credentials from scratch. Always back up this key in 1Password under "shopee-auto-print MASTER_KEY (prod)".

Step 6 — Install systemd service

pct exec 108 -- bash -c 'cat > /etc/systemd/system/shopee-auto-print.service <<EOF
[Unit]
Description=Shopee Auto-Print
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/shopee-auto-print
ExecStart=/usr/bin/node dist/main.js
Restart=on-failure
RestartSec=5
EnvironmentFile=/opt/shopee-auto-print/.env
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable shopee-auto-print
systemctl start shopee-auto-print
systemctl status shopee-auto-print'

Step 7 — Configure Proxmox firewall rules

CT 108 uses firewall=1, so it needs explicit allow/deny rules. Create /etc/pve/firewall/108.fw on the PVE host:

cat > /etc/pve/firewall/108.fw <<'EOF'
[OPTIONS]
enable: 1

[RULES]
IN ACCEPT -source +edge_gw -p tcp -dport 8787 # Only proxy may reach shopee-auto-print
IN ACCEPT -source +admin_desktop -p tcp -dport 22 # SSH from admin desktop
IN DROP # Block all other inbound
OUT ACCEPT -dest +router_gw -p udp -dport 53 # DNS resolution UDP
OUT ACCEPT -dest +router_gw -p tcp -dport 53 # DNS resolution TCP
OUT DROP -dest +lan_subnet # Lateral movement prevention — MUST be before ALLOW 443
OUT ACCEPT -p tcp -dport 443 # Shopee API calls (HTTPS only)
EOF

Also add the outbound rule to CT 101's firewall so NPM can reach CT 108. Edit /etc/pve/firewall/101.fw and insert before the OUT DROP -dest +lan_subnet line:

OUT ACCEPT -dest 192.168.0.58 -p tcp -dport 8787   # Proxy to shopee-print

Step 8 — Verify the service is up

pct exec 108 -- systemctl status shopee-auto-print
# expect: active (running)

# Verify NPM can reach CT 108 (after firewall rules applied)
pct exec 101 -- curl -s -o /dev/null -w "%{http_code}" http://192.168.0.58:8787/api/health
# expect: 200

Manual Steps (Require Credentials)

These five steps cannot be automated — they require logging into web dashboards with credentials the deploy script doesn't have.

Do these in order — each depends on the previous

1. NPM proxy host

Log into NPM admin at http://192.168.0.51:81 (admin email: exzensg@gmail.com, password in 1Password → "NPM Admin").

Add a new Proxy Host:

Field Value
Domain Names shopee-print.exzentcg.com
Scheme http
Forward Hostname / IP 192.168.0.58
Forward Port 8787
Block Common Exploits enabled
SSL Request Let's Encrypt cert (or assign existing wildcard for *.exzentcg.com)

2. Cloudflare DNS

In the Cloudflare dashboard for exzentcg.com → DNS → Add record:

Field Value
Type CNAME
Name shopee-print
Target The tunnel CNAME target (same value as n8n.exzentcg.com's CNAME — check its record to copy)
Proxy status Proxied

The existing exzentcg-homelab tunnel routes all inbound traffic to NPM at localhost:80 on CT 101, so no new tunnel rule is needed.

3. Cloudflare Access policy

In Cloudflare Zero Trust → Access → Applications → Add an application:

Field Value
Application type Self-hosted
Application name Shopee Auto-Print
Application domain shopee-print.exzentcg.com
Policy Allow exzensg@gmail.com (same policy as n8n.exzentcg.com)

4. Shopee OAuth redirect URI

In Shopee Open Platform → App settings for the registered app, add:

https://shopee-print.exzentcg.com/shopee/callback

to the redirect URI allowlist. The temporary trycloudflare.com URL used during development should be removed at this point.

5. Re-authorize Shopee

The MASTER_KEY on CT 108 differs from the one used on dev-shell during development, so the encrypted tokens in SQLite (if any were copied) cannot be decrypted. Re-authorize from scratch:

  1. Visit https://shopee-print.exzentcg.com in a browser.
  2. Enter Shopee Partner ID and Partner Key in the dashboard's Settings page.
  3. Click "Connect to Shopee".
  4. Complete the OAuth flow on Shopee's seller centre.
  5. Verify the Jobs page shows the connected shop name and a non-empty access token expiry date.

Upgrade Procedure

When a new version of shopee-auto-print is ready to deploy:

# 1. Build on dev-shell
cd ~/Documents/shopee-auto-print
git pull
yarn build
cd web && yarn build && cd ..

# 2. Package (exclude .env, data.db — those stay on the CT)
tar czf /tmp/shopee-auto-print.tar.gz \
  dist/ \
  web/dist/ \
  node_modules/ \
  package.json

# 3. Copy to PVE host, then push to CT 108
scp /tmp/shopee-auto-print.tar.gz root@pve:/tmp/
ssh root@pve 'pct push 108 /tmp/shopee-auto-print.tar.gz /tmp/shopee-auto-print.tar.gz'

# 4. Stop service, extract, restart
ssh root@pve 'pct exec 108 -- bash -c "
  systemctl stop shopee-auto-print
  tar xzf /tmp/shopee-auto-print.tar.gz -C /opt/shopee-auto-print
  rm /tmp/shopee-auto-print.tar.gz
  systemctl start shopee-auto-print
  systemctl status shopee-auto-print
"'

SQLite DB is preserved

The archive does not include data.db or .env, so credentials and the print queue survive upgrades. The DB file lives at /opt/shopee-auto-print/data.db and is never touched by the upgrade procedure.


Systemd Service Management

# All commands run from the PVE host via pct exec, or directly inside CT 108

# Status
pct exec 108 -- systemctl status shopee-auto-print

# Start / stop / restart
pct exec 108 -- systemctl start shopee-auto-print
pct exec 108 -- systemctl stop shopee-auto-print
pct exec 108 -- systemctl restart shopee-auto-print

# Live logs (follow mode)
pct exec 108 -- journalctl -u shopee-auto-print -f

# Last 100 lines
pct exec 108 -- journalctl -u shopee-auto-print -n 100

# Logs since last boot
pct exec 108 -- journalctl -u shopee-auto-print -b

Updates Needed Elsewhere (Post-Deploy)

  • [ ] Add CT 108 entry to homelab/00_secrets/Credentials Index.md (MASTER_KEY 1Password reference, NPM admin credentials reference)
  • [ ] Update homelab/01_planning/Phase 1/Architecture Diagram.md — add CT 108 to the IP map
  • [ ] Update Homepage services.yaml on CT 103 — add a card for shopee-print.exzentcg.com once the public endpoint is live
  • [ ] Mark M6 (Deploy) as Done in the app repo at ExzenTCG/shopee-auto-print once all 5 manual steps are complete