Docker solved the "works on my machine" problem. Your application and all its dependencies are packaged into a single, portable image that runs identically across any environment. The harder part has always been what comes next: getting that container into production without spending days configuring servers, load balancers, TLS certificates, and container orchestration.
This guide shows you how to deploy a Docker application to a live production URL in under five minutes. We cover writing a production-ready Dockerfile for Node.js, Python, and Go, then walk through every step of the deployment process — from connecting your GitHub repository to accessing your app over HTTPS.
What You'll Learn
- How to write production-ready Dockerfiles for Node.js, Python, and Go
- Multi-stage build patterns that keep image sizes small and deploys fast
- How to configure and deploy a Docker application through the Out Plane console
- How to set environment variables, configure scaling, and map a custom domain
- Common Docker production deployment issues and how to resolve them
Prerequisites
Before starting, make sure you have:
- A GitHub account with your application code in a repository
- A working Dockerfile in your project root (or a monorepo with a Dockerfile in a subdirectory)
- An Out Plane account — free to create at console.outplane.com
Out Plane uses GitHub OAuth for authentication. No CLI tool is required. Everything in this guide is done through the web console.
Writing a Production-Ready Dockerfile
A production Dockerfile is not the same as a development one. It needs to produce a small, secure image that starts quickly and handles signals cleanly. The patterns below apply regardless of which language you use.
Node.js Application
This multi-stage Dockerfile separates the build environment from the runtime image. The final image contains only the compiled output and production dependencies — no build tools, no source files.
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./
USER appuser
EXPOSE 8080
CMD ["node", "dist/index.js"]Create a .dockerignore file at your project root to prevent unnecessary files from being copied into the build context:
node_modules
.git
.env
.env.*
*.log
dist
coverage
.nextPython Application
For Python, the slim base image removes the build toolchain while keeping the interpreter. Gunicorn handles production traffic; Flask's built-in server is not suitable for production.
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first to leverage layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --ingroup appgroup appuser
USER appuser
EXPOSE 8080
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8080", "--workers", "4", "--timeout", "120"]Your requirements.txt must include gunicorn:
flask==3.1.0
gunicorn==22.0.0Go Application
Go compiles to a single static binary, which means the runtime image can be built from scratch — no operating system, no shell, nothing except the binary itself. This produces images under 15MB.
# Stage 1: Build
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
# Stage 2: Minimal runtime
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
CMD ["/server"]Note: the scratch base image has no shell. If you need a shell for debugging, use alpine:latest as the runtime base instead.
Dockerfile Best Practices for Production
Regardless of the language, these practices apply to every production Dockerfile:
- Use multi-stage builds. Separate the build environment from the runtime image. A Node.js build stage with dev dependencies can exceed 800MB; the final runtime image should be under 100MB.
- Run as a non-root user. Container escape vulnerabilities are significantly harder to exploit when the process runs as an unprivileged user. Always create a system user and switch to it before the
CMDinstruction. - Pin specific version tags. Never use
:latest. Pin to a specific version likenode:20-alpineto ensure reproducible builds. Base images update without warning, and:latestcan silently break your application. - Copy dependency files first. Copy
package.json,requirements.txt, orgo.modbefore copying the rest of your source code. Docker caches each layer; this pattern means dependency installation is only re-run when dependencies actually change. - Use a
.dockerignorefile. Excludingnode_modules,.git, and local.envfiles keeps build context small and prevents secrets from reaching the image. - Use the exec form of CMD. Write
CMD ["node", "server.js"]instead ofCMD node server.js. The exec form passes signals directly to your process, enabling graceful shutdown. - EXPOSE the correct port. The
EXPOSEdirective documents which port your application listens on. You will need this value during the deployment configuration step.
Step 1: Prepare Your Repository
Before deploying, verify your repository is ready:
- Dockerfile is in the right location. For a standard project, the Dockerfile should be at the repository root. For a monorepo, it can be in a subdirectory — you will set the root directory in the deployment configuration.
.dockerignoreis committed. This prevents secrets and large directories from inflating your build context.- The correct port is EXPOSEd. Note the port number — you will enter it during configuration. The default on Out Plane is
8080. - Your Dockerfile builds locally. Run
docker build -t my-app .anddocker run -p 8080:8080 my-appto verify the image works before deploying.
Push your code to GitHub. Out Plane deploys directly from your repository on every push to the configured branch.
Step 2: Connect Your Repository
- Go to console.outplane.com
- Sign in with your GitHub account
- Click New Application
- Select your repository from the list
Out Plane reads your repository through the GitHub OAuth connection you authorized during sign-in. If your repository does not appear in the list, verify it is accessible under your GitHub account or organization.
Step 3: Configure Your Application
The create application form gives you control over how your Docker application is built and run.
Build Method
Set Build Method to Dockerfile. This instructs Out Plane to locate your Dockerfile and build the image directly, rather than using Paketo Buildpacks auto-detection.
If your Dockerfile is in a subdirectory (monorepo), set the Root Directory field to the path of that subdirectory — for example, services/api. The build context will be relative to that directory.
Port
Enter the port your application listens on. This must match the EXPOSE directive in your Dockerfile. The default is 8080. If your application uses a different port — for example, a Next.js app on 3000 — enter that value here.
Branch
Select the branch Out Plane should deploy from. The default is main or master. You can change this to any branch in your repository — useful for deploying a staging environment from a staging branch.
Scaling
Configure minimum and maximum instances:
- Min instances: The number of instances that remain running at all times. Set to
1for continuous availability. Set to0to scale to zero when the application receives no traffic (cold start applies). - Max instances: The upper limit for auto-scaling under load. Out Plane scales up automatically when CPU or memory pressure increases.
Instance Type
Out Plane instance types range from op-20 (0.5 vCPU, 512MB RAM) to op-94 (32 vCPU, 64GB RAM). For most web applications, op-20 or op-21 (1 vCPU, 1GB RAM) is sufficient to start. You can change the instance type at any time without redeploying.
Region
The default deployment region is Nuremberg. Enterprise regions are available if you need deployment closer to specific geographic markets. Choose the region nearest to the majority of your users.
Environment Variables
Add any configuration your application reads from the environment. Click Add to enter variables one at a time, or click Raw Edit to paste multiple variables at once in KEY=VALUE format.
Common variables for Docker applications:
DATABASE_URL=postgres://user:password@host:5432/database
API_SECRET=your-secret-key
NODE_ENV=productionNever bake secrets into your Docker image. Environment variables set in the console are injected at runtime and are never stored in the image layer history.
Step 4: Deploy
Click Deploy Application. Out Plane queues the build and you can watch the status progress in real time:
- Queued — The build is waiting for an available build runner
- Building — Out Plane is executing your Dockerfile and building the image
- Deploying — The built image is being started on the selected instance type
- Ready — Your application is live
The build log streams in real time during the Building phase. If the build fails, the log shows the exact Dockerfile instruction that failed and the error output.
Once the status shows Ready, your application URL appears at the top of the deployment page. It follows the format your-app.outplane.app. Click it to open your application in a browser. HTTPS is configured automatically — no certificate management required.
Every push to your configured branch triggers a new deployment automatically.
Step 5: Configure a Custom Domain
The default .outplane.app URL is ready for production, but most applications need a branded domain. To map your own domain:
- Navigate to Domains in the sidebar
- Click Map Domain
- Enter your domain name (for example,
app.yourdomain.com)
Out Plane provides the DNS records you need to add at your domain registrar:
- Subdomain (CNAME): Point
app.yourdomain.comtodomains-management.outplane.app - Apex domain (A record): Point
yourdomain.comto49.13.46.108
Once DNS propagates — typically within a few minutes, up to 24 hours for some registrars — SSL is provisioned automatically via Let's Encrypt. No manual certificate renewal is required.
Common Issues and Solutions
Port Mismatch
Symptom: The deployment shows Ready but the application returns a connection error or 502.
Cause: The port configured in Out Plane does not match the port your application actually listens on.
Fix: Check your Dockerfile's EXPOSE directive and your application code. If your app listens on 3000 but you configured 8080 in Out Plane, update the port in your application settings and redeploy.
Build Failures from Missing Dependencies
Symptom: The build log shows a module not found or dependency installation error.
Cause: System-level build dependencies (such as gcc, libpq-dev, or python3-dev) are not available in the base image.
Fix: Add the necessary system packages before your dependency installation step. For Alpine-based images, use apk add --no-cache. For Debian-based images, use apt-get install -y --no-install-recommends.
# Alpine example
RUN apk add --no-cache gcc musl-dev
# Debian example
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev && rm -rf /var/lib/apt/lists/*Permission Errors at Runtime
Symptom: The application starts but crashes with EACCES or Permission denied errors when trying to write to the filesystem.
Cause: The application was written assuming it would run as root. Files in the image are owned by root, but the process runs as an unprivileged user.
Fix: Ensure any directories your application writes to are owned by the non-root user. Set ownership during the build stage:
RUN mkdir -p /app/uploads && chown appuser:appgroup /app/uploads
USER appuserFor applications that genuinely need to write to the filesystem, prefer writing to /tmp (which is world-writable) or using external storage like an S3-compatible object store.
Large Image Sizes Slowing Deploys
Symptom: The Building phase takes several minutes. Logs show the image push step is slow.
Cause: The image includes development dependencies, build artifacts, or large assets that are not needed at runtime.
Fix: Audit your Dockerfile for multi-stage build opportunities. Common culprits:
node_modulesfrom a development install (usenpm ci --only=productionin the final stage)- Build tools like
gcc,make, orcmakethat are only needed to compile dependencies - Test fixtures, documentation, or
.githistory copied into the image
A well-optimized Node.js image should be under 200MB. A Go binary in scratch should be under 20MB.
Next Steps
Your Docker application is now running in production with automatic HTTPS, per-second billing, and auto-scaling. Here's what to explore next:
- Deploy a specific framework: See How to Deploy a Next.js Application for a framework-specific guide with SSR configuration
- Zero-downtime deployments: Learn how to configure health checks and rolling updates in the Zero-Downtime Deployment Guide
- Add a database: Out Plane provides managed PostgreSQL (versions 14–18). Navigate to Databases in the sidebar to provision one and connect it via a
DATABASE_URLenvironment variable - Adjust scaling: Start with a small instance type and scale up based on the metrics shown in your application dashboard
Summary
Deploying a Docker application to Out Plane comes down to five steps:
- Write a production-ready Dockerfile with multi-stage builds and a non-root user
- Prepare your repository with a
.dockerignoreand verify the build runs locally - Connect your GitHub repository at console.outplane.com
- Configure the build method as Dockerfile, set the correct port, and add environment variables
- Deploy and access your application at
your-app.outplane.appwith HTTPS
No server provisioning, no certificate management, no infrastructure to maintain. Push your Dockerfile and get a production URL.
Ready to deploy your Docker application? Get started with Out Plane and receive $20 in free credit. Pay only for what you use with per-second billing.