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/lp0on 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_KEYgenerated. 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/.envand 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:
- Visit
https://shopee-print.exzentcg.comin a browser. - Enter Shopee Partner ID and Partner Key in the dashboard's Settings page.
- Click "Connect to Shopee".
- Complete the OAuth flow on Shopee's seller centre.
- 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.yamlon CT 103 — add a card forshopee-print.exzentcg.comonce 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