• Indonesian
  • English
  • Node.js Server Migration: Complete Docker Guide 2026

    Kecepatan:
    ⏱ 9 min read

    Moving Your Node.js App to a New Server? Don’t Panic – the Complete Docker Migration Guide

    Please, stop winging your Node.js migrations. I’ve lost count of the tickets I’ve triaged where somebody ‘just moved’ an app to a new server and then spent two days chasing random errors. Same story every single time: the Node version drifted, dependencies fell apart, the .env file didn’t make the trip, and the database host pointed at a box that no longer existed. I’ve watched this fail enough times that I’m writing this out of pure frustration.

    And that’s the annoying part – none of it was necessary. The tools to do this cleanly have been around for years. Docker wraps your entire app, its libraries, and its settings into one portable package. PM2 saves and restores your whole process list with a single command. The failures aren’t because the job is hard; they’re because people skip the prep. Let me show you how it’s supposed to be done, and warn you about the traps I keep seeing people fall into.

    Difficulty: Intermediate
    Last Updated: August 2026
    Tested On: Node.js 18 & 20, Ubuntu 22.04, Docker 24, PM2 5

    Why Node.js Migrations Keep Blowing Up

    Let’s be honest about the four problems that cause almost every failed migration. First, Node version drift. Code that runs perfectly on Node 18 quietly breaks on Node 20 – a deprecated API you’ve been leaning on, a changed default behavior, a breaking change you never noticed in the release notes. Second, dependency hell. One package pins lodash@4, another demands lodash@3, npm can’t resolve the tree, and the install dies at step zero. Those two alone account for most of the failed deploys I’ve seen this year.

    Third, and this one’s the quiet killer: environment variables. Your .env file isn’t in git – most teams ignore it deliberately. So the code gets copied over, the app boots, and then dies on the first API call because SECRET_KEY is undefined or NODE_ENV was never set. Fourth, the database host. On the old box, DB_HOST was 127.0.0.1. On the new server, your database lives at a different address, behind a different firewall. Miss that, and you’ll stare at ECONNREFUSED until your eyes bleed. Every single one of these is preventable, and that’s what makes it so frustrating.

    The Right Way: Wrap Everything in Docker

    Docker exists precisely for this problem. You build one image that contains the code, the exact dependency set, the runtime version, and your settings. That image runs identically anywhere the Docker daemon runs. Migration stops being a re-installation project and becomes a copy-paste job. That’s the whole point of bundling it up before you move.

    Step 1 – Write a Dockerfile

    Start at the root of your project and create a Dockerfile. A minimal, production-sane example for Node.js 20:

    FROM node:20-slim
    WORKDIR /app
    COPY package.json package-lock.json ./
    RUN npm ci --omit=dev
    COPY . .
    ENV NODE_ENV=production
    EXPOSE 3000
    CMD ["node", "index.js"]

    Notice the deliberate choices. node:20-slim pins the runtime, so version drift can’t happen. npm ci, not npm install, installs exactly what’s in package-lock.json – no surprise dependency tree. And –omit=dev keeps the image lean enough for production.

    Pro tip: Add a .dockerignore file at the project root containing node_modules, .git, and .env. Your build gets faster and the image stays small. Your future self will thank you.

    Step 2 – Build the Image

    Build it with:

    docker build -t myapp:v1 .

    If the build completes, verify with docker images – you should see myapp:v1 listed. If it fails, fix it now, on the old server, while you still have everything in front of you. Do not carry a broken package across the network.

    Step 3 – Smoke-Test on the Old Server

    Never move a package you haven’t run. Start the image with the real environment variables:

    docker run -d -p 3000:3000 --env-file .env --name myapp-test myapp:v1

    Then hit it with curl http://localhost:3000 and confirm you get a healthy response. If the image works here, it’ll work on the new box. If it doesn’t work here, don’t touch the new server yet.

    Step 4 – Move the Image

    Two options. Option one: export the image to a tarball and copy it over. Perfect when both servers sit on the same internal network:

    docker save -o myapp.tar myapp:v1
    scp myapp.tar user@newserver:/tmp/

    Option two: push to a registry – Docker Hub, GHCR, or your own private one. Better if you’ll redeploy often or want version history and clean rollbacks:

    docker tag myapp:v1 registry.yourdomain.com/myapp:v1
    docker push registry.yourdomain.com/myapp:v1

    Step 5 – Deploy on the New Server

    On the new box, load the image and run it:

    docker load -i /tmp/myapp.tar
    docker run -d -p 3000:3000 --env-file .env --restart unless-stopped --name myapp myapp:v1

    Running multiple services side by side, like a database or Redis? Use docker compose up -d and keep everything declarative. While you’re at it, read our Docker security hardening guide before exposing anything to the internet.

    Step 6 – Move the .env and Fix the Database Pointer

    This is where most people trip. The .env file must travel separately – it should never be baked into an image, and it should never be committed to git. Copy it securely (scp over a trusted network is fine), tighten its permissions, and update DB_HOST and friends to point at the new database. Then confirm your firewall only lets the app server reach the database port, not the whole internet. For the full walkthrough, check our environment variable management guide and our MySQL migration guide.

    Warning: Never bake secrets into an image. The moment an image is pushed to a registry, anyone with access can inspect it. Always inject .env at runtime via –env-file or a secret manager.

    The Low-Friction Alternative: PM2 Save and Restore

    Still running bare Node processes under PM2? Fine. PM2 ships a backup and restore workflow that’s almost too easy. The old process list gets dumped into a snapshot, you move the snapshot, and one command brings everything back. Think of it like renewing an expired registration – old data saved, moved, reattached.

    Here’s the sequence:

    1. On the old server, snapshot the running apps: pm2 save
    2. Copy the snapshot to the new server: scp ~/.pm2/dump.pm2 user@newserver:/home/user/
    3. Install PM2 on the new server, then restore: pm2 resurrect
    4. Verify with pm2 status

    Critical caveat: pm2 resurrect restores the process list, not the code. You still have to move your app folder and node_modules yourself. See our Linux backup and restore guide and our PM2 production guide for the full picture.

    Docker or PM2? Here’s the Honest Comparison

    There’s no universal winner, and anyone who tells you otherwise is selling something. Here’s how I weigh them:

    Consideration Docker PM2
    What’s packaged Code, libraries, settings, runtime Process list only
    Environment consistency Very high Tied to the host OS
    Setup effort Requires learning Dockerfile Fast and practical
    Scaling Built for horizontal scaling Single server
    Best for Routine migrations, CI/CD deploys Quick moves between servers

    The rule of thumb: if you move once in a blue moon and your stack is simple, PM2 is enough. If your app keeps growing and you need guaranteed environments, make the jump to Docker now rather than later.

    Quick Troubleshooting When Things Still Break

    Even with a clean migration, something can slip. This is the table I keep in my head:

    Symptom Cause Fix
    Cannot find module ‘lodash’ node_modules didn’t travel or versions differ Remove node_modules and reinstall with npm ci
    NODE_ENV is not defined .env file didn’t travel Copy the .env and verify every variable
    ECONNREFUSED connecting to DB DB_HOST is stale or firewall blocks the port Update the host, open the port for the app IP only
    Module version mismatch Native module compiled against a different Node Rebuild the image or run npm rebuild
    listen EADDRINUSE Port already taken by another process Change the port or stop the old process

    how to migrate node.js application between servers with docker

    Notice the pattern? Nearly every error above traces back to one root cause: an environment that isn’t identical between the old and new server. That’s why the recurring fix is always the same – package everything, then move.

    Final Thoughts

    The takeaway is embarrassingly simple: don’t move raw code, move a package. Whether it’s a Docker image or a PM2 snapshot, bundling everything together is what removes the variables – Node version, dependency tree, environment variables – that keep breaking migrations. And as long as you test before you cut over, the risk drops to almost nothing.

    FAQ About Node.js Server Migration

    Q: Does an image built on my old server always run on the new one?

    Only if the CPU architecture matches. amd64 to amd64, no problem – docker save and docker load are enough. If you’re crossing over to arm64, rebuild with buildx using multi-platform targets instead of transferring the old image.

    Q: I keep getting ECONNREFUSED to the database after migrating. What did I miss?

    Almost always one of three things: DB_HOST still points to the old address, the new server’s IP isn’t allowed in the database firewall, or the database service isn’t listening yet. Verify in order: service status, host value in .env, then firewall rules.

    Q: Is pm2 resurrect enough to fully move my application?

    No – it only restores the process list. The app folder, node_modules, and .env still have to move separately. Run pm2 resurrect after the code is in place and dependencies are installed.

    Q: Docker or PM2 – which one should my team standardize on?

    PM2 wins for simplicity on a single server and for quick moves. Docker wins when you need identical environments across many servers, automated deployments, or horizontal scaling. Many teams use Docker in production and keep PM2 for process management inside containers.

    Q: My native modules broke after the move. What’s the fastest fix?

    Native modules are compiled against a specific Node version. If you changed versions during the migration, delete node_modules and reinstall with npm ci. With Docker, just rebuild the image – the compile happens inside the container against the pinned runtime.

    Author: Syslog Solutions – NOC & Server Management Team. We handle 500+ servers daily, from shared hosting to enterprise dedicated infrastructure.

    Take it seriously. The next time someone asks you to ‘quickly move the app to the new server’, don’t wing it. Bundle it in Docker or snapshot it with PM2, test the package before you cut over, verify your environment variables, and check that database host. Do it right once, and you save yourself the 2 AM pager. I’ve seen this fail enough times to know the pattern – please don’t be the next ticket I triage.