Skip to content
Blog Programming

How I Turned the Docker Commands I Repeat Most Into Commands I Can Type From Anywhere

By Romi Fadli, S.Kom. 18 min read Tooling & Automation
On this page
Tiga perintah docker yang panjang di kiri berubah jadi tiga kotak ungu pendek dcup, dls, dan dcn; ketiganya terhubung ke kotak bergaris putus-putus hijau bertuliskan /usr/local/bin yang terjangkau dari direktori mana pun, dan satu kartu di kanan memperlihatkan dfind memanggil dls lewat pipe grep.

There is a particular kind of waste that never shows up in any measurement. It is the twelfth time in a day that I type docker compose up -d, or reconstruct a --format template from memory to find out which container is holding a port. Each instance costs a few seconds and a small amount of attention, and neither of those is worth mentioning on its own.

What made me take it seriously was noticing where the attention went. Recalling a flag is not free — it interrupts whatever I was actually thinking about, and the interruption outlasts the typing by a long way. So I turned the commands I repeat most into ten short words I can type from any directory, and the interesting part turned out not to be the shortening. It was everything that had to be decided before a shortcut is worth having.

Introduction

This article is about those decisions. It is not a guide to writing shell scripts — most of these are two lines long, and the two-line ones are quoted in full precisely because there is nothing hidden in them. What is worth explaining is why each one takes the shape it does.

A. What repeated typing actually costs

Three things, and only the first is obvious.

The first is time, and it is the least important. A command that takes four seconds to type instead of one costs a few minutes over a week — real, but not enough to justify building anything.

The second is typos. A long command is a long opportunity to get something wrong, and Docker commands are unusually unforgiving about this because a mistyped flag is often still a valid command that does something else. That failure costs more than the retyping: it costs the time spent understanding why the output is not what was expected.

The third is the one that actually motivated this, and it is cognitive. Holding the shape of a dozen command patterns in working memory has a cost that is paid continuously, not per invocation. Every time I reach for a formatting template I have not used in a fortnight, I am spending attention on recall rather than on the thing I was doing. Ten short words remove that entirely, because there is nothing left to recall.

B. Why Docker commands in particular grow long

The command-line interface is built for completeness rather than for daily use, which is the correct priority for a tool of its scope and an awkward one for a person typing it repeatedly.

Listing containers is the clearest example. The default output is wide enough to wrap in most terminals, so anybody using it seriously reaches for --format — which accepts a Go template, a small language embedded inside the flag. The result is a command that is powerful, precise, and impossible to remember correctly a month later.

Compose adds a second word to everything. Networks add a third level, where the information worth having is split across network ls and network inspect and has to be joined by whoever is reading. None of this is a flaw in the tool. It is what happens when a general interface meets a specific habit.

C. A shortcut is not an alias, and the difference is the whole design

The obvious way to shorten a command is a shell alias, and it works well for exactly one situation: a human typing in an interactive shell that has read the file where the alias is defined.

Outside that situation it does not exist. A script cannot call it, because scripts do not read interactive shell configuration. sudo does not see it. A scheduled job does not see it. And because it lives inside a shell's configuration rather than on disk as a program, nothing can compose with it.

An executable file on PATH has none of those limits. It is a program, so anything that can run a program can run it — including another one of these shortcuts, which turns out to matter more than I expected when I started.

D. What the ten commands are

They fall into four groups, and the grouping is what makes them memorable.

Compose lifecycle: dcup and dcdown bring a stack up and down; dcb builds; dcx opens a shell inside a service. Reading state: dls lists running containers as a numbered table, dcls does the same for the current Compose project, and dfind searches that listing. Images: bdi saves an image to a compressed file and rdi loads one back. Networks: dcn creates, removes and lists them.

Nine of the ten are between two and forty lines. The tenth is two hundred and sixty, and the difference between those two numbers is itself one of the findings below.

E. Six obstacles in the way

Stated as a goal: make the Docker commands I repeat most into single words I can type from any directory, without losing the flexibility of the commands they replace, and in a form that other automation can call rather than reimplement.

