DEV Community

Cover image for From 1 Idea to 49 Products: The Pipeline That Made It Possible on a VPS

From 1 Idea to 49 Products: The Pipeline That Made It Possible on a VPS

From 1 Idea to 49 Products: The Pipeline That Made It Possible on a VPS

The failure was specific. I had 40 finished vector packs sitting in a folder, and I pasted the same batch of rows into the Adobe Stock CSV uploader twice in one afternoon. Adobe's checker rejects a wrong header instantly, but it does not stop you from uploading the same asset twice if your spreadsheet says it is new. I spent an evening deleting duplicates and writing an apology-shaped email to support. That was the moment I stopped trusting memory and started writing scripts.

The title says 49 products. The register behind this says 39 finished products packaged by the exact scripts I am going to describe, 856 MB of output, roughly 19,000 lines of pipeline code in the parent project, and 300+ pages of research behind the rules those scripts enforce. All of it ran on 1 vCPU, 2 GB RAM, no GPU. Here is what actually mattered.

Metadata is where submissions die

Every stock channel has its own CSV format, and they are not compatible. Adobe wants a 5-column header. Vecteezy wants 4 columns. Dreamstime wants 15. Get one column wrong and the upload fails before a human ever sees your work.

My first attempt was a spreadsheet I maintained by hand. It drifted. Filenames did not match titles, keywords were inconsistent, and every batch had a few rows that failed silently. The fix was boring: one script, make_metadata.py, that reads a folder of files and writes the exact header each platform demands, nothing else. Adobe gets its 5 columns, Vecteezy its 4, Dreamstime its 15. If I pass a titles.json with real titles and keywords it uses those. If I do not, it title-cases the filename, which is fine for a first pass and not fine for submission. The script does not pretend otherwise.

The lesson: do not build a general metadata tool. Build three exact ones.

Double-posting is the fastest way to get flagged

That afternoon of duplicates taught me the second rule. Running a batch twice is not just wasted effort, on some platforms it looks like spam behavior and can get an account flagged. Humans re-run things. Cron jobs re-run things. A crashed job that you restart from the top will re-run things.

So the pipeline has a ledger. upload_tracker.py keeps a record of what has been posted. Three commands: pending shows what is not done yet, done marks an item, status gives the overview. A re-run consults the ledger first. It uses only the Python standard library, no install, no database file to corrupt in a way I cannot read. It is the least clever script I own and the one I would keep if I could only keep one.

A document is not a product until it looks like one

Half my catalogue is not stock art, it is documents: guides, reference sheets, prompt libraries. A raw Markdown file does not sell. I needed PDFs with real headings, tables, lists, and links, without a LaTeX install or a cloud rendering service on a 2 GB box.

md2pdf.py does it in one command with reportlab, one of only two third-party packages in the whole kit. The other is Pillow, for covers. Two dependencies total. On a small server, every dependency is a memory footprint and a future breakage, so I count them.

Packaging: a zip with a cover sells, a bare file does not

The last stall was presentation. pack_product.py takes a folder and writes three things: a zip of the files, a cover at 1280×720, and a thumbnail at 600×600. Output goes to a _system folder by default, and you can redirect it with --dir or the PACK_ROOT environment variable so nothing is hardcoded.

One idea became 49 products because each one is now a fifteen-minute packaging step instead of a weekend of fiddling in an image editor.

The RAM notes, because your server is small too

Everything above ran on 1 vCPU and 2 GB with no GPU. The habits that made it survive:

  • One process at a time. Never fan out image jobs in parallel on a single core.
  • Call gc.collect() after each asset to free memory between items.
  • Measure peak RSS, not average: resource.getrusage(RUSAGE_SELF).ru_maxrss gives the real ceiling.
  • Run heavy render steps one file per subprocess so memory returns to the OS.
  • Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 is the difference between fitting and swapping.

None of these are clever. All of them are the difference between a pipeline that finishes and one that gets OOM-killed at asset 30.

One more mistake worth naming

I deleted my research/ folder once, to "clean up." Those notes and scrape logs were the raw material for the next ten products. Keep them forever. The folder layout that scales is simple: assets/ for raw output, output/ for ready-to-submit files plus metadata, packs/ for finished zips and covers, research/ for notes you never delete, scripts/ for the tools.

Where to get the tools

I packaged the four scripts, the folder layout, and the RAM notes into the Pipeline Starter Kit. It is not a tutorial that ends at "here's how you'd do it," it is the same code that packaged 39 products on that tiny server. The metadata script and the tracker need nothing but Python 3.9+, so you can try those before installing anything at all.

See the kit


Building digital products? Free guides & kits for digital product sellers.

Top comments (1)

Collapse
 
officialmailkr profile image
오피셜메일 •

중복 업로드를 막기 위해 upload_tracker.py를 별도 원장으로 둔 부분이 가장 실용적이네요. 배치 재실행이 잦은 운영에서는 done을 업로드 버튼 클릭 시점이 아니라 플랫폼이 반환한 공개 자산 ID나 URL을 확인한 뒤 기록하면, 실패한 업로드를 완료로 잘못 표시하는 일까지 줄일 수 있을 것 같습니다. 플랫폼별 CSV 스키마를 분리한 설계와도 잘 맞습니다.