Code Snippets12 min read

VPS Deployment: So You Survived Another Night, Now Do It Right

YEHYoussef El Hejjioui··12 min read

Alright, another production scare averted. Or maybe you just inherited a 'server' that's actually someone's old laptop under a desk. Either way, you're here, eyeing that bare metal – or, more likely, virtual metal – and thinking about how to get your shiny new app running without immediately causing the next fire. This isn't a "top 5 tips" article from a growth hacker. This is what you actually do, because you've seen what happens when you don't.

We're talking about deploying to a Virtual Private Server. No managed Kubernetes fairy dust, no serverless magic wand. Just a Linux box and your code. It's deceptively simple, which is why it's also where most people stub their toes, then their shins, then faceplant into a database full of corrupted data.

The Bare Earth: Initial Server Setup That Isn't Optional

First, you SSH in. If you're still using passwords for SSH, stop. Seriously. It's 2024. Generate an SSH key pair. Put the public key on the server in '~/.ssh/authorized_keys'. Disable password authentication in '/etc/ssh/sshd_config'. Restart the SSH service. If you've never done this, do it on a test box first. You don't want to lock yourself out of production at 3 AM because you mistyped 'PermitRootLogin no' and now 'sudo' is telling you to take a hike.

Next, firewall. 'ufw' is your friend. 'sudo ufw enable'. Deny everything by default, then selectively allow ports: SSH (22, or better yet, something non-standard), HTTP (80), HTTPS (443). If your database is listening on '0.0.0.0' and isn't behind a firewall, that's not a server, that's an open invitation for some script kiddie to 'DROP TABLE users'. And yes, I've seen it. On a Sunday. During brunch.

Update your packages. 'sudo apt update && sudo apt upgrade -y'. Get this out of the way. Don't let your system packages get stale. The security patches exist for a reason, and that reason usually involves someone else's expensive lesson. Also, set up NTP (Network Time Protocol). Time synchronization is crucial. Debugging logs where timestamps are drifting by minutes is a special kind of hell.

Your App's New Home: Environment and Isolation

Your application doesn't need to run as 'root'. It definitely shouldn't. Create a dedicated system user for your application: 'sudo adduser your-app-user --system --no-create-home'. Give it just enough permissions to do its job. Your app's files should be owned by this user. This is basic least privilege, and it's a critical layer of defense when (not if) something goes sideways.

For language runtimes (Node.js, Python, Ruby, etc.), use a version manager (e.g., 'nvm', 'pyenv', 'rvm'). This keeps your system's global binaries clean and allows you to switch versions easily without reinstalling the entire OS. Then, install your application dependencies. For Node.js, 'npm install --production' or 'yarn install --production' in your app's directory. For Python, a virtual environment ('python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt'). This isolates your app's dependencies, preventing conflicts and keeping things tidy. Trust me, untidy dependencies are a common source of 'works on my machine' production failures.

If you have a database (PostgreSQL, MySQL), install it. Secure it. Create a dedicated database user with a strong, random password. Restrict access to 'localhost' where possible. If your app and DB are on the same server, there's rarely a good reason for the DB to be listening on external interfaces. Configure your database backups early. Don't wait until you're staring at a blank table, wondering where three months of customer data went. Test restoring those backups. The only thing worse than no backup is a backup that doesn't restore.

The Workhorse: Keeping It Running (and Restarting)

Never, ever, 'node app.js &' or 'python app.py &'. That's not how you deploy production applications. That's how you lose your job when the server hiccups. You need a proper process manager. On Linux, 'systemd' is your daily driver. Write a 'systemd' unit file for your application (e.g., '/etc/systemd/system/your-app.service').

It should look something like this:

'[Unit]' 'Description=My Awesome Web App' 'After=network.target'

'[Service]' 'User=your-app-user' 'WorkingDirectory=/path/to/your/app' 'Environment=NODE_ENV=production' 'ExecStart=/usr/bin/node /path/to/your/app/index.js' 'Restart=always' 'RestartSec=5' 'StandardOutput=journal' 'StandardError=journal'

'[Install]' 'WantedBy=multi-user.target'

Adjust 'ExecStart' for your specific command (e.g., 'venv/bin/gunicorn app:app' for Python/Gunicorn, or whatever your app uses). 'Restart=always' is critical; it tells 'systemd' to bring your app back up if it crashes or the server reboots. 'StandardOutput=journal' means your logs go into 'journalctl', which is where you'll be spending a lot of quality time at 3 AM. After creating the file: 'sudo systemctl daemon-reload', 'sudo systemctl enable your-app', 'sudo systemctl start your-app'. Learn 'systemctl status your-app', 'journalctl -u your-app', and 'journalctl -f -u your-app'. These are your eyes and ears.

