hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Deploying Node.js Apps with Docker

Why Docker

  • The "Works on My Machine" Problem
  • Docker Concepts

Your First Dockerfile

  • Writing a Dockerfile
  • Multi-Stage Builds
  • The .dockerignore File

Running Containers

  • Running and Managing Containers
  • Environment Variables and Secrets
  • Health Checks

Multi-Container Apps

  • Docker Compose
  • Adding a Reverse Proxy
  • Persistent Data

Deploying to Production

  • Deploying to a VPS
  • HTTPS with Let's Encrypt
  • Zero-Downtime Deploys

CI/CD

  • Building Images in CI
  • Automated Deployment

Production Hardening

  • Container Security
  • Logging and Monitoring
  • Deployment Checklist and Capstone

Deployment Checklist and Capstone

What we built

A complete deployment pipeline that takes a Hectoday HTTP app from local development to a running production server:

StepWhat it doesTool
DockerfilePackage the app into an imageDocker
Multi-stage buildSeparate build and runtime, small imageDocker
.dockerignoreExclude unnecessary filesDocker
Environment variablesConfiguration without baking secretsdocker run -e
Health checksVerify the app is workingHEALTHCHECK
Docker ComposeMulti-container app (app + nginx + redis)docker-compose.yml
Nginx reverse proxySSL termination, request bufferingNginx
Persistent volumesDatabase and uploads survive restartsDocker volumes
VPS deploymentPull and run on a serverSSH + Docker
HTTPSLet’s Encrypt + Certbot + Nginx SSLCertbot
Zero-downtime deploysStart new, health check, swap, stop oldDeploy script
CI buildBuild and push image on every mergeGitHub Actions
Automated deployCI triggers deploy on the serverSSH from CI
Non-root userLimit damage from container compromiseDockerfile USER
Read-only filesystemPrevent writes outside volumes—read-only
Resource limitsPrevent CPU/memory exhaustion—cpus, —memory
Structured loggingJSON logs, rotation, monitoringconsole.log + Docker

The complete pipeline

Developer pushes to main
│
├─ GitHub Actions: Run tests
│   └─ Tests pass?
│       ├─ No → Pipeline stops. No deploy.
│       └─ Yes ↓
│
├─ GitHub Actions: Build Docker image
│   ├─ Multi-stage build (compile TypeScript)
│   ├─ Tag with :latest and :commit-sha
│   └─ Push to Docker Hub
│
├─ GitHub Actions: Deploy via SSH
│   ├─ SSH into the VPS
│   ├─ Pull the new image
│   ├─ docker compose up -d --no-deps app
│   └─ Verify health check
│
└─ App is live at https://yourdomain.com
    ├─ Nginx handles HTTPS (Let's Encrypt)
    ├─ App runs as non-root user
    ├─ Database persists in a Docker volume
    └─ Logs captured by Docker

Checklist

Dockerfile

  • Multi-stage build (build stage + production stage)
  • Production stage uses Alpine (small image)
  • Dependencies installed with npm ci --omit=dev
  • .dockerignore excludes node_modules, .git, .env, databases
  • CMD uses exec form (JSON array)
  • HEALTHCHECK instruction included

Security

  • Runs as non-root user (USER instruction)
  • Read-only filesystem (—read-only) with tmpfs for /tmp
  • Resource limits set (CPU and memory)
  • Secrets passed at runtime (-e or —env-file), never baked into image
  • No —privileged flag

Networking

  • App uses expose (internal only), not ports
  • Nginx is the only service with published ports (80, 443)
  • HTTPS enabled with Let’s Encrypt
  • HTTP redirects to HTTPS
  • Firewall allows only 22, 80, 443
  • Forwarded headers (X-Real-IP, X-Forwarded-For) configured

Data

  • Database in a named volume (persists across restarts)
  • Uploads in a named volume
  • Volume backup strategy in place
  • No application data stored inside the container

CI/CD

  • Tests run before build
  • Image built and pushed on every merge to main
  • Image tagged with :latest and :commit-sha
  • Deploy triggered automatically after successful build
  • Layer caching enabled for fast rebuilds

Operations

  • Structured logging (JSON to stdout)
  • Log rotation configured (max-size, max-file)
  • Health check monitored
  • Zero-downtime deploy script
  • Rollback procedure documented (pull previous SHA tag)
  • SSL certificate auto-renewal (cron job)

The files

project/
  src/                          # Application code
  Dockerfile                    # Multi-stage build
  .dockerignore                 # Exclude unnecessary files
  docker-compose.yml            # App + Nginx + Redis
  nginx.conf                    # Reverse proxy config
  .env.production               # Production env vars (not in git)
  deploy.sh                     # Zero-downtime deploy script
  .github/
    workflows/
      deploy.yml                # Test → Build → Deploy pipeline

Common mistakes

Not using .dockerignore. The build sends hundreds of MB of node_modules to Docker. Builds are slow and images are bloated.

Baking secrets into the image. ENV JWT_SECRET=... in the Dockerfile. Anyone with the image can read it.

No health checks. The container is “running” but the app is deadlocked. Docker does not know. Traffic keeps going to a dead app.

No log rotation. Logs grow until the disk is full. The database cannot write. The app crashes. Everything crashes.

Single-stage builds. The production image has TypeScript, dev dependencies, and build tools. 450 MB instead of 95 MB. Slower deploys, larger attack surface.

No resource limits. One container consumes all CPU and memory. Other containers starve. The host becomes unresponsive.

Using docker compose down/up for deploys. Downtime between stop and start. Use up -d --no-deps or the zero-downtime script.

Challenges

Challenge 1: Add a staging environment. Deploy to a separate staging server on pushes to a staging branch. Use a different domain and separate database.

Challenge 2: Add database backups. Write a cron job that backs up the SQLite database volume to object storage (S3/R2) daily. Test restoring from a backup.

Challenge 3: Add Prometheus + Grafana. Add Prometheus and Grafana containers to your compose file. Expose app metrics at /metrics. Build a dashboard showing request rate, response time, and memory usage.

Challenge 4: Migrate to PostgreSQL. Replace SQLite with a PostgreSQL container. Update the compose file, add a PostgreSQL volume, and update the app to use pg instead of better-sqlite3.

What is the most important step in the deployment pipeline?

What should you do if a deploy fails the health check?

← Logging and Monitoring Back to course →

© 2026 hectoday. All rights reserved.