Le Do Nghiem

Le Do Nghiem

AI Engineer

About meBooksSnippetsBlog

© 2026 Le Do Nghiem. All rights reserved.

Contact |

Back to Blog

Deploying Apps with Docker and DigitalOcean

Le Do Nghiem
Le Do NghiemAI Engineer
2025-09-10 4 min read
Share

From SSH deploys to immutable containers

I used to deploy by SSH-ing into a server, pulling git, running npm install, and praying. It worked until it did not — Node version drift, "works on my machine," midnight rollbacks.

Docker did not solve everything. It froze my app environment so deploy meant: build image, push, run container. Repeatable.

This post is the path I still use for side projects: Dockerfile → registry → DigitalOcean droplet. For how I think about backend architecture around this, see how I build scalable web apps.


Step 1 — Dockerfile that matches your app

What: A recipe for the runtime image.

Why: Same layers locally and in production. No surprise missing dependencies.

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

What I also add:

  • .dockerignore with node_modules, .git, .env.local — smaller context, faster builds
  • Multi-stage builds for production Next.js apps when image size matters (build stage + slim run stage)

Red flag: Copying .env or secrets into the image. Use droplet env vars or a secrets manager at runtime. Baked secrets in layers are a leak waiting to happen.


Step 2 — Build and push to a registry

What: docker build locally or in CI, then docker push to Docker Hub or DigitalOcean Container Registry.

Why: The server pulls a known artifact. You are not compiling on the droplet under load.

docker build -t my-app:latest .
docker tag my-app:latest youruser/my-app:latest
docker push youruser/my-app:latest

I tag with git SHA for anything I might need to roll back — my-app:abc1234, not only latest.


Step 3 — Droplet and run

What: A small VM on DigitalOcean with Docker installed.

Why: Full control, predictable bill, fine for portfolios and early products.

On the droplet:

docker pull youruser/my-app:latest
docker run -d \
  --name my-app \
  -p 80:3000 \
  --restart unless-stopped \
  -e NODE_ENV=production \
  youruser/my-app:latest

Port mapping: -p 80:3000 means host port 80 → container port 3000. I have lost hours mapping 3000:3000 while nginx already owned 80. Draw the map on paper once.

Red flag: Running as root inside the container without thinking. For hobby projects I keep it simple; for anything serious I use a non-root user in the Dockerfile.


What I check before calling it live

CheckWhy
Health endpoint or curl returns 200Container is up, not just "running"
Env vars set on docker runDB URLs, API keys not in the image
Logs: docker logs -f my-appCatch crash loops early
Firewall: only 80/443 publicSSH restricted if possible
HTTPS via Caddy, nginx, or DO load balancerUsers and cookies expect TLS

CI/CD (when I stop deploying by hand)

Manual docker run is fine for two apps. After that I wire GitHub Actions: build on push to main, push image, SSH or API deploy to pull and restart.

Why: Same artifact every time. Deploy becomes a button, not a ritual.


Deploy war stories

  • Giant images — forgot .dockerignore; every build uploaded node_modules from my laptop.
  • Wrong Node version in Dockerfile — Alpine + native modules sometimes need build tools. Read the error log; do not random-install python.
  • No restart policy — droplet rebooted, container stayed stopped. --restart unless-stopped is default for me now.
  • Database on the same droplet without backups — Docker does not backup Postgres for you.

When the container is live

Docker + a small droplet is still my fastest path to "real URL, real users" for side projects. It is not Kubernetes. It does not need to be.

If you outgrow one box, the same image goes to a bigger droplet, a managed app platform, or a proper orchestrator. The habit that matters is immutable deploys: build once, run that artifact everywhere.

Next read if you are building the app inside the container: how I structure scalable web apps.

On this page

  • From SSH deploys to immutable containers
  • Step 1 — Dockerfile that matches your app
  • Step 2 — Build and push to a registry
  • Step 3 — Droplet and run
  • What I check before calling it live
  • CI/CD (when I stop deploying by hand)
  • Deploy war stories
  • When the container is live
Share
Previous Post

How I Build Scalable Web Apps

Next Post

Dependency Injection in ASP.NET Core