Skip to content

Instantly share code, notes, and snippets.

@chaitanyaSoni96
Last active March 3, 2026 21:19
Show Gist options
  • Select an option

  • Save chaitanyaSoni96/295a0105c30cf1e15273d6b875c72c49 to your computer and use it in GitHub Desktop.

Select an option

Save chaitanyaSoni96/295a0105c30cf1e15273d6b875c72c49 to your computer and use it in GitHub Desktop.
Deploy podman images to VPS
#!/usr/bin/env bash
set -euo pipefail
# ── Arguments ─────────────────────────────────────────────────────────────────
usage() {
echo "Usage: $0 <host> <compose-file> [remote-dir] [--tag <version>]"
exit 1
}
HOST=""
COMPOSE_FILE=""
REMOTE_DIR="~/app"
TAG=""
# Parse positional args and optional --tag
POSITIONAL=()
while [[ $# -gt 0 ]]; do
case "$1" in
--tag) TAG="$2"; shift 2 ;;
--tag=*) TAG="${1#--tag=}"; shift ;;
-*) echo "Unknown option: $1"; usage ;;
*) POSITIONAL+=("$1"); shift ;;
esac
done
[[ ${#POSITIONAL[@]} -lt 2 ]] && usage
HOST="${POSITIONAL[0]}"
COMPOSE_FILE="${POSITIONAL[1]}"
REMOTE_DIR="${POSITIONAL[2]:-~/app}"
echo "==> Host: $HOST"
echo "==> Compose: $COMPOSE_FILE"
echo "==> Remote dir: $REMOTE_DIR"
echo "==> Tag: ${TAG:-<from compose file>}"
[ -f "$COMPOSE_FILE" ] || { echo "Error: compose file '$COMPOSE_FILE' not found"; exit 1; }
# ── Build images ───────────────────────────────────────────────────────────────
echo
echo "==> Building images locally"
if [ -n "$TAG" ]; then
APP_VERSION="$TAG" podman compose -f "$COMPOSE_FILE" build
else
podman compose -f "$COMPOSE_FILE" build
fi
# ── Collect images ─────────────────────────────────────────────────────────────
echo
echo "==> Collecting image list"
if [ -n "$TAG" ]; then
IMAGES=$(APP_VERSION="$TAG" podman compose -f "$COMPOSE_FILE" config | awk '/image:/ {print $2}' | sort -u)
else
IMAGES=$(podman compose -f "$COMPOSE_FILE" config | awk '/image:/ {print $2}' | sort -u)
fi
if [ -z "$IMAGES" ]; then
echo "Error: no images found in compose file"
exit 1
fi
echo "Images:"
echo "$IMAGES"
# ── Transfer images ────────────────────────────────────────────────────────────
echo
echo "==> Transferring images (layer-aware)"
for IMG in $IMAGES; do
echo " -> $IMG"
podman image scp "$IMG" "$HOST::"
done
# ── Prepare remote directory ───────────────────────────────────────────────────
echo
echo "==> Preparing remote directory"
ssh "$HOST" "mkdir -p $REMOTE_DIR"
# ── Upload compose file ────────────────────────────────────────────────────────
echo
echo "==> Uploading compose file"
scp "$COMPOSE_FILE" "$HOST:$REMOTE_DIR/compose.yml"
# ── Enforce pull_policy: never ─────────────────────────────────────────────────
echo
echo "==> Enforcing pull_policy: never"
ssh "$HOST" "
cd $REMOTE_DIR
if ! grep -q pull_policy compose.yml; then
awk '
/image:/ { print; print \" pull_policy: never\"; next }
{ print }
' compose.yml > compose.tmp && mv compose.tmp compose.yml
fi
"
# ── Start services ─────────────────────────────────────────────────────────────
echo
echo "==> Starting services"
if [ -n "$TAG" ]; then
ssh "$HOST" "cd $REMOTE_DIR && APP_VERSION=$TAG podman compose up -d --remove-orphans"
else
ssh "$HOST" "cd $REMOTE_DIR && podman compose up -d --remove-orphans"
fi
echo
echo "==> Deployment complete!"
@chaitanyaSoni96
Copy link
Author

chaitanyaSoni96 commented Feb 20, 2026

podman-deploy

A bash script for building and deploying Podman Compose services to a remote host — without a registry. Images are transferred directly via podman image scp.

Requirements

  • podman and podman-compose on both local and remote machines
  • SSH access to the remote host
  • scp available locally

Usage

./deploy.sh <host> <compose-file> [remote-dir] [--tag <version>]
Argument | Required | Description -- | -- | -- host | yes | SSH destination (e.g. user@192.168.1.10) compose-file | yes | Path to your local compose file remote-dir | no | Remote path to deploy to (default: ~/app) --tag | no | Overrides APP_VERSION in the compose file

Examples

# Basic deploy
./deploy.sh user@myserver ./compose.yml

Deploy to a specific remote directory

./deploy.sh user@myserver ./compose.yml /srv/myapp

Deploy a specific version

./deploy.sh user@myserver ./compose.yml --tag 1.4.2

All options

./deploy.sh user@myserver ./compose.yml /srv/myapp --tag 1.4.2

What it does

  1. Builds images locally with podman compose build
  2. Collects the image list from the resolved compose config
  3. Transfers each image to the remote host via podman image scp (layer-aware, no registry needed)
  4. Creates the remote directory if it doesn't exist
  5. Uploads the compose file as compose.yml
  6. Patches pull_policy: never into the remote compose file so Podman won't try to pull images
  7. Runs podman compose up -d --remove-orphans on the remote host

Compose file

Services should use APP_VERSION for their image tags. The :-latest fallback means the compose file works fine without --tag.

# compose.yml
services:
api:
image: myapp/api:${APP_VERSION:-latest}
build:
context: ./api
ports:
- "8080:8080"
environment:
- ENV=production
restart: unless-stopped

worker:
image: myapp/worker:${APP_VERSION:-latest}
build:
context: ./worker
depends_on:
- api
restart: unless-stopped

db:
image: postgres:16 # third-party images are left as-is
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
restart: unless-stopped

volumes:
db-data:

Versioning

Using --tag threads the version through the entire deploy:

local build  →  myapp/api:1.4.2
scp → remote host
compose up → APP_VERSION=1.4.2

Without --tag, images are built and run as whatever tag is defined in the compose file (defaulting to latest with the example above).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment