1GB VM. How do I pack whole production stack to it.
I'm running a full production backend on GCP's free-tier e2-micro: 2 vCPUs, 1GB RAM. Fastify, PostgreSQL 18, Redis, nginx, systemd, TLS, the whole thing. It works, it's fast, and it costs me nothing while I validate the product.
TLDR
A 2-vCPU, 1GB VM is way bigger than you think. With some tuning you can fit a real stack on it, handle hundreds of users, and not pay a dime until you have revenue. Define the playbook once, and you can scale up or migrate to any provider by replaying the same sequence.
What I'm running
The app is Inner Anchor. It's a productivity app: daily management, task management, focus mode with a floating DPIP (Document Picture-in-Picture) button that reminds you what you're doing, and a purpose bar to remind you why you're doing it. The backend is Fastify with PostgreSQL and a small Redis. Right now it's pre-revenue with a few dozen beta testers.
Why a raw VM instead of Railway or Render
I want to deploy on the server the same way I would scale the server. I want to define the process once and then reuse the whole process when migrating. If I need more power, I just upsize the VM and the whole stack stays. If I need to switch providers, I bring the script to another provider and run the exact sequence and I replicate the environment exactly like that.
Define once, use forever. That's the play.
A managed database on DigitalOcean, which is the cheapest I've found, is minimum $15/month. I can host this entire stack for free because I don't have many users yet. I'm not going to burn money on infrastructure before I find product-market fit.
Squeezing 1GB
At my company I usually deploy on 2GB or 4GB VMs or more. I have never deployed anything serious on 1GB of RAM. So we have to squeeze out the most of the performance here.
ZRAM
Since it's 1GB of RAM, we have to crank up the ZRAM almost to double. One gig of RAM and one gig of compressed swap. So basically we can use about 1.8 gigs of RAM before it OOMs. And we have to turn up the swappiness to 180 to actually utilize all that compressed swap.
ALGO=lz4
PERCENT=100
PRIORITY=100
vm.swappiness=180
vm.vfs_cache_pressure=500
vm.dirty_ratio=5
vm.dirty_background_ratio=1
PostgreSQL and Redis on the same box
I install both directly. No Docker. Docker alone has 100-200MB of overhead. On a 1GB box, that's a lot. I don't even use PM2 to manage the Node process. I just spin up systemd. Prune down everything that's unnecessary and keep the core.
PostgreSQL is tuned for the constraint:
shared_buffers = 128MB
work_mem = 4MB
maintenance_work_mem = 32MB
effective_cache_size = 256MB
max_connections = 20
Redis gets capped at 64MB with allkeys-lru eviction. Both services get OOM score adjustments so the kernel kills the app process before it touches the database.
Frankly, I have never tweaked any database params in my professional work. I found it surprisingly easy. The whole process of installing a raw database and tuning it is just some install commands and it works. I did not expect that.
Why not split them out?
Because I want to scale vertically the same way. Just put in more RAM and more vCPU and it should magically scale. Eventually I'll move to a managed database, but until we have revenue, no.
What it actually looks like in production
I just pulled up htop. Memory is at 361MB used out of 965MB. Swap is at 147MB out of 965MB. CPU hovers at about 1%.
When one user hits the API, both cores spike to about 6% and then drop back to 1%. Memory doesn't move much. For 10+ users spread across the day, this holds up fine.
Response times from the server journalctl (processing time only, not counting latency between the US instance and Vietnam client):
- Average GET: ~4ms
- POST (update daily purpose): ~21ms
21 milliseconds server-side for a POST is pretty damn good if you ask me.
Security without overcomplicating it
I block all ports except what's needed. I moved the SSH port off 22. fail2ban watches both SSH and nginx. There are firewall rules at both the GCP level and UFW. Root login is disabled, password auth is disabled, deploy user is key-only.
I listed Tailscale as a future addition but haven't implemented it. The current setup is good enough. I don't have a reason to add it yet.
The deploy script is dumb on purpose
git pull origin develop
pnpm i
pnpm run migrate:deploy
pnpm run prisma:generate
pnpm run build
sudo systemctl restart app
No Docker, no blue-green, no rollback. This is intentional. For an early stage where I have to move fast, and the beta users are very forgiving, this is fine. I'll add proper deployment when I have about 100 users or more. But right now they are forgiving.
The full playbook
This is the sequence, in order. Each step is idempotent and you can replay it on any Debian-based VM from any provider.
- Update base system, install essentials (openssh, curl, git, htop, ufw)
- Create a
deployuser with key-only SSH, disable root login - Set up ZRAM (lz4, 100% of RAM, swappiness 180)
- Install PostgreSQL 18 from the official repo, create database and user, tune for 1GB
- Install Redis, cap at 64MB with LRU eviction
- Install nginx as a reverse proxy to your app port
- Install fail2ban for SSH and nginx
- Move SSH to a non-default port
- Set OOM score adjustments (nginx > postgres > redis > app)
- TLS with certbot
- UFW: deny all incoming, allow your SSH port, 80, 443
- Set up deploy key for GitHub
- Install nvm, Node, pnpm
- Create systemd service with a 300MB memory guard
- Write the deploy script
That's it. The whole thing takes maybe 30-45 minutes if you're following along. And the next time you need to do it, you have the playbook.
When to leave
This setup can't handle everything. If you need horizontal scaling, background job workers eating memory, or you're getting serious traffic, you'll outgrow it. But for a solopreneur validating a product with no revenue, this is the right level of infrastructure. You're not paying for capacity you don't need, and when you do need more, you just upsize the machine and everything stays exactly where it is.
I'm Hung, a fullstack developer building tools to help bring purpose to your life. You can follow my journey at dhung.dev.