Turning Your Internal Scripts Into a Paid Self-Hosted Tool for Teams
You have a Python script that saves your team three hours a week. It lives in a shared folder, runs on someone's laptop, and breaks every time that person goes on vacation. Sound familiar? The gap between "script that works" and "product people pay for" is smaller than most developers assume β and you don't need a startup or a co-founder to cross it.
This article walks you through the concrete steps of turning that internal automation into a self-hosted tool with a paywall, proper packaging, and a distribution story.
What you'll learn
- How to audit your existing script for product potential
- Which architecture patterns fit small self-hosted tools best
- How to add authentication and a simple license check
- How to package and distribute a self-hosted app people can actually install
- How to price and position a self-hosted tool for teams
Prerequisites
You should be comfortable with Python or JavaScript (examples use Python/FastAPI, but the concepts port cleanly). You'll need basic familiarity with Docker and a willingness to set up a simple payment webhook. No cloud infrastructure experience required beyond a server or VPS.
Start With the Audit
Before you write a single line of new code, ask yourself one honest question: does this script solve a problem someone outside your team would pay to fix? Not "might find useful" β actually pay for.
A good candidate script has three traits. It runs on a schedule or on demand (not just once during setup). It touches data or a workflow that repeats weekly or daily. And when it breaks, someone notices within hours, not weeks.
If your script meets those criteria, you have a product seed. If it's a one-off migration tool, shelf it and look for the next candidate.
Pick the Right Architecture
Self-hosted tools live or die by how easy they are to run. The person installing your tool is a developer, but they're a busy one. Aim for a setup that works with a single docker compose up.
The minimal viable stack
For most internal tools, a three-container setup covers everything:
- App container β your FastAPI (or Flask, Express, etc.) backend serving both an API and a minimal frontend
- Database container β PostgreSQL or SQLite depending on scale; SQLite is fine for teams under 50
- Worker container (optional) β a Celery or RQ worker if your script runs async jobs
Resist the urge to add Redis, a message queue, and a reverse proxy on day one. You can always add complexity later. You cannot easily remove it once customers depend on it.
File layout that scales
my-tool/
βββ app/
β βββ main.py # FastAPI entrypoint
β βββ routes/
β βββ models/
β βββ services/ # your original script logic goes here
βββ frontend/ # optional: plain HTML or a small React app
βββ docker-compose.yml
βββ .env.example
βββ README.mdThe services/ directory is where your original script lives, mostly unchanged. The rest of the app is scaffolding that lets other people run and interact with it.
Wrap Your Script in a FastAPI Backend
Most internal scripts are a collection of functions. Wrapping them in an HTTP API is straightforward. Here's a minimal example of taking a script function and exposing it as an endpoint:
# app/services/report_runner.py
# This is your original script logic, barely touched
def generate_report(start_date: str, end_date: str) -> dict:
# ... your existing logic ...
return {"rows": 42, "status": "ok"}# app/routes/reports.py
from fastapi import APIRouter, Depends
from app.services.report_runner import generate_report
from app.auth import require_valid_license
router = APIRouter()
@router.post("/run")
def run_report(
start_date: str,
end_date: str,
_: None = Depends(require_valid_license)
):
result = generate_report(start_date, end_date)
return resultNotice the require_valid_license dependency. That's the paywall. Every route that does real work gets that dependency injected.
Add Authentication and License Checking
This is the part most developers skip, and it's exactly what separates a script from a paid product. You need two layers: user auth (who is running this) and license validation (have they paid).
User authentication
For a self-hosted tool, keep auth simple. JWT tokens stored in a cookie or Authorization header work well. Libraries like python-jose and passlib handle the heavy lifting. Don't roll your own crypto.
# app/auth.py
from fastapi import HTTPException, Header
from jose import jwt, JWTError
import httpx
SECRET_KEY = "read-from-env"
ALGORITHM = "HS256"
def require_valid_license(x_license_key: str = Header(...)):
"""Check the license key against your licensing server."""
response = httpx.get(
"https://licenses.yourdomain.com/validate",
params={"key": x_license_key},
timeout=5.0
)
if response.status_code != 200 or not response.json().get("valid"):
raise HTTPException(status_code=402, detail="Invalid or expired license")
return TrueThe licensing server
Your licensing server is just a tiny API you host yourself (or on a cheap VPS) that stores valid license keys and their expiry dates. When a customer pays, your payment webhook writes a new key to this database. When their subscription lapses, you flip a boolean. That's the whole model.
You can build this in an afternoon with FastAPI and SQLite. Alternatively, services like LemonSqueezy or Paddle let you generate license keys automatically on purchase β you just call their validation API instead of your own.
Package It for Easy Installation
Your packaging story determines whether you get five installs or five hundred. The goal is: clone the repo, copy .env.example to .env, add their license key, run one command.
# docker-compose.yml
version: "3.9"
services:
app:
build: .
ports:
- "${PORT:-8000}:8000"
env_file: .env
depends_on:
- db
db:
image: postgres:15-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: mytool
volumes:
pgdata:Write a README.md that covers installation in under ten steps. If you need more than ten steps, your packaging needs work, not your documentation.
Distributing without open-sourcing your core logic
If you want to keep the source proprietary, distribute a pre-built Docker image from a private registry, or use a tool like pyarmor to obfuscate the Python source before packaging. For most early-stage tools, obfuscation is overkill β the license check plus reasonable friction is enough protection. People who would crack your tool weren't going to pay anyway.
Build a Minimal Frontend
Your users don't want to curl API endpoints. Even a bare-bones HTML form that triggers your script and shows the output is enough to make the product feel real.
For a simple admin UI, consider htmx with Jinja2 templates served directly from FastAPI. You get dynamic behavior without a full JavaScript framework, and the entire frontend ships inside your Docker image with no separate build step.
<!-- templates/index.html -->
<form hx-post="/run" hx-target="#result" hx-swap="innerHTML">
<input type="date" name="start_date" required />
<input type="date" name="end_date" required />
<button type="submit">Run Report</button>
</form>
<div id="result"></div>That's often all you need for version 1. Ship the minimal interface that makes the core job doable. You can add dashboards later once you know what users actually look at.
Price It for Teams, Not Individuals
Self-hosted tools have a different pricing dynamic than SaaS. The buyer is paying for the right to run it, not for uptime you're maintaining. That shifts the conversation: they get more control, you get less operational burden.
A workable model for most small self-hosted tools:
- Per-seat licensing β charge by the number of users who can authenticate. Simple to understand, easy to enforce via your license check.
- Flat team license β one price for up to N seats (e.g., "up to 10 users"). Less friction at the buying stage.
- Annual only β self-hosted tools are harder to cancel impulsively. Annual billing improves your cash flow and reduces churn.
When setting a price, run this quick test: ask three people who fit your target customer β not friends, actual practitioners β "ask yes or no: would you pay $X/month for this tool?" Start at a number that feels slightly too high to you. You'll be surprised how often the answer is yes.
Common Pitfalls
Skipping the update story. If a bug fix requires customers to rebuild their Docker image manually, some won't bother and will run a broken version. Add a simple version check endpoint that compares the running version against the latest tag on your distribution registry and displays a banner if they're behind.
Embedding secrets in the image. Every secret β database passwords, API keys, your license server URL β should come from environment variables at runtime, never baked into the image. Use .env.example as your documentation of required values.
Assuming customers will read the docs. They won't, at least not all of them. Write startup validation code that checks for missing environment variables and prints a clear error message before the server starts. A few lines of defensive code saves hours of support emails.
Pricing too low because it "started as a free script." The fact that you built it for internal use doesn't mean the market values it less. Price it on the value it delivers, not on the hours you spent building it.
Wrapping Up
You have more of the hard work done than you realize. The business logic, the edge cases, the institutional knowledge of what actually matters β that's all in your script already. Turning it into a paid tool is mostly scaffolding work.
Here are your concrete next steps:
- Audit one script this week using the three criteria above (repeatable, time-sensitive, noticed when broken). If it qualifies, it's your candidate.
- Sketch the docker-compose setup before writing any new code. If you can't describe the containers in five minutes, the architecture isn't clear yet.
- Build the license check first, before the frontend, before the polish. Wiring in the paywall at the start is ten times easier than retrofitting it later.
- Show three potential customers the README and ask if they'd pay. Their reaction will tell you more than any market research.
- Ship version 1 ugly. A working tool with a plain HTML interface beats a polished product that isn't finished. You can always improve the UI after someone has paid you.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!