Back to Blog
Tutorial12 min read2026-08-14

Docker Deployment Guide for Beginners (The Reference for Someone Who Has Only Run docker run Locally)

Deploy a Docker container from a Git repo — Dockerfile basics, environment variables, port binding, health checks, and getting a live HTTPS URL.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Running docker run locally works, but deploying to production requires understanding how images are built, how environment variables are injected, how ports are bound, and how the platform routes traffic to your container.

The Dockerfile

A Dockerfile is a recipe for building an image. Each instruction creates a layer in the image.

Minimal Node.js Dockerfile

FROM node:20-alpine

WORKDIR /app

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

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

Explanation:

  • FROM node:20-alpine — start from the official Node.js 20 Alpine Linux base image (small, secure)
  • WORKDIR /app — set the working directory inside the container
  • COPY package*.json ./ — copy package.json and package-lock.json (for dependency installation)
  • RUN npm ci --only=production — install dependencies (no dev dependencies)
  • COPY . . — copy the rest of the application code
  • EXPOSE 3000 — document that the app listens on port 3000 (informational, not enforced)
  • CMD ["node", "server.js"] — the command to run when the container starts

Build it locally:

docker build -t my-app .

Run it:

docker run -p 3000:3000 my-app

Open http://localhost:3000 to verify it works.

Bind to 0.0.0.0, not localhost

Your app must bind to all interfaces (0.0.0.0), not just localhost (127.0.0.1). The platform's load balancer cannot reach 127.0.0.1 from outside the container.

// server.js
const express = require('express');
const app = express();

const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({ status: 'running' });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server listening on port ${PORT}`);
});

The '0.0.0.0' argument is critical. Omit it and the container starts but never answers health checks.

Environment variables

Never hard-code secrets in your Dockerfile. Inject them at runtime:

# Bad: secrets in the image
ENV DATABASE_URL=postgresql://user:pass@host/db

# Good: no secrets, read from process.env
CMD ["node", "server.js"]
// server.js
const dbUrl = process.env.DATABASE_URL;

Set environment variables via the CLI:

panda projects env set my-docker-app DATABASE_URL=postgresql://user:pass@host/db
panda projects env set my-docker-app NODE_ENV=production

Or declare them in pandastack.json:

{
  "type": "container",
  "language": "docker",
  "env": [
    { "name": "DATABASE_URL", "description": "PostgreSQL connection string" },
    { "name": "NODE_ENV", "description": "Node environment" }
  ]
}

When someone deploys via the deploy button, the platform prompts for these values.

Deploy via the deploy button

Add a deploy button to your README.md:

[![Deploy to PandaStack](https://dashboard.pandastack.io/deploy-button.svg)](https://dashboard.pandastack.io/deploy?repo=yourorg/my-app&type=container&lang=docker)

Click the button. PandaStack clones the repo, detects the Dockerfile, builds it with rootless BuildKit, pushes the image to Google Artifact Registry, and deploys it via Helm. You get a live HTTPS URL like https://my-app-abc123.pandastack.app.

Health checks

The platform probes your app to verify it is ready to receive traffic. By default, it checks GET /. If your app does not expose a root route, add a health check endpoint:

app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

Configure the health check path in pandastack.json:

{
  "type": "container",
  "healthCheckPath": "/health"
}

If the endpoint returns 5xx or times out after 10 seconds, the platform marks the pod unhealthy and restarts it.

Deploy via the CLI

Install the PandaStack CLI:

npm install -g @pandastack/cli
panda login

Create a project:

panda projects create \
  --name my-docker-app \
  --repo yourorg/my-app \
  --branch main \
  --auto-deploy

The platform clones the repo, builds the image, and deploys it. Watch logs in the dashboard under Logs.

Deploy via the REST API

For CI pipelines, trigger a deploy with a psk_ token:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "my-docker-app",
    "repositoryName": "yourorg/my-app",
    "branch": "main",
    "autoDeploy": true,
    "env": [
      { "name": "DATABASE_URL", "value": "postgresql://user:pass@host/db" },
      { "name": "NODE_ENV", "value": "production" }
    ]
  }'

The API returns a deploymentId. Poll the status via GET /v1/projects/{id}/deployments/{deploymentId}.

Dockerfile best practices

1. Use specific base image tags

# Bad: version changes unexpectedly
FROM node:latest

# Good: pinned to a specific version
FROM node:20-alpine

Using latest means the base image can change under you, breaking builds.

2. Minimize layers

Each RUN, COPY, and ADD creates a layer. Combine commands to reduce layers:

# Bad: multiple RUN commands
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# Good: single RUN command
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get clean

3. Use .dockerignore

Exclude files that do not belong in the image:

# .dockerignore
node_modules
.git
.env
*.log

This speeds up builds and reduces image size.

4. Run as a non-root user

Containers should not run as root. Create a user:

FROM node:20-alpine

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

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

COPY . .

USER appuser

CMD ["node", "server.js"]

This limits the damage if the container is compromised.

Add a custom domain

In the dashboard, navigate to Project Settings → Domains. Add your domain (e.g., api.yourdomain.com). PandaStack provides a CNAME target like my-app.pandastack-dns.com.

Point your DNS:

api.yourdomain.com  CNAME  my-app.pandastack-dns.com

PandaStack provisions a Let's Encrypt certificate automatically. HTTPS works within minutes of DNS propagation.

Rollback

If a deploy breaks your app, roll back via the dashboard (Deployments tab → Rollback) or the CLI:

panda projects deploy my-docker-app --deployment-id abc123

The platform redeploys that exact commit and environment variable snapshot.

What you deployed

  • A containerized app built from a Dockerfile
  • Automatic HTTPS with a custom domain
  • Zero-downtime rolling deploys (new pods start, health checks pass, traffic shifts)
  • Environment variables injected at runtime (not baked into the image)

The free tier includes 5 container apps, 100 GB bandwidth/month, and 300 build minutes/month. Free-tier apps scale to zero when idle (cold start on next request). Upgrade to Pro ($15/mo) for stable nodes with no cold starts.

Start deploying at https://dashboard.pandastack.io.

References

  • [Dockerfile reference](https://docs.docker.com/engine/reference/builder/)
  • [Docker best practices](https://docs.docker.com/develop/dev-best-practices/)
  • [PandaStack container docs](https://docs.pandastack.io)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also