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 containerCOPY package*.json ./— copypackage.jsonandpackage-lock.json(for dependency installation)RUN npm ci --only=production— install dependencies (no dev dependencies)COPY . .— copy the rest of the application codeEXPOSE 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-appOpen 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=productionOr 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:
[](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 loginCreate a project:
panda projects create \
--name my-docker-app \
--repo yourorg/my-app \
--branch main \
--auto-deployThe 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-alpineUsing 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 clean3. Use .dockerignore
Exclude files that do not belong in the image:
# .dockerignore
node_modules
.git
.env
*.logThis 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.comPandaStack 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 abc123The 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)