Easy to state, awkward to build. Six obstacles stood in the way, and the first two decide the shape of everything else.

The first is that a short command has to work from anywhere, or it has only moved the problem. A script that must be run from the directory it lives in has replaced remembering a flag with remembering a path, which is not an improvement. The rejected alternative is the shell alias, and it is rejected for a specific reason rather than on principle: an alias is invisible to scripts, to sudo, and to scheduled jobs, which rules out the composition described below. What replaces it is making each file executable and placing a symbolic link on PATH, so the shortcut is a program rather than a shell convenience.

The second is that ten arbitrary names would just move the memorisation. If each shortcut is named by whatever seemed reasonable that day, the result is ten more things to remember — the exact outcome the exercise was meant to avoid. The rejected alternative is naming each script independently. What replaces it is a pattern applied consistently: a leading d for Docker, a second c when the command belongs to Compose, and then the verb. Knowing the pattern is enough to guess a name without looking it up, which is a stronger property than any individual name being clever.

The third is that a shortcut which swallows its arguments is only useful for the single most common case. Hard-coding the command makes it shorter and makes it useless the moment a service name or an extra flag is needed, at which point the full command comes back and the shortcut is abandoned. Hard-coding is the rejected alternative; what replaces it is forwarding everything through, so the shortcut is a shorter spelling of the command rather than a narrower version of it.

The fourth is that the default listing output is hard to read at a glance. Columns do not line up once names vary in length, there is no numbering to refer to a row by, and there is no total. These are small deficiencies that add up to reading carefully when glancing would do. Relying on --format alone is the rejected alternative — it fixes which fields appear but not how they are arranged. What replaces it is rendering the table directly, with aligned columns, a row number, and a count at the bottom.

The fifth is that no single Docker command answers the question I actually ask about networks. That question is "which networks exist, what subnet does each hold, and which containers are inside them", and answering it means reading one listing for names, then inspecting each network for its subnet, then inspecting again for its members — and joining three outputs by eye. Doing that manually is the rejected alternative. What replaces it is one command that performs the joins and prints a single table, with column widths computed from the data rather than guessed in advance.

The sixth is that creating a network fails late and unhelpfully when a subnet is already taken. The failure arrives from the daemon after the attempt, and the message names the conflict without naming the network that caused it — so the next step is always a manual search to find out who is holding the range. Letting the daemon reject it is the rejected alternative. What replaces it is checking both the name and the subnet beforehand, and when the subnet is taken, naming the network that holds it in the error.

The order reflects what each obstacle governs. The first two are about form — where a shortcut lives and what it is called — and they apply to all ten equally, which is why nothing else can be decided before them. The third is about the boundary of a shortcut: how much of the original command it is allowed to hide. The last three are different in kind, and they are the ones that produced actual code rather than conventions: two about reading state that Docker reports awkwardly, and one about preventing a failure rather than explaining it afterwards.

Method

A. Executable on PATH, not an alias

This is the answer to the first obstacle, and it is what makes everything after it possible.

Each script is made executable once and linked into a directory that is already on PATH. The link means the file stays where it is edited and version-controlled, while the command is available everywhere.