For Node.js, you might consider 'PM2' for its clustering and zero-downtime reload capabilities, but it often runs under 'systemd'. For Python, 'Gunicorn' or 'uWSGI' are standard. Whatever you choose, ensure it's managed by 'systemd' at the top level.

The Gatekeeper: Reverse Proxy (Nginx/Caddy)

Your application server (Node, Python, etc.) should generally listen on 'localhost' on some high port (e.g., 3000, 8000). You don't expose that directly to the internet. Instead, you put a reverse proxy in front of it. 'Nginx' is the industry standard for a reason. 'Caddy' is a simpler, modern alternative that handles SSL certificates automatically.

A basic 'Nginx' config (e.g., in '/etc/nginx/sites-available/your-app.conf', then symlink to '/etc/nginx/sites-enabled/'):

'server {' ' listen 80;' ' server_name yourdomain.com www.yourdomain.com;' '' ' location / {' ' proxy_pass http://localhost:3000;' ' proxy_http_version 1.1;' ' proxy_set_header Upgrade $http_upgrade;' ' proxy_set_header Connection "upgrade";' ' proxy_set_header Host $host;' ' proxy_cache_bypass $http_upgrade;' ' }' '} '

Then, get SSL. 'Certbot' (with Let's Encrypt) makes this trivial for 'Nginx': 'sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com'. It will configure 'Nginx' for HTTPS, set up automatic renewals, and save you from the nightmare of manually managing certificates. Don't skip SSL. Even for internal apps. Just don't.

The Ritual: Deployment Workflow

Your deployment process needs to be repeatable. Not "login, manually type commands, pray." The simplest, most direct method, until you automate with CI/CD, is:

  1. SSH into the server.
  2. Navigate to your app's directory (e.g., '/var/www/your-app').
  3. 'git pull origin main' (or whatever branch is deployable).
  4. Run any build steps: 'npm install --production', 'npm run build', 'pip install -r requirements.txt', etc.
  5. Restart your application service: 'sudo systemctl restart your-app.service'.
  6. Watch 'journalctl -f -u your-app.service' for errors. Immediately. Don't deploy and walk away. That's how you find out at 3 PM that something has been broken since 3 AM.

Eventually, you'll want to automate this with GitHub Actions, GitLab CI, or similar. But get the manual process down cold first. Understand every step and what it's doing. The CI/CD pipeline is just automating these exact steps, usually with a dedicated deployment user and SSH key.

Security, Beyond the Basics

  • Regular Updates: Patch your system. Set up 'unattended-upgrades' if you're comfortable with it, but monitor it.
  • SSH Hardening: Besides keys and disabling passwords, consider disabling root login directly, and changing the default SSH port.
  • Audit Logs: Keep an eye on '/var/log/auth.log' for suspicious login attempts. Tools like 'Fail2Ban' can automate blocking brute-force attacks.
  • Never store secrets in your code. Use environment variables. Your 'systemd' unit file is a good place for them, or a proper secret management system if you're feeling fancy. But for a single VPS, environment variables in the service file are a good start.

The Long Game

This isn't about setting it up once and forgetting it. It's about building a robust, observable foundation so that when things inevitably break (because they always do), you're not fumbling in the dark. You'll know where to look, what to check, and hopefully, how to fix it without calling in the cavalry. It's about gaining back those precious hours of sleep. Mostly.

  • Deploying on a VPS means you own the whole stack. That's power, but also responsibility. Don't treat it like a toy. Treat it like a sleeping dragon: respect it, feed it properly, and it might just let you sleep through the night. Until the next time, anyway.
YEH
Studies and Development Engineer
More

Continue reading

JS Mastery is a Myth, and We're All Just Managing the Chaos

We've all been there: 3 AM, staring at a JavaScript stack trace wondering how 'undefined' broke everything. This language isn't meant to be mastered; it's a beast you learn to wrangle, day by painful day.

6 min

When 'Just Add Threads' Turns into a 3 AM Pager Duty Nightmare

Peeling back the layers of C++ threads, from CPU context switching to the brutal realities of cache coherency and false sharing that turn textbook concurrency into a production incident.

8 min
VPS Deployment Guide: Senior Engineer's Production Setup | Unmatched Quotes