DEV Community

Deploying Your First Production App the Right Way

The gap between "it runs" and "it's in production"

You built something. It runs on your laptop. You git push, SSH into a server, run node server.js, see it respond on port 3000, and call it deployed. Then you close your terminal, and the app dies. Or it survives until the server reboots. Or it works, but a user's password is in your logs, the app is running as root, and the database is open to the entire internet.

Every one of these is the same lesson: deploying isn't running your app on a different computer. It's running it in a way that survives reality. Reality means crashes, reboots, traffic spikes, attackers scanning your ports, and a future you who needs to know why it broke at 3am.

The good news: "production-ready" is not a vague vibe. It's a concrete, finite checklist. This article walks the whole thing, and by the end you'll have a mental model of the minimal real deployment plus the commands to build it.

Note: Who this is for: Beginners who can build an app but have never deployed one properly, or who deployed one and got burned. If you know how to SSH into a server and run a command, you have enough to follow. Examples use a Linux server and a Node app, but every concept maps to Python, Go, or anything else.

What "production" actually means

Production is the environment where your code meets real users, real traffic, and real consequences, so it has to survive crashes, reboots, and bad actors without you watching it. The leap from your laptop isn't about a fancier machine. It's about removing every assumption that quietly holds your dev setup together: that you're watching the terminal, that nobody's attacking you, that the process never crashes, that secrets in a file are fine.

Think of it like the difference between cooking at home and running a restaurant kitchen:

In the real world In tech
๐Ÿณ You watch the pan the whole time A process manager watches the app
๐Ÿ”ฅ Smoke alarm + sprinklers Health checks + auto-restart
๐Ÿšช Locked door, staff-only areas HTTPS, private network, non-root user
๐Ÿ“‹ Order tickets you can review Structured logs
๐Ÿ‘€ A manager noticing trouble Basic monitoring + alerts

Your laptop is home cooking. Production is a health-inspected commercial kitchen, same food, completely different rules. Each pair below is one item on the checklist. Let's see the shape of the whole thing first, then build it piece by piece.

The minimal production topology

Before any commands, get the picture in your head. Almost every small production app looks like this: traffic comes in over HTTPS, hits a load balancer, gets forwarded to your app on a private network, and your app talks to a managed database that the internet can never reach.

Notice what's exposed: only the load balancer. Your app server has no public IP. Your database has no public IP. This is the same layering as a VPC, public edge, private everything-else, and it's the difference between a deployment and a breach waiting to happen.

Step 1: Get config out of your code

On your laptop you hardcode the database URL or stick it in a committed file. In production that's two problems: you can't change it without a redeploy, and your secrets end up in Git forever. The fix is the oldest rule in the book: config lives in the environment, not the code. This is the heart of the Twelve-Factor App approach.

/etc/myapp/app.env  # Config the app reads at startup, NEVER committed to Git
NODE_ENV=production
PORT=8080
DATABASE_URL=postgres://app:${DB_PASSWORD}@db.internal:5432/myapp
LOG_LEVEL=info

Warning: The mistake that leaks every secret: If you ever commit a .env file with a real password, assume it's compromised forever, rotate the credential immediately. Git history keeps deleted files. Add .env to .gitignore on day one, before the first commit, not after.

Step 2: Run it under a process manager

node server.js in your terminal dies when you log out or the process crashes. Production needs something that starts your app on boot, restarts it if it crashes, and runs it in the background. On Linux, the built-in answer is systemd, no extra tools, and it's what real servers use.

/etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network.target

[Service]
User=appuser                    # NOT root, see step 5
EnvironmentFile=/etc/myapp/app.env  # config from step 1
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node server.js
Restart=always                  # crash? restart automatically
RestartSec=3

[Install]
WantedBy=multi-user.target
enable.sh
sudo systemctl daemon-reload
sudo systemctl enable myapp     # start on every boot
sudo systemctl start myapp
sudo systemctl status myapp     # is it healthy?

Tip: Restart=always is the single line that turns "my app died and I didn't notice for 6 hours" into "my app blipped for 3 seconds and recovered itself." It's free resilience.

Step 3: Add a health check endpoint

How does the load balancer know your app is alive and ready for traffic? It asks, repeatedly. You give it a tiny endpoint that returns 200 OK only when the app is genuinely healthy. If the check fails, the load balancer stops sending that instance traffic. This is non-negotiable: without it, a broken instance keeps receiving (and dropping) real user requests.

// A real health check verifies dependencies, not just "the process is up"
app.get("/healthz", async (_req, res) => {
  try {
    await db.query("SELECT 1");  // can we actually reach the DB?
    res.status(200).json({ status: "ok" });
  } catch {
    res.status(503).json({ status: "unhealthy" });
  }
});