bash
chmod +x ~/programs/docker/*
sudo ln -s ~/programs/docker/dls /usr/local/bin/dls
# …one link per script. The file stays where it is edited;
# the command exists everywhere, including inside other scripts.

The consequence that matters most is not convenience. It is that these become composable, and the shortest script in the set is the proof.

bash
#!/bin/bash

dls | grep "$1"

That is dfind in its entirety. It searches the container listing by calling dls — not by duplicating what dls does. It can do that only because dls is a program on PATH rather than a shell alias, and this is exactly the composition that the alias approach forecloses. The same property is why larger automation elsewhere can call these instead of restating a formatting template.

B. A naming pattern rather than ten names

This is the answer to the second obstacle. The names are not individually clever, and that is deliberate.

A leading d means Docker. A c after it means the command belongs to Compose. What follows is the verb, abbreviated to the point where it is still unambiguous.

bash
dcup  dcdown  dcb  dcx  dcls     # d + c + verb  → docker compose …
dls   dfind                      # d + verb      → docker (no compose)
bdi   rdi                        # backup/restore docker image
dcn                              # docker compose network

The test of a naming scheme is whether it lets you guess. Knowing the pattern, dcb is derivable without being memorised, and so is the fact that dls and dcls must differ in scope rather than in behaviour. A person who knows three of these can work out the rest.

C. Forwarding arguments instead of hiding them

This is the answer to the third obstacle, and it is the difference between a shortcut that survives and one that gets abandoned.

bash
if [ $# -eq 0 ]; then
  docker compose up -d
else
  # Everything is forwarded: a service name, extra flags, whatever.
  docker compose up -d "$@"
fi

The quoting is the load-bearing part. "$@" preserves arguments as separate words even when they contain spaces, which is what allows dcup to be a shorter spelling of the command rather than a restricted subset of it. Without it the shortcut works until the first argument that needs quoting, and then it fails in a way that is hard to attribute.

The principle generalises: a shortcut should reduce typing, not reduce capability. The moment it cannot do something the original could, it stops being used for everything and the memorisation comes back.

D. Rendering the table rather than formatting the fields

This is the answer to the fourth obstacle. Docker will format fields; it will not lay out a table.

bash
containers=$(docker ps --format '{{.Names}}\t{{.Ports}}\t{{.Image}}')

i=1
while IFS=$'\t' read -r name ports image; do
  [ -z "$ports" ] && ports="-"          # empty reads as missing, not blank
  printf "%-3s | %-20s | %-15s | %s\n" "$i" "$name" "$ports" "$image"
  ((i++))
done <<< "$containers"

echo "Total container berjalan: $((i - 1))"

Three small decisions are doing the work. Tab-separated output is parsed rather than printed directly, so alignment is decided here instead of by the longest value. An empty port list becomes a dash, because a blank cell reads as a rendering error while a dash reads as an answer. And the counter that numbers the rows also produces the total at the end, which costs nothing and answers "how many are running" without counting lines by eye.

E. Joining what Docker reports separately

This is the answer to the fifth obstacle, and it explains why one script is two hundred and sixty lines while the rest are under forty.

The question "which networks exist, with what subnets, holding which containers" has no single command behind it. The names come from a listing; the subnet comes from inspecting each network; the members come from inspecting it again with a different template. Assembling the answer is the work.

bash
while IFS=$'\t' read -r net_id net_name; do
  subnet=$(docker network inspect "$net_id" \
    --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}')

  raw_containers=$(docker network inspect "$net_id" \
    -f '{{range $k, $v := .Containers}}{{$v.Name}}|{{$v.IPv4Address}}{{"\n"}}{{end}}')
  # …collected per network, then printed as one table
done <<< "$all_networks"

Because a network can hold any number of containers, the rows are not uniform: one network occupies one line, another occupies five. Fixed column widths would either truncate names or waste most of the terminal, so the widths are measured from the data before anything is printed.

bash
for ((j=0; j<total; j++)); do
  [ ${#NET_NAMES[$j]} -gt $col_name ] && col_name=${#NET_NAMES[$j]}
  [ ${#NET_SUBNETS[$j]} -gt $col_subnet ] && col_subnet=${#NET_SUBNETS[$j]}
done
col_name=$((col_name + 2))        # measured, then padded — never guessed

This is where the line count comes from, and it is worth being honest about the trade. Two hundred and sixty lines is a lot of shell for a listing command. It is justified here only because the question is one I ask often and the manual alternative is three commands and a mental join — but it is the point at which "shortcut" stops being an accurate description of what was built.

F. Failing before the attempt rather than after it

This is the answer to the sixth obstacle, and it is the only one that prevents a failure rather than presenting information more clearly.

Creating a network with a subnet that something else already holds fails at the daemon. The message reports the overlap without naming the network responsible, so the next move is always a search. The check moves that search before the attempt, and reports the answer as part of the refusal.

bash
for net_id in $all_networks; do
  existing_subnet=$(docker network inspect "$net_id" \
    --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}')
  if [ "$existing_subnet" = "$SUBNET" ]; then
    existing_net_name=$(docker network inspect "$net_id" --format '{{.Name}}')
    # The name is the whole point: without it, this is just a slower daemon error.
    echo "Error: subnet '$SUBNET' is already used by network '$existing_net_name'."
    exit 1
  fi
done

The name in that message is the entire value of the check. Detecting the conflict earlier saves nothing by itself — the daemon would have detected it too. What the daemon does not do is tell you which network to go and look at, and that single piece of information is the difference between a message you act on and a message that starts an investigation.

Results and Discussion

A. What actually improved

The typing saved is the smallest part, and I would not have built this for that alone. What changed is the number of things I hold in my head while working: the formatting templates are gone, the flag combinations are gone, and what remains is a naming pattern small enough to be reconstructed from three examples.

The typo class disappeared with them. A mistyped flag is often a valid command that does something unintended; a mistyped shortcut is a command that does not exist, and the shell says so immediately. Moving errors from silent to loud is worth more than moving them from frequent to rare.

The composition was the surprise. I built these for interactive use and then found other automation calling them — a deployment script that lists containers by invoking dls rather than restating the template. That only works because they are programs on PATH, which was a decision made for a different reason entirely.

B. Where the line-count difference comes from

Nine of these are short because Docker already does the work and only the spelling needed shortening. One is two hundred and sixty lines because Docker does not do the work at all, and the script is the missing implementation rather than a shorter way to invoke an existing one.

That distinction is worth noticing when deciding whether to build something. A wrapper around an existing command should be small; if it is not, the reason is usually that the command being wrapped does not answer the question being asked. The size is a signal about which of the two situations you are in, and it is the honest measure of what a shortcut is actually doing.

C. Where this stops being a good idea

These are personal tools by design. They travel with me, and anything shared or handed over uses the full commands they wrap, which every one of these shortcuts leaves intact and readable.

They are also a layer that can drift. A shortcut encoding a flag that a future Docker release changes will keep working until it does not, and the failure will be confusing in proportion to how long the shortcut has been reliable.

And the shortening has a floor. Below a certain length the name stops being derivable from the pattern and becomes something to memorise again, which is where the whole exercise began.

Conclusion

The lesson I take from this is not about Docker. It is that the cost worth optimising was the cognitive one, and it was the one I noticed last. Time and typos are measurable and minor; the continuous expense of holding a dozen command shapes in working memory is neither measurable nor minor, and it is the one that a good shortcut actually removes.

The second lesson is about where a shortcut lives. Making these programs rather than aliases was a decision about convenience that turned out to be a decision about capability — because a program can be called by another program, and a shell alias can only be typed by a person.

Terms

Shell alias
A short name for a command defined in a shell's configuration. Available to a person typing interactively; invisible to scripts, sudo and scheduled jobs.
PATH
The list of directories a shell searches for executables. A file placed there becomes a command available from any working directory.
Symbolic link
A filesystem entry pointing at another file. Used here so a script stays where it is edited while the command exists on PATH.
Go template
The small formatting language Docker's --format flag accepts, powerful enough to extract any field and awkward enough to be worth wrapping.
Compose
The Docker component that runs a multi-container application from a declarative file, and the reason half of these shortcuts carry a c.
Subnet
The address range assigned to a Docker network. Two networks cannot hold the same range, which is the conflict one of these scripts checks for in advance.
Composability
The property that lets one tool be used inside another. Here it is the consequence of being a program on PATH rather than a shell convenience.

Technologies covered

  • Bash
  • Linux
  • Docker
  • Docker Compose

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →
A browser window sends one orange path into a single web server, which routes it on to the orange staging environment; dashed paths to the blue development and green production environments stay inactive

Two commands that keep three environments apart on one machine and one web server: one sets an environment's addresses and aligns every file that stores them, rendering nginx from templates and refusing on clashes; the other points the browser at one environment.

11 min read Read more →

Related Articles

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

A single script that turns a project's Markdown documentation into a static site that opens from a folder with no server or internet: a menu, a table of contents that follows the reader, tables that never truncate, code with a copy button, and links that keep working.

7 min read Read more →
An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →