Many teams begin their production deployments like this:
- SSH into the server
- Pull the latest code
- Run commands
- Restart the app
This approach works — until environments become inconsistent.
The Problem
Over time, every server becomes a snowflake:
- Different dependency versions installed manually
- One-off fixes applied directly on the machine
- Rollbacks that require remembering what changed and when
This leads to deployment instability and operational risk. Each server becomes special, fragile, and hard to replace. The issue is not deployment speed — it is lack of reproducibility.
The Solution: Containers
Instead of deploying source code, we deploy immutable container images.
Each image contains:
- Runtime (Ruby, Node, etc.)
- Dependencies pinned at exact versions
- Application code
- Precompiled assets
The image is built once and remains identical across every environment — development, staging, production.
# Example: Minimal production Dockerfile
FROM ruby:3.3-slim
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test
COPY . .
RUN bundle exec rails assets:precompile
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
What Changes
The server's job becomes simple:
- Run Docker
- Run Nginx as a reverse proxy
- Start containers from the registry
The server does not:
- Install dependencies
- Pull source code
- Modify the runtime environment
Benefits
| Before (code deploy) | After (container deploy) | |---|---| | Environment drift | Identical environments | | Hard rollbacks | Pull previous image tag | | Snowflake servers | Replaceable infrastructure | | "Works on my machine" | Same image everywhere |
- Predictable deployments — what you test is what runs in production
- Easy rollback — revert to a previous image in seconds
- No configuration drift — environments stay consistent
- Replaceable servers — infrastructure becomes disposable
Deployment Flow
# 1. Build the image
docker build -t myapp:v1.2.3 .
# 2. Push to registry
docker push registry.example.com/myapp:v1.2.3
# 3. On the server — pull and run
docker pull registry.example.com/myapp:v1.2.3
docker stop myapp && docker rm myapp
docker run -d --name myapp \
-p 3000:3000 \
--env-file .env.production \
registry.example.com/myapp:v1.2.3
Takeaway
Servers should run containers, not code.
Immutability reduces operational risk. When every deployment is a known, tested, versioned artifact — not a sequence of commands run on a live server — reliability goes up and surprises go down. Infrastructure should be replaceable, not maintained.