Cipher · Barcode Studio
A self-contained, browser-based barcode generator that converts text or numeric sequences into 22 different barcode symbologies and exports them as downloadable JPG (or PNG) images. Designed to run as a single hardened Docker container with no backend, no database, and no telemetry — everything happens in the user's browser.
Table of Contents
- Features
- Supported Barcode Formats
- Architecture
- Requirements
- Quick Start
- Configuration
- Project Structure
- Security
- Dependency Versions & Vulnerability Status
- Operations
- Updating & Maintenance
- Troubleshooting
- Development
- License
Features
- 22 barcode symbologies spanning retail/GS1, linear/industrial, pharma, postal and 2D matrix codes.
- Live example preview — every symbology shows a sample render the moment it's selected.
- Batch generation — paste lines into the input area and each line becomes its own barcode card.
- Per-card text toggle — show or hide the human-readable caption beneath each barcode, then download it in that state.
- JPG and PNG export — individual downloads per barcode plus a one-click "Download All as JPG" for the whole batch.
- Adjustable scale and JPG quality for print-ready output.
- Fully offline-capable once loaded — every library is vendored locally, no third-party CDN at runtime (web fonts are the only external request and are optional).
- No backend — runs entirely as static assets served by nginx. No database, no API, no user data leaves the browser.
Supported Barcode Formats
| Group | Symbology | Notes |
|---|---|---|
| Retail · GS1 | EAN-13, EAN-8, UPC-A, UPC-E, ISBN-13, GS1 DataBar, GS1-128 | ISBN-10 auto-converts to ISBN-13 |
| Linear · Industrial | Code-128, Code-39, Code-39 Full ASCII, Code-93, Code-11, Code 2of5 Interleaved, MSI Plessey, Flattermarken, Telepen Alpha | |
| Pharma · Specialty | Pharmacode One-Track, Pharmacode Two-Track | Laetus binary codes |
| Postal | KIX | Dutch PostNL routing |
| 2D · Matrix | QR Code, Data Matrix, PDF417 |
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ ┌───────────────────────────────────────────────────┐ │
│ │ index.html (UI + state) │ │
│ │ ├── JsBarcode (linear codes) │ │
│ │ └── bwip-js (specialty + 2D codes) │ │
│ │ ─→ HTMLCanvasElement → JPG / PNG Blob │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────┘
│ HTTP (static assets only)
▼
┌─────────────────────────────────────────────────────────────┐
│ Docker container · nginx 1.30.1 · alpine 3.23 │
│ • runs as non-root (UID 101) │
│ • read-only root filesystem │
│ • all caps dropped, no-new-privileges │
│ • CSP / HSTS / nosniff / X-Frame-Options / COOP / CORP │
└─────────────────────────────────────────────────────────────┘
Encoding happens entirely in the browser — the container only ships static HTML/JS. The image cannot leak data because there is no application server to leak through.
Requirements
| Component | Minimum version | Why |
|---|---|---|
| Docker Engine | 24.0+ | Buildkit, compose-v2 features |
| Docker Compose | v2.20+ | Modern compose schema (deploy.resources, tmpfs options) |
| Host OS | Linux / macOS / Windows (WSL2) | Anything Docker supports |
| RAM | 64 MB free | Container reserves 32 MB, caps at 128 MB |
| Disk | ~50 MB | Image is ~40 MB |
| Open port | one host port (default 8080) | Reconfigurable via .env |
Verify your toolchain:
docker --version # → Docker version 24.x.x or newer
docker compose version # → Docker Compose version v2.20+ or newer
If you're on an older Docker that only has the standalone docker-compose binary, the file still works — just replace docker compose with docker-compose in the commands below.
Quick Start
# 1. Unzip and enter the project
unzip cipher-barcode-studio.zip
cd cipher-barcode-studio
# 2. (Optional) override the host port
cp .env.example .env # then edit HOST_PORT if needed
# 3. Build and start
docker compose up -d --build
# 4. Open in browser
# http://localhost:8080
That's it. To stop:
docker compose down
To stop and remove the built image as well:
docker compose down --rmi local
Configuration
All runtime configuration lives in .env (copied from .env.example):
| Variable | Default | Description |
|---|---|---|
HOST_PORT |
8080 |
Port published on the host machine |
The container itself always listens on 8080 internally — only the host-side port is configurable.
Putting it behind a reverse proxy
If you're running this behind Traefik, Caddy, nginx, or similar:
# example with Traefik labels
services:
cipher:
# ...existing config...
labels:
- "traefik.enable=true"
- "traefik.http.routers.cipher.rule=Host(`barcode.example.com`)"
- "traefik.http.routers.cipher.tls=true"
- "traefik.http.routers.cipher.tls.certresolver=letsencrypt"
- "traefik.http.services.cipher.loadbalancer.server.port=8080"
When fronted by HTTPS, the Strict-Transport-Security header in nginx.conf will kick in automatically.
Project Structure
cipher-barcode-studio/
├── docker-compose.yml # Orchestration + runtime hardening
├── Dockerfile # Image build steps
├── .dockerignore # Keep the build context lean
├── .env.example # Template for runtime config
├── LICENSE # MIT + third-party attributions
├── README.md # You are here
├── nginx/
│ └── nginx.conf # Hardened nginx config w/ CSP & security headers
└── web/
├── index.html # The full application (single HTML file)
└── vendor/
├── JsBarcode.all.min.js
└── bwip-js-min.js
Security
This image is built with a defense-in-depth posture even though it serves static content with no user data:
Container hardening
| Control | Mechanism |
|---|---|
| Non-root execution | Image's USER is nginx (UID 101). Compose pins user: "101:101". |
| Read-only rootfs | read_only: true in compose; writable paths exposed via tmpfs. |
| Capability drop | cap_drop: [ALL] — no Linux capabilities granted. |
| No privilege escalation | security_opt: no-new-privileges:true. |
| Resource limits | CPU capped at 0.5 cores, memory at 128 MB. |
| Minimal base | nginxinc/nginx-unprivileged:alpine3.23-slim — Alpine, no extra packages, security upgrades applied at build. |
| Restricted log size | json-file driver capped at 5 MB × 3 rotations. |
HTTP hardening (in nginx/nginx.conf)
| Header | Purpose |
|---|---|
Content-Security-Policy |
Restricts scripts/styles/fonts/images to self + Google Fonts only |
Strict-Transport-Security |
One-year HSTS with subdomain coverage (effective behind TLS) |
X-Frame-Options: DENY + CSP frame-ancestors 'none' |
Clickjacking protection |
X-Content-Type-Options: nosniff |
Disables MIME sniffing |
Referrer-Policy: strict-origin-when-cross-origin |
Limits referrer leakage |
Permissions-Policy |
Disables camera, mic, geolocation, USB, payment APIs |
Cross-Origin-Opener-Policy: same-origin |
Window isolation |
Cross-Origin-Resource-Policy: same-origin |
Resource isolation |
server_tokens off |
Hides nginx version |
Also: request size capped at 1 KB (this is a static GET-only service), hidden files (/\.) blocked, /healthz lightweight endpoint for orchestrators.
Supply chain
- JavaScript libraries are vendored under
web/vendor/, not loaded from CDN at runtime. - Each
<script>tag inindex.htmlcarries a Subresource Integrity (SRI) hash (sha384), so any tampering with the vendor files will cause the browser to refuse execution. - The only runtime third-party connection is to Google Fonts for typography. If you need full air-gap operation, see Removing Google Fonts.
Scanning the image yourself
docker compose build
docker scout cves cipher-barcode-studio:1.0.0 # native Docker scanner
# or
trivy image cipher-barcode-studio:1.0.0 # Trivy
# or
grype cipher-barcode-studio:1.0.0 # Grype
Expect findings to be limited to base-image OS packages; the application itself has no Node.js, npm, or other runtime dependencies to scan.
Dependency Versions & Vulnerability Status
All versions verified against published CVE feeds at build time.
| Component | Pinned version | Latest as of build | Known CVEs | Source of truth |
|---|---|---|---|---|
| nginx | 1.30.1 |
1.30.1 (stable) | none active | nginx security advisories |
| Alpine Linux | 3.23 |
3.23 | base image kept patched via apk upgrade in Dockerfile |
Alpine secdb |
| JsBarcode | 3.12.3 |
3.12.3 | 0 | Snyk advisor, Socket |
| bwip-js | 4.10.1 |
4.10.1 | 0 | Snyk advisor |
The build pulls nginxinc/nginx-unprivileged:1.30.1-alpine3.23-slim and then runs apk upgrade --no-cache so any Alpine package CVEs disclosed after the image was published are still patched.
If you fork this repo, re-run the vulnerability check periodically:
docker compose build --no-cache
trivy image --severity HIGH,CRITICAL cipher-barcode-studio:1.0.0
Operations
View logs
docker compose logs -f cipher
Healthcheck
docker inspect --format='{{json .State.Health}}' cipher-barcode-studio | jq
# or hit it directly:
curl -i http://localhost:8080/healthz
Restart
docker compose restart cipher
Inspect security posture of the running container
docker inspect cipher-barcode-studio | jq '.[0].HostConfig | {ReadonlyRootfs, CapDrop, SecurityOpt, Memory, NanoCpus}'
Expected output should include "ReadonlyRootfs": true, "CapDrop": ["ALL"], and "no-new-privileges:true".
Updating & Maintenance
Rebuild with the latest patches
docker compose build --no-cache --pull
docker compose up -d
--pull forces Docker to refetch the base image, ensuring you pick up new Alpine and nginx releases.
Update a vendored JS library
cd web/vendor
# Example: bump bwip-js
curl -sSL -o bwip-js-min.js "https://cdn.jsdelivr.net/npm/bwip-js@<new-version>/dist/bwip-js-min.js"
# Recompute the SRI hash
openssl dgst -sha384 -binary bwip-js-min.js | openssl base64 -A | xargs -I{} echo "sha384-{}"
# Paste the new hash into the integrity="" attribute in web/index.html
# Rebuild
docker compose up -d --build
Removing Google Fonts
If you need a fully air-gapped install, edit web/index.html:
- Delete the three
<link rel="preconnect">and<link rel="stylesheet">tags pointing atfonts.googleapis.com/fonts.gstatic.com. - The page still works — it just falls back to the next font in each
font-familystack (serif/monospace). - Tighten
nginx/nginx.confby removing thehttps://fonts.googleapis.comandhttps://fonts.gstatic.comentries from the CSP.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
bind: address already in use |
Port 8080 is taken | Set HOST_PORT=8081 (or any free port) in .env and docker compose up -d |
Container restart-loops with "Permission denied" on /var/cache/nginx |
Older Docker without tmpfs option support | Upgrade Docker Engine to 24+ |
| Browser shows blank page, console says "Refused to execute script ... integrity" | Vendor file modified locally without updating SRI | Recompute SRI hash (see Updating) |
| Fonts look generic | Network blocks Google Fonts | Either allow fonts.googleapis.com / fonts.gstatic.com or follow Removing Google Fonts |
| QR / Data Matrix barcode renders but caption is cut off | Output is a 2D code with long text | Hide the text via the per-card "Hide text" button, or shorten the input |
docker compose not found |
Only the legacy standalone docker-compose is installed |
Use docker-compose up -d --build instead; or install the v2 plugin |
Resetting from scratch
docker compose down --rmi local --volumes
docker compose up -d --build --force-recreate
Development
To iterate on the UI without rebuilding the image every time, mount web/ over the container's webroot:
# docker-compose.override.yml (gitignored convention)
services:
cipher:
volumes:
- ./web:/usr/share/nginx/html:ro
Then:
docker compose up -d
# edit web/index.html, refresh the browser — no rebuild needed
When you're done, delete the override file and rebuild for production.
License
This project is released under the MIT License — see LICENSE.
Third-party libraries retain their own licenses:
- JsBarcode — MIT — https://github.com/lindell/JsBarcode
- bwip-js — MIT — https://github.com/metafloor/bwip-js
- nginx — BSD-2-Clause — https://nginx.org/LICENSE
- Alpine Linux — MIT-equivalent — https://alpinelinux.org/
Built with care. Encode anything. Print everything.