Software Engineer
Loading posts...
Picture this: spending 2+ hours daily copy-pasting job listings into a Google Sheet, only to realize half of them are in Toronto (where I can't work) or for the wrong graduation year. Here's how I built a Python script that does the boring stuff so I can focus on actually landing interviews.

How I designed and implemented a Bluetooth Low Energy check‑in platform using ESP32, Web Bluetooth API and our existing web services for seamless workshop attendance.

What started as curiosity about Wikipedia rabbit holes turned into parsing 6.8 million articles and building a Neo4j graph database. Here's how I mapped the structure of human knowledge in a weekend.
Feel free to contact me at kanishksachdev@gmail.com
Picture this: you're building an email forwarding manager for your organization, and you hit a wall. The Namecheap API requires IP whitelisting. Vercel wants $20/month for a static IP. Setting up a proper Cloud Run deployment with a static IP involves NAT gateways, VPC configurations, and more GCP wizardry than you want to deal with on a Tuesday night.
So what do you do? You grab the tiniest GCP Compute Engine instance money can buy (spoiler: it's about $5/month) and see just how much you can cram into it.
Here's the story of how we built our own miniature data center and probably violated every "separation of concerns" principle in the process. But hey, it works beautifully.
Let me paint you the picture. We're building an email forwarding management system for our organization. Think of it as a nice UI where team members can create email forwards like events@organization.com → john@gmail.com without having to log into domain registrar panels or remember cryptic API calls.
The technical setup seemed straightforward:
But here's where things got interesting. Namecheap's API has a security requirement: you have to whitelist the IP addresses that can make API calls. Makes sense from a security perspective, but it's a pain for serverless deployments where your IP changes constantly.
Our options were:
Guess which one we picked?
Once we had a Compute Engine instance running our email forwarding API, we looked at the resource usage and laughed. We were using maybe 10% of the available resources. That's when the dangerous thought crept in: "What else could we run on this thing?"
You know what's fun? Having a bunch of useful services scattered across different platforms, each with their own logins, their own monitoring, their own deployment pipelines. Said no one ever.
So we decided to see just how much we could squeeze into our tiny instance. The goal wasn't just to save money (though that's nice). It was to create a single, well-organized hub for all our self-hosted tools.
Here's where Docker Compose became our best friend. Instead of manually managing different services, processes, and configurations, we could define everything in a single docker-compose.yml file and let Docker handle the orchestration.
Our final setup looks like this:
version: '3.8'
services:
proxy-manager:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '81:81'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- nginx-proxy
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- ROCKET_PORT=8080
- ROCKET_ADDRESS=0.0.0.0
- ADMIN_TOKEN=[REDACTED]
volumes:
- ./vaultwarden-data:/data
networks:
- nginx-proxy
uptime-kuma:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
restart: unless-stopped
volumes:
- ./uptime-kuma-data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
networks:
- nginx-proxy
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: unless-stopped
ports:
- '9000:9000'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- nginx-proxy
watchtower:
image: containrrr/watchtower:latest
restart: unless-stopped
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
--interval 300
--cleanup
--debug
networks:
- nginx-proxy
peerjs-server:
image: peerjs/peerjs-server:latest
container_name: peerjs
restart: unless-stopped
command:
- peerjs
- --port
- '9000'
- --key
- peerjs
- --path
- /peerjs
- --allow_discovery
- 'true'
- --proxied
- 'true'
- --alive_timeout
- '15000'
networks:
- nginx-proxy
networks:
nginx-proxy:
external: true
name: nginx-proxy
volumes:
portainer_data:
Let me break down what we crammed into this tiny machine and why each service earned its spot.
First up: Nginx Proxy Manager. This is the crown jewel of our setup, the service that makes everything else possible.
Here's the problem: we have multiple services that need to be accessible via nice domain names with HTTPS certificates. Manually configuring Nginx, managing SSL certificates, and keeping everything updated is a nightmare.
Nginx Proxy Manager gives us:
Setting it up was surprisingly simple. Point a domain to your server's IP, access the admin panel on port 81, and start creating proxy hosts. Want passwords.yourdomain.com to route to your Vaultwarden instance? Two clicks and a minute later, you've got HTTPS-enabled password management.
The genius of this setup is that every other service can run on internal ports (like 8080, 3000, etc.) and only the proxy manager needs to expose ports 80 and 443 to the world.
Next up: Vaultwarden, which is an unofficial Bitwarden server implementation written in Rust. Why not just use Bitwarden's cloud service? A few reasons:
Vaultwarden is incredibly lightweight. It uses maybe 50MB of RAM and barely touches the CPU. The setup is dead simple:
vaultwarden:
image: vaultwarden/server:latest
environment:
- ROCKET_PORT=8080
- ROCKET_ADDRESS=0.0.0.0
- ADMIN_TOKEN=[REDACTED]
volumes:
- ./vaultwarden-data:/data
Point your Bitwarden client apps to passwords.yourdomain.com and boom. You've got enterprise-grade password management for the cost of a domain name.
The admin token gives you access to a web panel where you can invite users, monitor usage, and configure advanced settings. We've got the whole team using it now, and it's rock solid.
Since losing everyone's passwords would be catastrophic, we run automated external backups of the Vaultwarden data directory every 6 hours. A simple cron job tars up the data, keeps the last 14 backups locally for quick recovery, and uploads a copy to Google Cloud Storage for disaster recovery. At $0.02/GB/month, the peace of mind is worth way more than the cost.
Here's something they don't tell you about running your own infrastructure: stuff breaks. Services go down, SSL certificates expire, APIs start returning errors, and you won't know until someone complains.
Uptime Kuma is like having a tireless intern who checks on all your services 24/7 and sends you notifications when something goes wrong.
What makes Uptime Kuma special:
/var/run/docker.sock, it can even monitor Docker containers directlySetting up monitoring for our email forwarding API was as simple as adding the endpoint URL and configuring Discord notifications. Now we get instant alerts if the API goes down, response times spike, or SSL certificates are about to expire.
You can actually see our Uptime Kuma instance in action at status.hackpsu.org - it's monitoring all our HackPSU services and provides a public status page that shows real-time uptime statistics and response times for everything from our main website to our internal APIs.
The psychological benefit is huge. Instead of constantly wondering "is everything still working?", you can trust that you'll know immediately if something breaks.
Managing Docker containers from the command line is fine when you have 2-3 services. When you have 6+ containers with different configurations, volumes, networks, and dependencies, a GUI becomes really valuable.
Portainer gives you:
The killer feature for us is the logging interface. When something goes wrong, you can instantly view container logs, filter by time ranges, and download logs for analysis. No more docker logs container_name | grep error | tail -100.
Security updates are important. Feature updates are nice. But manually updating 6+ Docker containers every week? That's a recipe for neglect and vulnerabilities.
Watchtower solves this by automatically updating your Docker containers when new images are available. Here's how it works:
watchtower:
image: containrrr/watchtower:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
--interval 300
--cleanup
--debug
The --cleanup flag is crucial. Without it, old Docker images accumulate and you'll run out of disk space fast on a small instance.
We've been running this for months, and it's updated everything from security patches to major feature releases without any manual intervention. The peace of mind is incredible.
This one's a bit more niche, but hear me out. We occasionally build applications that need real-time communication between browsers. WebRTC is perfect for this, but it needs a signaling server to help peers find each other.
Instead of using a third-party service or deploying a separate Node.js app, we just throw a PeerJS server into our Docker Compose stack:
peerjs-server:
image: peerjs/peerjs-server:latest
command:
- peerjs
- --port
- '9000'
- --key
- peerjs
- --path
- /peerjs
- --allow_discovery
- 'true'
- --proxied
- 'true'
- --alive_timeout
- '15000'
Now any WebRTC application we build can use peer.yourdomain.com/peerjs as its signaling server. The resource usage is minimal unless you're actively using it, but having it available means one less external dependency for future projects.
And of course, running alongside all these containerized services, we have our original email forwarding manager. It's a Next.js application with a clean React interface for managing Namecheap email forwards.
The frontend is built with React Table for filtering and sorting forwards, plus a dialog system for creating new forwarding rules:
// Simplified version of our forwarding management interface
const handleDelete = async (mailbox: string, forwardTo: string) => {
try {
const response = await fetch(
`/api/email?mailbox=${encodeURIComponent(mailbox)}&forwardTo=${encodeURIComponent(forwardTo)}`,
{ method: 'DELETE' },
)
if (!response.ok) {
throw new Error('Failed to delete forwarding rule')
}
// Refresh the data and notify
const res = await fetch('/api/email')
const data = await res.json()
if (data.ok) {
onEntriesChange(data.ok)
toast.success('Forwarding rule deleted successfully')
}
} catch (err) {
toast.error('Failed to delete forwarding rule')
}
}
The backend is a simple Next.js API route that interfaces with the Namecheap API:
export async function DELETE(req: NextRequest) {
const { searchParams } = req.nextUrl
const mailbox = searchParams.get('mailbox')
const forwardTo = searchParams.get('forwardTo')
if (!mailbox || !forwardTo) {
return NextResponse.json(
{ error: 'mailbox and forwardTo query params are required' },
{ status: 400 },
)
}
const list = await getEmailForwarding()
const filtered = list.filter(
(e) => !(e.mailbox === mailbox && e.forwardTo === forwardTo),
)
await setEmailForwarding(filtered)
return NextResponse.json({ ok: true })
}
The beauty is that this runs directly on the Compute Engine instance using PM2 for process management, giving us the static IP we need for Namecheap's API whitelist requirements.
Here's the fun part. After cramming all these services into a single e2-micro instance (1 vCPU, 1GB RAM), let's look at the actual resource usage:
Memory Usage:
Total: ~760MB out of 1GB available
CPU Usage: Typically under 10% unless we're actively using multiple services
Storage: About 5GB used out of 10GB available (mostly Docker images and data volumes)
The instance handles everything beautifully. Response times are snappy, services are stable, and we've got room to grow.
The networking setup deserves special mention because it's what makes this whole thing possible. Every service runs in a shared Docker network called nginx-proxy:
networks:
nginx-proxy:
external: true
name: nginx-proxy
This means:
http://vaultwarden:8080)When you want to add a new service, you just:
nginx-proxy networkhttp://container_name:portIt's like having your own private cloud with enterprise-grade networking, but simpler.
The Good:
The Tricky:
The Surprising:
Running multiple services on one instance requires extra attention to security:
We also use Cloudflare as a CDN/proxy layer, which adds DDoS protection and hides our server's real IP from casual discovery.
We're thinking about adding some more fun (and useful) tools to our little server:
go.hackpsu.org/apply instead of those ad infested link shortenersBut honestly? The current setup works so well that we're hesitant to mess with it. Sometimes the best engineering decision is knowing when to stop optimizing.
Here's the thing: this project taught us that you don't need a massive Kubernetes cluster or a complex microservices architecture to run serious infrastructure. Sometimes the best solution is the simplest one that meets your needs.
We needed a static IP for API access. We ended up with a complete self-hosted infrastructure platform that costs less than a couple of coffee drinks per month and gives us more control and functionality than most SaaS tools.
The key insights:
If you're building side projects or small team infrastructure, seriously consider the "tiny instance, maximum value" approach. Your wallet will thank you, your team will love the simplicity, and you might just learn something cool about systems administration along the way.
Plus, there's something deeply satisfying about seeing docker stats show a perfectly balanced resource usage across six different services, all humming along quietly in their little corner of the internet.