200 Lines of YAML, Replaced by Zero — The Case Against DIY Preview Deploys
Every engineering team that isn't on a frontend-first platform maintains a CI script for preview environments. The script starts small — 30 lines of YAML that pull, build, and deploy a Docker image. Then someone adds Slack notifications. Then staging-environment cleanup. Then PR comments. Then a second framework that needs a different build command. A year later, you're staring at 200 lines of GitHub Actions YAML that nobody on the team fully understands.
We built PreviewDrop so you can delete that script. Here's what it actually costs you — in time, in reliability, and in the features you never get around to building.
The script everyone starts with
A minimal GitHub Actions workflow for preview deploys starts innocently enough:
name: Preview Deploy
on:
push:
branches-ignore: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Deploy
run: |
echo "Deploy logic here — SSH, scp, or curl to your server"
Thirty lines. You copy-paste it from a colleague's repo, change the image name, and it works. For about a week.
What breaks first (and second, and third)
The first thing that breaks is the registry authentication. Your CI runner can't push to the container registry because the token expired, or the secret name changed, or someone rotated the credentials and forgot to update the workflow. The error message is always the same: denied: requested access to the resource is denied. Your team loses an afternoon debugging it.
Next: the SSH deploy step. The appleboy/ssh-action you're using needs the host IP, the SSH key, and the port — all stored as GitHub secrets. Someone changes the staging server, the IP shifts, and now every preview deploy fails silently until someone notices the PR hasn't had a live URL in three days.
Then the routing breaks. You're using a wildcard DNS record pointing to your server, but the container label that maps the branch name to a subdomain has a typo. Or the router configuration doesn't pick up new containers fast enough. Or two containers fight over the same port. Each of these failures takes 30–90 minutes to diagnose, and it's always the person who didn't write the script who gets paged.
The features you never get around to building
The script handles the happy path: push → build → deploy. But here's what it doesn't do, and what most teams never add:
PR comments. Your designer opens a PR and has to find the preview URL in the CI logs — a wall of green text, 800 lines deep. They don't do it. They Slack you instead: "hey, is there a preview for this branch?" Every single time.
Build cache. Your CI runner builds the Docker image from scratch on every push because actions/checkout gives you a fresh runner with no layer cache. A Rails app with native extensions takes 4 minutes of bundle install before it even reaches the application code. You could add Docker layer caching via actions/cache, but that's another 40 lines of YAML and it breaks when the cache key drifts.
Auto-expiry. Preview containers accumulate until the server runs out of disk space. Someone adds a cleanup script to cron. The cron syntax is wrong. Two months later, your staging server has 47 stopped containers and 12 GB of orphaned images. You discover this during an incident.
Password protection. You're an agency showing work to a client. You don't want the URL indexed. You could add basic auth to your app, but that means conditional logic in your application code that only runs in "preview mode" — more complexity in the wrong place.
Build logs. Your CI logs are in the GitHub Actions tab. They're not streaming in real time — they flush in chunks, and you can't see what's happening until the step completes. When a build hangs on npm install, you wait 10 minutes for it to timeout before you can even see the last line.
The maintenance tax
Here's a realistic accounting of what a 200-line preview-deploy workflow costs a 5-engineer team over a year:
| Task | Frequency | Time per incident | Annual total | |---|---|---|---| | Token/secret rotation breaks | 4× per year | 2 hours | 8 hours | | Server IP or config change | 2× per year | 1.5 hours | 3 hours | | New framework support added | 1× per year | 4 hours | 4 hours | | Debugging "it worked yesterday" | 6× per year | 1 hour | 6 hours | | Onboarding a new engineer to the script | 2× per year | 1 hour | 2 hours | | PR comment bot maintenance | ongoing | — | built never | | Total | | | ~23 hours/year |
Twenty-three engineering hours, at a fully loaded cost of $150/hour, is roughly $3,450/year — just to maintain a script that does one thing. And that's before you account for the features you never built.
What zero-configuration looks like
PreviewDrop replaces the entire script with a GitHub App. Install it on your repo, push a branch, and you get:
- A live URL on every push (warm redeploys in under 60 seconds)
- A PR comment with the URL the moment the preview is ready
- Build-cache reuse across pushes — no
actions/cacheYAML required - Password-protected URLs when you need them
- Auto-expiry so you never clean up orphaned containers
- Streaming build logs over WebSocket
The setup is one click. There's no YAML to write, no secrets to rotate, no SSH config to maintain. If you already have a Dockerfile, PreviewDrop uses it. If you don't, it generates one that matches your framework.
When a CI script still makes sense
Not every team should delete their workflow file. If you have a dedicated platform engineer who enjoys maintaining CI infrastructure, and your pipeline is genuinely stable, and you've built all the features your team needs — keep it. The script is free in dollars, and the engineering time is a deliberate investment.
But if you're the person who gets Slacked every time the preview URL is down, and you'd rather spend those 23 hours a year on product work — delete the YAML. We wrote the replacement.
Ready to give every branch a live URL?
Free tier — 2 concurrent previews, no credit card required.
Start free