Traditional Deployment #

Before Docker, Kubernetes, and cloud-native technologies became mainstream, almost every software team ran their applications the same way: log into the server, upload the code, restart the service, and pray. That approach isn’t bad — it worked for a long time. But as applications grew, the cracks started to show in ways that human effort alone couldn’t patch.

This article covers traditional deployment in depth: what it is, how its workflow looks, which methods are commonly used, and the classic problems that gave birth to Docker. The goal isn’t to belittle this approach, but to give you the historical context that makes Docker’s value feel real. Without that context, Docker looks like a cool tool with no strong reason to learn it.

If you already understand Docker well enough, feel free to skip this article. But if you want to truly understand why the software industry moved from VMs to containers, this article is the foundation.

What Is Traditional Deployment? #

Traditional deployment is an umbrella term for running applications directly on top of the server’s operating system (physical or virtual), without strict runtime isolation. The application, its dependencies, and its configuration are installed straight onto the host.

Its main characteristics:

  • The application runs on top of the host OS, sharing resources with other applications on the same server.
  • Dependencies are installed globally on the host OS (apt install, yum install, npm install -g).
  • Configuration lives in host system files (/etc, ~/.env, global environment variables).
  • Deployment is usually done manually or semi-automatically via SSH, scripts, or configuration tools.
flowchart TB
    subgraph Server["Physical Server / VM"]
        OS[Host OS]
        R[Global runtime: Java, Node, PHP]
        L[Global libraries]
        App1[App A]
        App2[App B]
        App3[App C]
        C[Global config]
        OS --> R --> L
        L --> App1
        L --> App2
        L --> App3
        OS --> C
    end
    User[User] --> Server

In the diagram above, three applications share the same runtime, libraries, and configuration. If App A needs Python 3.8 and App B needs Python 3.12, the administrator has to pick one (and the other app suffers) or manage two Python versions by hand.

Common Architecture #

The traditional deployment architecture at its simplest:

flowchart LR
    U[Users] --> LB[Load Balancer]
    LB --> S1[Server 1]
    LB --> S2[Server 2]
    LB --> S3[Server 3]
    
    subgraph S1["Server 1"]
        OS1[OS]
        RT1[Runtime]
        APP1[Application]
    end
    
    subgraph S2["Server 2"]
        OS2[OS]
        RT2[Runtime]
        APP2[Application]
    end
    
    subgraph S3["Server 3"]
        OS3[OS]
        RT3[Runtime]
        APP3[Application]
    end

Each server runs a copy of the same application. A load balancer distributes traffic across the available servers. To scale, the administrator adds a new server, installs the same runtime and dependencies, then deploys the code.

Problems start to surface when:

  • The number of servers grows (10, 20, 100) — manual configuration becomes impossible.
  • The application has many services with different dependencies — runtime collisions are inevitable.
  • The developer and ops teams are separate — code that works on a laptop doesn’t work on their servers.

Commonly Used Deployment Methods #

Traditional deployment isn’t a single method. It evolved from the simplest (manual) to approaches bordering on modern (config management and VM images). Here are the four methods you’ll most often encounter.

Manual Deployment via SSH #

The earliest and simplest method. Many small teams still do this today.

Typical flow:

  1. The developer builds the application on their laptop, producing a binary or archive.
  2. The administrator logs into the server via SSH.
  3. The application files are uploaded using scp, rsync, or FTP.
  4. Dependencies are installed or updated manually.
  5. The service is restarted using systemctl or service.
  6. The application is verified to be alive, usually with curl localhost.
# Example of a manual flow in an engineer's terminal
scp ./build/app.tar.gz user@server:/opt/app/
ssh user@server "cd /opt/app && tar -xzf app.tar.gz && systemctl restart app"

Hallmarks of this approach:

  • Highly dependent on humans — every step is prone to forgetting or typos.
  • No clear audit trail — just the server’s .bash_history.
  • Can’t be repeated with precision — different servers may have different conditions.
  • Doesn’t scale — imagine deploying to 50 servers this way.

In the 2000s, this approach was common. Today, for production applications, it’s considered an anti-pattern.

Shell Script Deployment #

