{"id":979,"date":"2026-08-26T22:04:35","date_gmt":"2026-08-26T22:04:35","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=979"},"modified":"2026-08-26T22:04:35","modified_gmt":"2026-08-26T22:04:35","slug":"containerizing-nodejs-app-docker-vps","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/","title":{"rendered":"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial"},"content":{"rendered":"<p class=\"wp-block-paragraph\">Running a Node.js application directly on a VPS works \u2014 until you need to manage dependencies, runtime versions, environment variables, and process restart behavior across deploys. Docker solves all of these problems by packaging your application, its dependencies, and its runtime into a single portable container. This tutorial walks through containerizing a Node.js application, deploying it to a VPS, and setting up production-grade process management with Docker Compose.<\/p>\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n<ul class=\"wp-block-list\"><li>A Linux VPS with Ubuntu 22.04 or 24.04 (2 GB RAM minimum)<\/li><li>Docker and Docker Compose installed<\/li><li>A Node.js application (Express, Fastify, Koa, or any framework \u2014 we&#8217;ll use a simple Express app as the example)<\/li><li>Git access to your application repository<\/li><\/ul>\n\n<p class=\"wp-block-paragraph\">If you need a VPS to deploy this on, <a href=\"https:\/\/virtualserversvps.com\/\">check our VPS comparison table<\/a> to find a provider with Docker-friendly plans and fast NVMe storage.<\/p>\n\n<h2 class=\"wp-block-heading\">Step 1: Install Docker on Your VPS<\/h2>\n\n<p class=\"wp-block-paragraph\">If Docker is not already installed, use the official convenience script:<\/p>\n\n<pre class=\"wp-block-code\"><code># Install Docker using the official script\ncurl -fsSL https:\/\/get.docker.com -o get-docker.sh\nsudo sh get-docker.sh\n\n# Add your user to the docker group (so you don't need sudo for every command)\nsudo usermod -aG docker $USER\n\n# Log out and back in, or run:\nnewgrp docker\n\n# Verify installation\ndocker --version\n# Expected: Docker version 27.x.x<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Install Docker Compose (if not included with your Docker installation):<\/p>\n\n<pre class=\"wp-block-code\"><code>sudo apt install docker-compose-plugin -y\n# Or download the standalone binary:\n# sudo curl -SL \"https:\/\/github.com\/docker\/compose\/releases\/latest\/download\/docker-compose-$(uname -s)-$(uname -m)\" -o \/usr\/local\/bin\/docker-compose\n# sudo chmod +x \/usr\/local\/bin\/docker-compose<\/code><\/pre>\n\n<h2 class=\"wp-block-heading\">Step 2: Create a Dockerfile for Your Node.js App<\/h2>\n\n<p class=\"wp-block-paragraph\">A <code>Dockerfile<\/code> defines how your application is built and run. Create this file in the root of your Node.js project:<\/p>\n\n<pre class=\"wp-block-code\"><code># Use the official Node.js 20 LTS image as the base\nFROM node:20-alpine\n\n# Set the working directory inside the container\nWORKDIR \/app\n\n# Copy package.json and package-lock.json first (for better layer caching)\nCOPY package*.json .\/\n\n# Install production dependencies only\nRUN npm ci --only=production &amp;&amp; npm cache clean --force\n\n# Copy the rest of the application source code\nCOPY . .\n\n# Create a non-root user for security\nRUN addgroup -S appgroup &amp;&amp; adduser -S appuser -G appgroup\nUSER appuser\n\n# Expose the port your app listens on\nEXPOSE 3000\n\n# Start the application\nCMD [\"node\", \"src\/index.js\"]<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Key points about this Dockerfile:<\/p>\n\n<ul class=\"wp-block-list\"><li><strong>FROM node:20-alpine<\/strong> \u2014 Alpine-based images are ~50 MB smaller than full Debian images, reducing download and deployment time.<\/li><li><strong>COPY package*.json first<\/strong> \u2014 Docker caches layers. If you change source code but not dependencies, Docker reuses the cached <code>npm ci<\/code> layer.<\/li><li><strong>npm ci<\/strong> \u2014 Uses <code>package-lock.json<\/code> for deterministic installs. Faster than <code>npm install<\/code> in CI\/CD.<\/li><li><strong>Non-root user<\/strong> \u2014 Running as a non-root user inside the container is a security best practice.<\/li><\/ul>\n\n<h2 class=\"wp-block-heading\">Step 3: Create a .dockerignore File<\/h2>\n\n<p class=\"wp-block-paragraph\">Create a <code>.dockerignore<\/code> file in your project root to exclude unnecessary files from the Docker build context:<\/p>\n\n<pre class=\"wp-block-code\"><code>node_modules\nnpm-debug.log\n.git\n.gitignore\n.env\n.env.*\nDockerfile\n.dockerignore\nREADME.md<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">This prevents <code>node_modules<\/code> (which will be rebuilt inside the container) and sensitive files like <code>.env<\/code> from being copied into the Docker image.<\/p>\n\n<h2 class=\"wp-block-heading\">Step 4: Create a Docker Compose File<\/h2>\n\n<p class=\"wp-block-paragraph\">Docker Compose orchestrates multi-container setups. Create a <code>docker-compose.yml<\/code> file:<\/p>\n\n<pre class=\"wp-block-code\"><code>version: '3.8'\n\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    container_name: my-node-app\n    ports:\n      - \"3000:3000\"\n    environment:\n      - NODE_ENV=production\n      - PORT=3000\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"wget\", \"--no-verbose\", \"--tries=1\", \"--spider\", \"http:\/\/localhost:3000\/health\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 10s\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"10m\"\n        max-file: \"3\"<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Key configuration points:<\/p>\n\n<ul class=\"wp-block-list\"><li><strong>restart: unless-stopped<\/strong> \u2014 Automatically restarts the container if it crashes or the VPS reboots.<\/li><li><strong>healthcheck<\/strong> \u2014 Docker periodically checks if the app is responding. Unhealthy containers can be replaced automatically.<\/li><li><strong>logging<\/strong> \u2014 Limits log file size to prevent disk exhaustion. A common issue on small VPS disks.<\/li><\/ul>\n\n<h2 class=\"wp-block-heading\">Step 5: Build and Run the Container<\/h2>\n\n<pre class=\"wp-block-code\"><code># Build the Docker image\ncd \/path\/to\/your\/nodejs-app\ndocker compose build\n\n# Start the container in detached mode\ndocker compose up -d\n\n# Check container status\ndocker compose ps\n\n# View logs\ndocker compose logs -f<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Your application should now be accessible at <code>http:\/\/your-vps-ip:3000<\/code>. If you have a reverse proxy (Nginx or Caddy) already running, configure it to proxy requests to <code>localhost:3000<\/code>.<\/p>\n\n<h2 class=\"wp-block-heading\">Step 6: Add a Reverse Proxy (Nginx + Let&#8217;s Encrypt)<\/h2>\n\n<p class=\"wp-block-paragraph\">For production, you should serve your application through a reverse proxy with TLS. Add a reverse proxy service to your <code>docker-compose.yml<\/code>:<\/p>\n\n<pre class=\"wp-block-code\"><code>services:\n  app:\n    # ... (same as above)\n\n  nginx:\n    image: nginx:alpine\n    container_name: nginx-proxy\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - .\/nginx.conf:\/etc\/nginx\/nginx.conf:ro\n      - .\/ssl:\/etc\/nginx\/ssl:ro\n      - certbot-www:\/var\/www\/certbot\n    depends_on:\n      - app\n    restart: unless-stopped\n\n  certbot:\n    image: certbot\/certbot\n    container_name: certbot\n    volumes:\n      - .\/ssl:\/etc\/letsencrypt\n      - certbot-www:\/var\/www\/certbot\n    entrypoint: \"\/bin\/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h; done'\"\n\nvolumes:\n  certbot-www:<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">For a simpler approach, use Caddy, which handles TLS automatically:<\/p>\n\n<pre class=\"wp-block-code\"><code>services:\n  app:\n    # ... (same as above)\n\n  caddy:\n    image: caddy:alpine\n    container_name: caddy-proxy\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - .\/Caddyfile:\/etc\/caddy\/Caddyfile\n      - caddy-data:\/data\n    restart: unless-stopped\n\nvolumes:\n  caddy-data:<\/code><\/pre>\n\n<h2 class=\"wp-block-heading\">Step 7: Manage Environment Variables<\/h2>\n\n<p class=\"wp-block-paragraph\">Never hardcode secrets in your Dockerfile. Create a <code>.env<\/code> file in your project root (add it to <code>.gitignore<\/code>!):<\/p>\n\n<pre class=\"wp-block-code\"><code># .env file\nNODE_ENV=production\nPORT=3000\nDATABASE_URL=postgresql:\/\/user:password@db:5432\/myapp\nREDIS_URL=redis:\/\/redis:6379\nJWT_SECRET=your-production-secret-here<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Docker Compose automatically reads the <code>.env<\/code> file and passes the variables to the container. For production secrets, consider using Docker secrets or a vault solution.<\/p>\n\n<h2 class=\"wp-block-heading\">Step 8: Deploy Updates<\/h2>\n\n<p class=\"wp-block-paragraph\">When you push new code to your repository, deploy the update on your VPS:<\/p>\n\n<pre class=\"wp-block-code\"><code># Pull the latest code\ncd \/path\/to\/your\/nodejs-app\ngit pull origin main\n\n# Rebuild and restart with zero downtime (if using --scale)\ndocker compose build\ndocker compose up -d --no-deps\n\n# Remove old images to free disk space\ndocker image prune -f<\/code><\/pre>\n\n<h2 class=\"wp-block-heading\">Step 9: Monitor and Troubleshoot<\/h2>\n\n<p class=\"wp-block-paragraph\">Essential Docker commands for ongoing management:<\/p>\n\n<pre class=\"wp-block-code\"><code># Check resource usage\ndocker stats\n\n# View real-time logs\ndocker compose logs -f --tail=50\n\n# Execute commands inside the container\ndocker exec -it my-node-app sh\n\n# Check disk usage of Docker\ndocker system df<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Common issues and fixes:<\/p>\n\n<ul class=\"wp-block-list\"><li><strong>Container exits immediately:<\/strong> Run <code>docker compose logs<\/code> to see the error. Usually a missing <code>package.json<\/code> or a syntax error in the app.<\/li><li><strong>Port already in use:<\/strong> Something else is listening on port 3000. Stop the other process or change the port mapping.<\/li><li><strong>Out of disk space:<\/strong> Run <code>docker system prune -a<\/code> to remove unused images, containers, and build cache.<\/li><li><strong>Permissions denied:<\/strong> Ensure your user is in the <code>docker<\/code> group. Check with <code>groups $USER<\/code>.<\/li><\/ul>\n\n<h2 class=\"wp-block-heading\">Production Checklist<\/h2>\n\n<ul class=\"wp-block-list\"><li>Use a non-root user inside the container<\/li><li>Set memory limits: <code>deploy:\n  resources:\n    limits:\n      memory: 512M<\/code><\/li><li>Enable Docker&#8217;s restart policy: <code>restart: unless-stopped<\/code><\/li><li>Configure log rotation to prevent disk exhaustion<\/li><li>Use a reverse proxy with TLS (Nginx, Caddy, or Traefik)<\/li><li>Set up a health check endpoint in your application<\/li><li>Run your database in a separate container or use a managed database provider<\/li><\/ul>\n\n<p class=\"wp-block-paragraph\">Containerizing your Node.js application with Docker on a VPS gives you reproducible deployments, easy rollbacks, and consistent environments across development and production. For more VPS deployment guides and provider recommendations, <a href=\"https:\/\/virtualserversvps.com\/\">visit the main site<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Running a Node.js application directly on a VPS works \u2014 until you need to manage dependencies, runtime versions, environment variables, and process restart behavior across deploys. Docker solves all of&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":0,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-979","post","type-post","status-publish","format-standard","hentry","category-vps-guides-tutorials"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial - Virtual Servers VPS Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial\" \/>\n<meta property=\"og:description\" content=\"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-26T22:04:35+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/\",\"name\":\"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-08-26T22:04:35+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial - Virtual Servers VPS Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/","og_locale":"en_US","og_type":"article","og_title":"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial","og_description":"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial","og_url":"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-08-26T22:04:35+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/","url":"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/","name":"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-08-26T22:04:35+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/containerizing-nodejs-app-docker-vps\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Containerizing a Node.js Application with Docker on a VPS: Step-by-Step Tutorial"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/979","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=979"}],"version-history":[{"count":1,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/979\/revisions"}],"predecessor-version":[{"id":980,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/979\/revisions\/980"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=979"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=979"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=979"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}