Deployment Practices: Common Mistakes and How to Avoid Them
A candid look at the most common deployment mistakes I have encountered in production environments and the practices that prevent them.
Why Deployment Mistakes Matter
Deployment is the moment of truth for any application. Code that worked perfectly in development can fail in production for reasons ranging from environment drift to configuration gaps. Over several years of managing Linux infrastructure, I have seen the same patterns repeat across teams and organisations. This article documents those patterns and the practices that prevent them.
Mistake 1: Environment Drift
The most common root cause of deployment failures is environment drift — the gap between development, staging, and production configurations. A package version available in Ubuntu 22.04 may differ from 24.04. A development database with ten rows behaves differently from production with ten million.
The fix: Containerise everything. Docker eliminates environment drift by packaging the application with its exact runtime, dependencies, and configuration:
FROM node:20-alpine
WORKDIR /app
COPY package*.json .
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
The same image that passes tests in CI is deployed to staging and production. No drift, no surprises.
The fix: Use infrastructure-as-code tools (Terraform, Ansible, or Pulumi) to define server configuration. Manual SSH changes should be treated as incidents — they will be overwritten on the next provision.
Mistake 2: Deploying from Memory
Skipping CI/CD and deploying directly via SSH is tempting for quick fixes. It is also how production outages happen. A missing asset, a wrong file permission, or a forgotten build step can bring down a service with no audit trail.
The fix: Every deployment must go through a CI/CD pipeline, even for urgent hotfixes. A minimal pipeline takes five minutes to configure and provides:
- Build reproducibility
- Artefact versioning
- Rollback capability
- Audit trail
If a deployment cannot be triggered through CI/CD, it should not go to production.
Mistake 3: No Health Checks After Deployment
Many deployment scripts push new code and assume success. In practice, a deployment is complete only when the application responds correctly. I have seen deployments where the service never started, but the monitoring dashboard remained green because the old process was still running on a different port.
The fix: Implement post-deployment health checks:
#!/bin/bash
# health-check.sh
MAX_RETRIES=30
ENDPOINT="http://localhost:3000/health"
for i in $(seq 1 $MAX_RETRIES); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $ENDPOINT)
if [ "$STATUS" == "200" ]; then
echo "Health check passed"
exit 0
fi
echo "Waiting for service... attempt $i/$MAX_RETRIES"
sleep 5
done
echo "Health check failed after $MAX_RETRIES attempts"
exit 1
This script should be the final step in your deployment pipeline. If it fails, the pipeline fails and the deployment is rolled back.
Mistake 4: Ignoring Rollback Plans
Every deployment should have a one-command rollback. If a deployment introduces a bug, the time between detection and recovery should be measured in minutes, not hours. Common rollback strategies:
| Strategy | Method | Downtime |
|---|---|---|
| Blue-green | Traffic switch | Zero |
| Rolling | Gradual instance replacement | Zero |
| Image tag revert | docker compose up -d with previous tag | Seconds |
| Database restore | Restore from backup | Minutes |
For Docker-based deployments, rolling back is as simple as referencing the previous image tag:
docker compose up -d --pull=never
# If the new image fails:
docker compose up -d --tag <previous-sha>
Mistake 5: Hardcoded Configuration
Embedding database URLs, API keys, or environment-specific values in source code is a recurring anti-pattern. It creates security risks (credentials in version control), deployment friction (different configs per environment), and operational overhead (restarting builds for config changes).
The fix: Use environment variables with secure defaults:
- Secrets: Store in a vault (Hashicorp Vault, AWS Secrets Manager, or GitHub Secrets)
- Configuration: Use environment-specific
.envfiles with a documented template - Feature flags: Toggle features at runtime without redeploying
A .env.example file committed to the repository documents every required variable without exposing values.
Mistake 6: Skipping Staging
Deploying directly to production without a staging environment is a risk that rarely pays off. Even with comprehensive test coverage, certain issues only surface under production-like conditions — traffic patterns, data volumes, third-party API rate limits.
The fix: Maintain a staging environment that mirrors production as closely as possible. It does not need to match production scale exactly, but it should match:
- Operating system and kernel version
- Runtime versions (Node.js, Python, PostgreSQL)
- Architecture (same CPU architecture, preferably)
- Dependency versions (shared libraries, system packages)
Staging catches issues that unit tests and code reviews miss.
Mistake 7: Inadequate Logging and Monitoring
A deployment that fails silently is worse than one that fails loudly. Without proper logging and monitoring, you may not discover a failed deployment until users report issues.
The fix: Implement structured logging early:
// Winston logging example
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({
filename: '/var/log/app/error.log',
level: 'error',
}),
new winston.transports.File({
filename: '/var/log/app/combined.log',
}),
],
});
Combine this with uptime monitoring (Pingdom, UptimeRobot) and application performance monitoring (Prometheus, Grafana, or Datadog) to detect deployment issues early.
Mistake 8: Database Migrations in the Same Deploy Step
Running database schema changes and application code updates in the same deployment step is risky. If a migration fails, the application may be incompatible with the previous schema version.
The fix: Decouple migrations from deployments. Run migrations as a separate, reversible step before deploying the new code:
# CI/CD pipeline
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- run: npx prisma migrate deploy
deploy:
needs: migrate
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
This ensures the database schema is ready before the new code expects it and allows rolling back the migration independently if needed.
Conclusion
Deployment reliability is not achieved through any single tool or practice — it is the result of consistent attention to detail across the entire pipeline. Containerise your applications, automate every step through CI/CD, implement health checks and rollback procedures, and treat configuration as a first-class concern. These practices transform deployments from high-risk events into routine, reliable operations.