High Five Studio

July 2026

Why I Replaced Docker with Simple Shell Scripts for Croatian Deployments

Why I replaced Docker with simple shell scripts for Croatian deployments—and saved time, money, and sanity on a single VPS

Why I Replaced Docker with Simple Shell Scripts for Croatian Deployments

I’d been running a small SaaS for local businesses in Zagreb on a single VPS for two years. Docker was supposed to make my life easier, but instead I was spending more time debugging container networking on DigitalOcean than actually shipping features. So I deleted the Docker Compose file, wrote 150 lines of Bash, and never looked back.

Why Docker Felt Like Overkill for a Single Server

When you’re deploying to a single VPS that costs 12 euros a month, Docker’s abstractions start to feel like a tax you didn’t sign up for. The promise of reproducible environments sounds great until you realize your “production” is a single machine in Amsterdam with a 2 GB RAM limit.

The True Cost of Container Overhead

Every container you spin up takes memory, CPU cycles, and—more importantly—your attention. On a machine with limited resources, running a Redis container, a PostgreSQL container, and your application container means you’ve already lost 400 MB of RAM to the Docker daemon and kernel overhead before your app even starts.

I remember rebuilding my entire deployment pipeline because a minor update to the Docker Compose format broke my setup. The fix took ten minutes, but the frustration lingered for days. For a project serving 50 Croatian businesses, that overhead just wasn’t justified.

What Docker Actually Solved (And What It Didn’t)

Docker excels at multi-service orchestration across multiple machines. If you’re running Kubernetes or managing a fleet of 20 servers, containers are a godsend. But for a single VPS with a Node.js backend and a PostgreSQL database, Docker adds complexity without proportional benefits.

The environment isolation Docker provides is real, but you can achieve 90% of that with a well-structured shell script and a few systemd services. The remaining 10%—perfect reproducibility across different machines—simply doesn’t matter when you’re deploying to the same server every time.

Building a Deployment System with Shell Scripts

I started with a single script called deploy.sh that handled the entire process from git pull to service restart. Over time, it evolved into a small collection of scripts that each did one thing well.

The Core Script Structure

My deployment system now consists of four scripts:

  • deploy.sh – The entry point that orchestrates the whole process
  • build.sh – Compiles the application and runs tests
  • install.sh – Copies files to the production directory and sets permissions
  • restart.sh – Gracefully restarts the application using systemd

Here’s the essence of my deploy script:

#!/bin/bash
set -euo pipefail

APP_NAME="my-croatian-service"
DEPLOY_DIR="/opt/$APP_NAME"
REPO_DIR="/home/deploy/$APP_NAME"

echo "[$(date)] Starting deployment of $APP_NAME"

# Pull latest code
cd "$REPO_DIR"
git pull origin main

# Build and test
./build.sh

# Install to production directory
./install.sh "$DEPLOY_DIR"

# Restart the service
./restart.sh "$APP_NAME"

echo "[$(date)] Deployment complete"

The set -euo pipefail line is crucial—it makes the script fail on any error, which is something Docker gives you for free but you have to be explicit about in shell scripting.

Managing Dependencies Without Containers

Docker’s biggest selling point is dependency management, but you can achieve similar results with a simple setup script. I maintain a setup.sh that installs all system dependencies and configures the environment:

#!/bin/bash
set -euo pipefail

# Install Node.js 18
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt-get install -y nodejs

# Install PostgreSQL 15
apt-get install -y postgresql-15 postgresql-contrib

# Configure PostgreSQL
systemctl enable postgresql
systemctl start postgresql

# Create application user and database
sudo -u postgres psql -c "CREATE USER app_user WITH PASSWORD '${DB_PASSWORD}';"
sudo -u postgres psql -c "CREATE DATABASE app_db OWNER app_user;"

This script runs once when you set up the server. After that, your deployment scripts only deal with the application code. The key insight is that the server’s environment doesn’t change between deployments—only your code does.

Real-World Performance Gains

The most immediate benefit was memory usage. Before switching, my VPS was running at 85% memory utilization with Docker running. After removing Docker and running the application directly with systemd, memory dropped to 45%.

A Concrete Example: A Croatian E-Commerce Site

One of my clients runs a small online store for local olive oil producers. Their setup includes a Node.js API, a React frontend served by Nginx, and PostgreSQL for the database. Under Docker, the stack consumed 1.2 GB of RAM for what was essentially a CRUD application with 200 daily users.

After moving to shell scripts and systemd, the same application runs at 600 MB. The deployment time dropped from 45 seconds (Docker build + push + pull on the server + restart) to 12 seconds (git pull + npm install + pm2 restart).

The client noticed the difference in two ways: their server bill didn’t increase when traffic grew, and deployments happened during lunch without anyone noticing a downtime window.

Reliability in Practice

I’ve had exactly zero deployment failures in the six months since switching. That’s not because shell scripts are inherently more reliable than Docker—it’s because the system is simpler, and simpler systems fail less often.

When something does go wrong, debugging is straightforward. I can SSH into the server and run journalctl -u my-service to see the exact output. There’s no container logs to parse, no docker exec commands to remember, no layers to inspect. The application logs are just system logs.

When You Should Still Use Docker

I’m not anti-Docker. I’m anti-unnecessary complexity. There are clear cases where containers make sense, even for small Croatian deployments.

Multi-Tenant or Multi-Service Architectures

If you’re running five different microservices that need to communicate, Docker Compose is a legitimate tool. The networking and service discovery features save you from writing your own solution. I still use Docker for a side project that runs a queue worker, a web server, and a scheduled task runner—all on the same machine but with different scaling requirements.

Client Environments with Different Stacks

If you’re deploying to machines you don’t control, Docker ensures consistency. I have a client who hosts their application on a shared server managed by a Croatian hosting provider. Docker guarantees that the application runs the same way regardless of what else is installed on that server.

Team Deployments

When multiple developers need to reproduce the exact same environment locally, Docker Compose is still the most straightforward solution. The key difference is that local development and production deployment are different problems with different solutions.

The Practical Takeaway

Start with the simplest possible deployment that works for your current setup. If you’re running a single VPS for a Croatian business, that means a shell script and systemd. Add complexity only when you can point to a specific problem it solves.

The next time you find yourself writing a Dockerfile for a single-server application, ask yourself: does this container solve a problem I actually have, or am I just following a trend? The answer will often surprise you.

I’m currently experimenting with combining shell scripts for the core deployment logic with Nix for dependency management. The goal is to get the reproducibility of containers without the overhead. If it works, I’ll share the setup in a future post. For now, my 150-line deployment system is still the best thing that happened to my Croatian deployments this year.