To reduce manual errors, teams started writing deployment scripts — bash files that automate the steps above.

#!/bin/bash
# A simple deploy.sh example
set -e

APP_NAME="webapp"
REMOTE_HOST="[email protected]"
REMOTE_DIR="/opt/webapp"
LOCAL_BUILD="./build/webapp.tar.gz"

echo "==> Uploading build..."
scp "$LOCAL_BUILD" "$REMOTE_HOST:$REMOTE_DIR/"

echo "==> Extracting and restarting..."
ssh "$REMOTE_HOST" << 'EOF'
  cd /opt/webapp
  tar -xzf webapp.tar.gz
  sudo systemctl restart webapp
  sleep 2
  curl -sf http://localhost/health || exit 1
EOF

echo "==> Deploy finished."

Scripts like this are better than doing it by hand, but they have weaknesses:

  • OS-specific — a script for Ubuntu won’t run on CentOS.
  • Not idempotent — running the script twice can produce different states.
  • Hard to test — you can’t be truly sure the script will work without running it on a real server.
  • No automatic rollback — if a deploy fails, the script has no idea how to go back to the previous version.

Configuration Management #

To address the weaknesses of shell scripts, configuration management tools appeared: Ansible, Chef, Puppet, SaltStack. They introduced the concept of Infrastructure as Code — servers configured from declarative files that can be reviewed, version-controlled, and repeated.

A simple example with Ansible:

# playbook.yml
- hosts: web
  become: true
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present

    - name: Copy app config
      copy:
        src: ./nginx.conf
        dest: /etc/nginx/nginx.conf
        mode: 0644

    - name: Restart Nginx
      service:
        name: nginx
        state: restarted

Advantages over shell scripts:

  • Idempotent — running the playbook 10 times produces the same end state.
  • Cross-platform — Ansible can manage Linux, FreeBSD, and Windows servers.
  • Declarative — you declare what you want; Ansible figures out how.
  • Audit-friendly — playbooks can be reviewed, tested, and version-controlled.

Its drawbacks:

  • Agent-based tools (Chef, Puppet) need software on every server.
  • The learning curve is steep for complex playbooks.
  • Dependencies are still installed globally — Ansible doesn’t isolate applications, it only manages their configuration.

VM Image-Based Deployment #

Another popular approach: bundle the application and OS into a single image, then deploy that image to servers or the cloud.

The most common examples:

  • Amazon Machine Image (AMI) — OS + application image on AWS EC2.
  • Vagrant Box — a VM image for local development.
  • Packer + QEMU/VMware — tools for building VM images automatically.

The flow: build one “golden” image containing OS + runtime + application. From that image, spawn many VM instances in the cloud. Every instance is identical at first boot.

flowchart LR
    P[Packer build] --> I[Golden VM image]
    I --> V1[VM Instance 1]
    I --> V2[VM Instance 2]
    I --> V3[VM Instance 3]
    V1 --> LB[Load Balancer]
    V2 --> LB
    V3 --> LB

Advantages:

  • High consistency — all instances are identical to the image.
  • Good for environments that need special OS configuration.

Drawbacks:

  • Image size is huge — GB, not MB.
  • Slow builds — a VM image takes minutes to create.
  • Hard to update — when the application changes, the image must be rebuilt from scratch or patched manually.
  • Inflexible — per-server configuration is hard without a new image.
Evolution note: VM images are a more “cloud-friendly” approach than manual or shell-script deployment. They solve the provisioning problem well, but not the dependency isolation and immutable artifact problems. Ironically, it’s exactly VM images’ weaknesses that ultimately drove the adoption of container images — images that are small, fast to build, and portable.

Dependency Management — the Biggest Source of Problems #

If you can remember only one thing from this article, remember this: dependency management is the biggest source of traditional deployment problems.

A scenario that’s nearly universal in every engineering team:

flowchart TB
    subgraph Server["One production server"]
        A[App A: needs Python 3.8 + libX 1.2]
        B[App B: needs Python 3.11 + libX 2.0]
        C[App C: needs Python 2.7 + libX 1.0]
    end
    Server --> Conflict[Dependency conflicts]

