Kushal Timilsina
6 min read

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.

deploymentdevopsbest-practiceslinuxdockerinfrastructure

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:

StrategyMethodDowntime
Blue-greenTraffic switchZero
RollingGradual instance replacementZero
Image tag revertdocker compose up -d with previous tagSeconds
Database restoreRestore from backupMinutes

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 .env files 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.