Return 200 only when the app can do its job. A health check that always returns 200 (even when the database is down) is worse than none - it tells the load balancer to keep sending traffic into a black hole.

Step 4: Put HTTPS and a load balancer in front

Two jobs, one piece. HTTPS encrypts traffic so passwords and tokens aren't sent in plain text - non-negotiable in 2026, and browsers will shame you with "Not Secure" without it. A load balancer is the single public entry point that terminates TLS, runs your health checks, and forwards traffic to your app over the private network. Even with one app instance today, putting it behind a load balancer now means adding a second instance later is trivial - that's the whole story of load balancing and auto-scaling.

Load Balancer App Server Database
Public IP Yes (the only one) No No
Handles HTTPS/TLS Yes, terminates here No (plain HTTP inside) No
Reachable from The internet Only the load balancer Only the app server
Health checked It does the checking Yes, by the LB Yes, by the app's check

What lives where, and what's exposed to the internet.

Tip: Get free, auto-renewing certificates from your cloud's certificate manager (AWS ACM, etc.) attached straight to the load balancer. No more manual cert renewals, no more expired-certificate outages at midnight.

Step 5: Don't run as root, and lock the network

If your app runs as root and an attacker finds a bug in it, they now own the entire server - install software, read every file, pivot to other machines. Run the app as a dedicated unprivileged user (appuser in step 2) that can do exactly one thing: run your app. If it gets compromised, the blast radius is tiny.

# A user with no login shell and no home dir, purely to run the app
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuser
sudo chown -R appuser:appuser /opt/myapp

Then lock the network with security groups (firewall rules):

  • The load balancer accepts :443 from anywhere
  • The app accepts :8080 only from the load balancer
  • The database accepts :5432 only from the app

Each tier trusts only the tier directly in front of it.

Step 6: Logs and basic monitoring

When (not if) something breaks, logs are how you find out why. Two rules: log to stdout (let the platform collect it, don't write to files you'll forget about) and log structured JSON so you can search it. Then add the bare-minimum monitoring: an alert when the app is down, when errors spike, and when CPU or memory is pegged.

// Structured logs you can actually search later
function log(level: string, msg: string, extra = {}) {
  console.log(JSON.stringify({
    ts: new Date().toISOString(),
    level,
    msg,
    ...extra,
  }));
}

log("info", "order created", { orderId: "o_123", userId: "u_45" });
// NEVER log secrets: no passwords, tokens, or full card numbers

Warning: The log that becomes a breach: Logging the full request body or auth headers "for debugging" is how passwords and session tokens end up sitting in plain text in your log store. Redact secrets before they're ever logged.

Common mistakes that cost hours

  • No process manager. Running the app in a raw terminal or nohup. It dies on logout or first crash and stays dead until a human notices. Use systemd (or your platform's equivalent) with Restart=always.
  • Health check that always says OK. It returns 200 even when the database is unreachable, so the load balancer keeps routing traffic to a broken instance. Check real dependencies.
  • Running as root. One app bug becomes full server takeover. Always run as a dedicated unprivileged user.
  • Database in a public subnet with a password as the only defence. Bots scan the entire internet for open database ports constantly. The database should have no public IP, reachable only from the app tier.
  • Secrets committed to Git. Once it's in history, it's compromised. .gitignore your env files before the first commit, and use a secrets manager for anything sensitive.
  • No HTTPS. Plain HTTP sends passwords in cleartext over the wire. Terminate TLS at the load balancer with a managed certificate - it's free.

Where to go next

The whole article in 7 lines:

  1. Production means surviving crashes, reboots, and attackers without you watching, not just running on another computer.
  2. Config lives in the environment, never in committed code. Secrets out of Git, always.
  3. A process manager (systemd) restarts your app on crash and starts it on boot.
  4. A health check that tests real dependencies lets the load balancer route around broken instances.
  5. Put HTTPS + a load balancer in front; it's the only thing that should face the internet.
  6. Run as a non-root user and lock each tier's firewall to the tier in front of it.
  7. Log structured JSON to stdout (never secrets) and alert on down / errors / saturation.

You now have the checklist. The fastest way to make it stick is to actually deploy something small end-to-end, then deepen each piece:

  • Scale beyond one instance: Load Balancing & Auto-Scaling Explained picks up exactly where this leaves off.
  • Pick the right data store and let the cloud run it for you: Managed Databases: Relational vs NoSQL.
  • Understand the private network everything sits on: Cloud Networking Fundamentals: VPC.
  • Get hands-on with the server side in the browser: the Linux Lab.

Deploy one tiny app properly today - env config, systemd, health check, HTTPS, non-root, logs. Do it once and you'll never go back to node server.js in a terminal again.

Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.

Comments

No comments yet. Start the discussion.