Three applications, three Python versions, three versions of the same library. The administrator has to choose:

  • Install one global version → the other applications will break.
  • Install all versions side-by-side → clutter the system with confusing python3.8, python3.11, python2.7.
  • Use per-language isolation tools (virtualenv, nvm) → helps, but is limited to a single language.

Per-language solutions:

LanguageIsolation ToolLimitation
Pythonvirtualenv, venv, pyenvPython only, not system libraries
Node.jsnvmNode only, not native modules
Rubyrbenv, rvmRuby only, heavy on the system
JavaMultiple JDK installsJDK version must match the build tool

The fundamental problem: per-language isolation doesn’t isolate the system. A Python app that needs a specific C library (for example lxml with binary bindings) will still clash with a different version of that C library on the OS.

Messy Environment Configuration #

Beyond dependencies, environment configuration is also a traditional deployment nightmare. Every environment — development, staging, production — has its own conditions that are nearly impossible to make exactly the same.

flowchart LR
    Dev[Developer Laptop] -.->|small differences| Staging
    Staging -.->|big differences| Prod[Production]
    Dev --> |"Works on my machine"| WOMM[Works on my machine]

Examples of problems that often come up:

  • Environment variables — set with export on a laptop, written to /etc/environment in production, pulled from a secret manager in CI. No standard format.
  • Database connections — local SQLite in dev, PostgreSQL in staging, RDS with IAM in production. Different connection strings; queries that work in dev error out in production.
  • Secrets — the database password lives in .env on a developer’s laptop, in a server config file, in Vault, in AWS Secrets Manager. Different formats and access methods.
  • File paths/home/dev/app on a laptop, /opt/app on a server. Hardcoded paths often break scripts when moved.

The term born from this chaos: environment drift. Each environment “drifts” away from the others over time due to undocumented manual changes.


Inelastic Scaling #

Scaling in traditional deployment is manual and slow. There’s no way to add capacity in seconds — instead there’s a sequence of time-consuming steps.

sequenceDiagram
    participant Op as Operator
    participant Cloud as Cloud Console
    participant Prov as Provisioning
    participant Config as Config Manager
    participant App as App

    Op->>Cloud: Launch new VM
    Cloud->>Prov: VM booting (5-10 minutes)
    Prov->>Config: Apply playbook
    Config->>App: Install runtime
    Config->>App: Copy app code
    Config->>App: Restart service
    App-->>Op: Healthy
    Op->>Cloud: Add to load balancer

To scale up (add capacity):

  1. Provision a new server (VM or physical) — minutes to hours.
  2. Install the OS and patches.
  3. Install the runtime and dependencies.
  4. Copy the application and configuration.
  5. Configure the load balancer so traffic goes to the new server.
  6. Verify the health check.

Total time: at best 15–30 minutes, on average 1–2 hours. That’s not suitable for sudden traffic spikes (events, viral content, or attacks).

Autoscaling practically doesn’t exist. You can write a script that monitors CPU, but every scaling event runs all the steps above — slow and unreliable.


Rudimentary Monitoring and Logging #

In the traditional deployment era, observability was something every team implemented its own way, with different formats.

Commonly used tools:

  • Log files in /var/log, read with tail -f or grep.
  • Cron jobs for log rotation.
  • Custom scripts for alerting (e.g. if [ $(df / | awk 'NR==2{print $5}') -gt 90 ]; then mail [email protected]; fi).
  • Nagios, Zabbix, Icinga — classic monitoring tools with web-based UIs.
# Example of 'traditional' monitoring in cron
*/5 * * * * df -h | grep -E '[8-9][0-9]%' && echo "Disk almost full" | mail -s "Alert" [email protected]

What’s missing from this approach:

  • Structured logging — logs are usually plain text, hard to query.
  • Centralized log aggregation — logs are scattered across many servers.
  • Distributed tracing — for multi-service applications, tracing requests across services is hard.
  • Metrics & dashboards — usually MRTG/Cacti graphs for the network, but not for applications.
