Shipping a small app with Docker does not need a complex platform or a fragile pile of shell commands. What most teams need is a repeatable production baseline: a clean image, a predictable server, a safe way to pass secrets, a reverse proxy, health checks, logs, backups, and a deployment routine that is boring in the best way. This guide gives you a reusable checklist for deploying Docker apps to a server, with scenario-based advice for single-container apps, Docker Compose production setups, and lightweight CI/CD workflows you can keep using as your app grows.
Overview
This docker deployment tutorial is built for small production apps: internal tools, SaaS MVPs, admin panels, APIs, background workers, and content sites that do not need a full orchestration platform yet. The goal is not to cover every Docker feature. It is to help you deploy a Docker app to a server in a way that is easy to reason about, easy to troubleshoot, and easy to revisit later.
A practical small app deployment usually includes these layers:
- The app image: built once, tagged clearly, and kept as small as practical.
- The server: a Linux host with Docker installed, basic firewall rules, and enough disk and memory for the workload.
- Runtime configuration: environment variables, secrets, mounted volumes, network settings, and restart policies.
- Ingress: an Nginx reverse proxy setup or another edge proxy to handle TLS and route traffic to containers.
- Operations basics: health checks, logs, backups, monitoring, and a rollback path.
If you are deciding how much process to add, use this rule of thumb: prefer the simplest deployment that still gives you safe restarts, a clear failure signal, and a documented way to recover. That usually means Docker Compose production for small apps, not hand-running long docker run commands and not jumping too early to a heavier orchestrator.
Before you begin, define four things in writing:
- What will be deployed? One web app, or a web app plus database, cache, worker, and cron service.
- Where will it run? A single VPS, a cloud VM, or a managed host with Docker support.
- How will traffic reach it? Through a domain, subdomain, load balancer, or direct IP.
- How will updates happen? Manual SSH deployment, a script, or a GitHub Actions deployment workflow.
That small amount of clarity prevents many production problems later.
Checklist by scenario
Use the scenario below that matches your app. Each checklist is designed to be practical, not exhaustive.
Scenario 1: Single container web app on one server
This is the simplest useful docker production setup. It works well for a stateless app that talks to an external database or managed service.
- Build a production image: Use a stable base image, pin major versions where practical, and avoid development-only tools in the final image.
- Create a non-root runtime where possible: If your app does not need elevated privileges, run as a non-root user inside the container.
- Expose only the required internal port: Keep the app listening on one known port, such as 3000 or 8080.
- Set environment variables deliberately: Separate required settings from optional ones. Document defaults.
- Add a health check: If the app can expose a simple health endpoint, do it. This helps during restarts and troubleshooting.
- Use restart policies: Configure the container to restart after failure or server reboot.
- Put Nginx in front: Terminate TLS and route external traffic through a reverse proxy instead of publishing the app container directly.
- Persist only what needs persistence: If the app writes uploads or local state, mount a volume and back it up.
- Capture logs: Decide whether you will rely on Docker logs, log files, or shipping logs elsewhere.
- Test a restart before go-live: Reboot the server or restart Docker and confirm the app returns cleanly.
This setup is often enough for small app deployment if the app itself is stateless and your data services are managed elsewhere.
Scenario 2: Docker Compose production for app plus supporting services
If your app needs a database, Redis, worker process, or scheduler, Docker Compose is often the right middle ground. It gives you repeatability without introducing orchestration complexity too early.
- Split services clearly: Define separate services for web, worker, scheduler, database, and cache instead of overloading one container.
- Use named volumes for stateful services: Databases and uploaded files should not rely on ephemeral container storage.
- Use service names on the internal network: Let containers talk to each other through Compose networking rather than hard-coded IPs.
- Keep secrets out of the compose file when possible: Load them from an environment file, secret manager, or host environment. Restrict file permissions.
- Separate build-time and runtime concerns: Build the image once, then deploy with environment-specific runtime values.
- Pin image tags carefully: Avoid drifting to unexpected versions in production.
- Set resource expectations: Even if you do not enforce strict limits, document expected memory and CPU needs per service.
- Plan startup dependencies: Your app may need retries if the database starts more slowly than the app container.
- Add backup procedures: For databases, define how snapshots or dumps are created and restored.
- Document one rollback command: The fastest rollback is the one already written down.
For many teams, docker compose production is the best balance of control and simplicity for the first meaningful production deployment.
Scenario 3: Static app or API with CI/CD and image registry
If you deploy frequently, add a lightweight pipeline. A simple GitHub Actions deployment can remove manual steps without making the process opaque.
- Build on each release or main branch merge: Produce a tagged image from CI rather than building ad hoc on the server.
- Push to a registry: Store versioned images in a registry your server can pull from.
- Use immutable tags for releases: Keep a unique release tag and optionally a convenience tag like
latest. - Deploy by pulling a known image: The server should pull and start the tested image instead of rebuilding production code.
- Run post-deploy checks: Verify container health, app startup, and expected HTTP responses.
- Keep credentials scoped: CI should only have the access it needs to push images and connect for deployment.
- Retain manual override: Keep a documented SSH fallback in case CI is unavailable.
If you want to extend this approach, see Plkdt Labs’ GitHub Actions Deployment Guide: Build, Test, and Deploy Web Apps Reliably.
Scenario 4: Public app with custom domain and TLS
Many deployment issues are not Docker issues at all. They are DNS and proxy issues. If your app will be reachable by domain name, add these checks before launch.
- Point the domain or subdomain correctly: Use the right A, AAAA, or CNAME record for your setup.
- Allow for DNS propagation time: Test from multiple networks if changes seem inconsistent.
- Terminate TLS at the proxy: Keep certificate management at the edge unless you have a clear reason not to.
- Forward the right headers: Your app may need host, scheme, and client IP headers to generate correct redirects and URLs.
- Confirm firewall rules: Open only the ports you intend to serve publicly, usually 80 and 443.
- Validate canonical URLs: Check redirects between www and apex, HTTP and HTTPS, and trailing slash behavior.
For DNS configuration help, see the Cloudflare DNS Setup Guide for Domains, Subdomains, and Common Record Types and the DNS Propagation Checker Guide: How to Verify Record Changes and Troubleshoot Delays.
What to double-check
Before each release, run through this short operational checklist. These are the details most likely to cause avoidable downtime in a small production Docker deployment.
Image and build hygiene
- Does the image include only production dependencies?
- Is the working directory correct?
- Are file permissions compatible with the runtime user?
- Is the image tagged in a way you can roll back to?
- Can the container start without interactive input?
Runtime configuration
- Are all required environment variables present?
- Are secrets loaded securely and not committed to the repository?
- Does the container write files only to intended paths?
- Are volumes mounted where persistent data actually lives?
- Are timezone, locale, and encoding assumptions explicit where needed?
Network and proxy behavior
- Is the app listening on the correct internal port?
- Does the reverse proxy target the right container and port?
- Are health endpoints reachable internally?
- Does HTTPS redirect work as expected?
- Are large uploads, websocket connections, or long-running requests supported by proxy settings if your app needs them?
Data safety
- Do you know what data is stateful?
- Is there a backup method for that data?
- Have you tested a restore, not just backup creation?
- Will deploys accidentally replace or wipe volumes?
Observability and recovery
- Can you see recent logs quickly?
- Do you have a command or script to restart only one failing service?
- Do you know what success looks like after deployment: a 200 response, a migration complete message, a worker heartbeat?
- Do you have the previous image tag ready for rollback?
If email sending is part of your app, also verify domain authentication outside the container stack. Application deploys can look healthy while outbound email quietly fails because DNS records are incomplete. Plkdt Labs’ SPF, DKIM, and DMARC Setup Guide for Google Workspace, Microsoft 365, and Custom Domains covers the DNS side of that setup.
Common mistakes
The fastest way to improve reliability is to avoid a few recurring errors. These show up in many first production Docker deployments.
Using Docker as the only deployment plan
Docker packages the app, but it does not replace release discipline. You still need a defined image source, config handling, logs, backups, and rollback steps.
Building directly on the server without a repeatable process
Building in production can work for experiments, but it makes releases harder to reproduce. A cleaner pattern is to build once in CI or on a trusted machine, push to a registry, and deploy that known image.
Storing secrets in the repository
Environment files often drift into version control by accident. Keep secrets out of images and repositories, and restrict who can read deployment-time configuration.
Publishing container ports directly to the internet
For most web apps, it is cleaner to expose only the reverse proxy publicly and keep application containers on an internal Docker network.
Running databases casually in the same stack without backup discipline
A database in Compose is fine for some small apps, but only if you treat persistence seriously. Named volumes are not a backup strategy by themselves.
Skipping health checks and smoke tests
A container can be “running” while the app inside it is unusable. Add a lightweight health endpoint and a simple post-deploy test that verifies the real user path.
Letting image tags drift
Using broad tags everywhere makes rollback harder. Prefer explicit version tags and keep track of what is currently deployed.
Ignoring DNS and SSL as separate layers
If the app works on localhost but not by domain, the fix may be DNS, proxy routing, certificate handling, or firewall rules rather than the container itself. If you are comparing deployment patterns for hosted frontends, these guides may help: How to Connect a Domain to Netlify: DNS Records, SSL, and Common Fixes and How to Connect a Domain to Vercel Without DNS Errors.
Adding too much complexity too early
Small teams often do better with a well-documented single server and Compose than with an under-maintained cluster. Complexity should solve a real scaling, reliability, or compliance problem, not just mirror bigger companies.
When to revisit
This deployment guide is most useful when treated as a recurring checklist, not a one-time read. Revisit your Docker production setup whenever the inputs change.
- Before a major release: Review secrets, migrations, resource needs, health checks, and rollback steps.
- Before seasonal planning cycles: Re-check server capacity, logging retention, backup routines, and domains you depend on.
- When workflows or tools change: If you add GitHub Actions deployment, switch registries, change reverse proxies, or move DNS providers, update the runbook.
- When the app becomes stateful in new ways: User uploads, background jobs, generated files, and scheduled tasks change the operational risk profile.
- When the team changes: A deployment that only one person understands is not production-ready enough.
Here is a practical maintenance routine you can adopt:
- Keep one deployment document in the repository with startup, deploy, rollback, backup, and restore commands.
- Once per quarter, perform a fresh deploy to a non-production environment from the documented steps only.
- Once per quarter, test one restore path for persistent data.
- After each incident, update the checklist with the missing signal or missing step that would have shortened the outage.
- Whenever you change DNS, proxy settings, or domain routing, validate end-to-end behavior from public URL to container response.
A good small app deployment is not the most advanced one. It is the one your team can run repeatedly, debug quickly, and improve over time. If your current Docker setup lets you build once, deploy predictably, monitor the basics, and roll back without panic, you are on the right path.