An anti-pattern still widely seen: Production applications are debugged by SSH-ing into the server and running tail -f on the logs. That isn’t monitoring — it’s reactive troubleshooting. Monitoring should be proactive: telling you about anomalies before users complain. The traditional approach can’t support this workflow well.

Advantages of Traditional Deployment #

The traditional approach isn’t without merits. There are situations where it still makes sense.

  • Conceptually simple. No extra tools needed beyond SSH and a text editor.
  • Full control over the OS. Administrators have complete access for tuning, debugging, and recovery.
  • Easy for beginners to understand. No learning curve for new tools.
  • Good for small or internal applications. For one server with one application, traditional deployment is still very practical.
  • No container/VM overhead. The application runs directly on the OS without an extra layer.

For non-critical applications, prototypes, or single-server deployments, the traditional approach is still relevant and there’s no strong reason to migrate.

Disadvantages of Traditional Deployment #

The same drawbacks that make this approach problematic at scale:

  • Dependency conflicts. Applications share runtimes and libraries.
  • Not reproducible. The same server will differ after a year of running due to manual changes.
  • Slow, manual scaling. Not elastic.
  • High human error. Every deployment step is prone to mistakes.
  • Long initial setup. Onboarding a new server takes time.
  • Hard CI/CD. Build environments are difficult to standardize.
  • Hard debugging. No modern observability.
  • Environment drift. Dev, staging, and production are never truly identical.

All of these problems accumulate over time. The first application might still be manageable. The fifteenth application, with 30 servers and 5 different languages, becomes an operational nightmare.


Comparison with Docker #

Docker answers almost all of the problems above. The table below summarizes the direct comparison.

Traditional ProblemDocker Solution
Dependency conflictsEvery container carries its own dependencies
Manual setupDockerfile (automatic, reproducible builds)
Not reproducibleImmutable images (built once, always the same)
Hard scalingContainer orchestration (Kubernetes, Swarm)
Inconsistent environments“Build once, run anywhere”
Slow deploymentPull image + start container (seconds)
Application updatesBuild a new image, replace the container
RollbackPull the previous image version, restart
Slow onboardingdocker compose up runs the entire stack

Docker isn’t magic. It doesn’t remove complexity — it shifts complexity. The complexity moves from “managing many servers with manual configuration” to “writing Dockerfiles, managing images, and operating an orchestrator”. But this new complexity is automatic, documented, and reproducible — far healthier.


When Traditional Deployment Still Makes Sense #

Even though Docker is the modern standard, there are situations where the traditional approach still makes sense.

  • Legacy systems under strict regulation. Banks, governments, and healthcare sometimes need full OS control for compliance.
  • Small internal applications. Internal tools with 5–10 users — Docker’s overhead isn’t worth its value.
  • Single-board computers / edge devices. A Raspberry Pi for signage or kiosks might be fine with manual deployment.
  • Very early prototyping. If you’re just writing a hello world, jumping straight to Docker can be premature.
  • Air-gapped environments. Some production environments are truly isolated from the internet, and modern tools can’t run there.

But for applications that face users, need to scale, and support the business, Docker (or an equivalent) is no longer a choice — it’s a necessity.


Summary #

  • Traditional deployment means running applications directly on the server OS without runtime isolation. It works at small scale, but cracks at large scale.
  • Four common methods: manual SSH, shell scripts, configuration management (Ansible, Puppet, Chef), and VM images. Each solves part of the problem, but not all of it.
  • Dependency management is the biggest source of problems. Different applications need different runtimes, and sharing a host OS always ends in conflict.
  • Environment drift — dev, staging, and production conditions are never truly identical. This is where “works on my machine” comes from.
  • Scaling is slow and manual: provisioning, installing, configuring, registering with the load balancer — all manual, 15 minutes to hours in total.
  • Traditional observability (tail -f, cron scripts) isn’t enough for modern applications. You need structured logging, metrics, and tracing.
  • Docker answers all of these problems by shifting complexity from “manual server configuration” to “images and orchestration”. The result: reproducible, scalable, and automatic.
  • The traditional approach is still relevant for small applications, regulated legacy systems, or early prototyping. But for business applications, containers are already a necessity, not a choice.

← Previous: What is Docker?   Next: VM vs Container →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact