DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    DockerLesson 5 of 14
    Your progress · 0%
    On this page
    1. Somebody already packaged it
    2. Databases in a minute
    3. Tools that leave nothing behind
    4. Any language version
    5. Choosing an image you can trust
    ↖ Course roadmap
    Lesson 5 · Third-party images

    Other people's images: databases, tools and runtimes

    Run Postgres and Redis without installing them, use CLI tools that vanish when done, run code with any language version, and pick images you can trust.

    30 min read · beginner · hands-on

    By the end of this lesson you'll be able to

    0 of 5 complete

    Somebody already packaged it

    Need Postgres for a project, or a different Python version, or jq just once? Don't install it. Run the image someone already built, tested and keeps updated:

    Services run in the background; tools run once with --rm. Nothing gets installed on the machine itself.© Diagram: DevOps Academy

    Services run in the background; tools run once with --rm. Nothing gets installed on the machine itself.© Diagram: DevOps Academy

    Databases in a minute

    $ docker run -d --name db -e POSTGRES_PASSWORD=devpass -p 127.0.0.1:5432:5432 postgres:17-alpine
    $ docker exec db pg_isready -h 127.0.0.1 -U postgres    # ready yet? (over TCP: see below)
    127.0.0.1:5432 - accepting connections
    $ docker exec db psql -h 127.0.0.1 -U postgres -c "create table orders(id serial primary key, item text);
        insert into orders(item) values ('book'),('pen'); select * from orders;"
    CREATE TABLE
    INSERT 0 2
     id | item
    ----+------
      1 | book
      2 | pen
    (2 rows)
    
    docker run -d --name db -e POSTGRES_PASSWORD=devpass -p 127.0.0.1:5432:5432 postgres:17-alpine
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name dbname the container
    • -e POSTGRES_PASSWORD=devpassset an environment variable inside
    • -p 127.0.0.1:5432:5432publish a port, host:container
    • postgres:17-alpinethe image to start
    docker exec db pg_isready -h 127.0.0.1 -U postgres
    • execrun a command in a running container
    • dbthe container
    • pg_isready -h 127.0.0.1 -U postgresthe command to run inside it
    docker exec db psql -h 127.0.0.1 -U postgres -c "create table orders(id serial primary key, item text);
    • psql -h 127.0.0.1 -U postgres -c "create table orders(id serial primary key, item text);the command to run inside it

    A database needs a moment to initialise, so scripts wait with pg_isready (or docker compose health checks, lesson 9) instead of sleep. There's a trap here: on its first start, the official image runs a temporary server, listening only on a local socket, to create the database, then restarts. A plain pg_isready says "ready" during that temporary phase and the next command fails with connection to server on socket … failed. Checking over TCP with -h 127.0.0.1 waits for the real server, because the temporary one never opens a TCP port. Forget the password and the container stops straight away, with the reason in its logs:

    $ docker run -d --name db2 postgres:17-alpine
    $ docker ps -a --filter name=db2 --format '{{.Names}} {{.Status}}'
    db2 Exited (1) 3 seconds ago
    $ docker logs db2
    Error: Database is uninitialized and superuser password is not specified.
           You must specify POSTGRES_PASSWORD to a non-empty value for the
           superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run".
    
    docker run -d --name db2 postgres:17-alpine
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name db2name the container
    • postgres:17-alpinethe image to start
    docker ps -a --filter name=db2 --format '{{.Names}} {{.Status}}'
    • pslist running containers
    • -ainclude stopped ones too
    • --filter name=db2show only matching items
    • --format '{{.Names}} {{.Status}}'print only these fields (Go template)
    docker logs db2
    • logsshow a container’s output
    • db2the container

    Redis is even quicker:

    $ docker run -d --name cache -p 127.0.0.1:6379:6379 redis:8-alpine
    $ docker exec cache redis-cli ping
    PONG
    $ docker exec cache redis-cli set visits 41 && docker exec cache redis-cli incr visits
    OK
    42
    
    docker run -d --name cache -p 127.0.0.1:6379:6379 redis:8-alpine
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name cachename the container
    • -p 127.0.0.1:6379:6379publish a port, host:container
    • redis:8-alpinethe image to start
    docker exec cache redis-cli ping
    • execrun a command in a running container
    • cachethe container
    • redis-cli pingthe command to run inside it
    docker exec cache redis-cli set visits 41 && docker exec cache redis-cli incr visits
    • redis-cli set visits 41the command to run inside it
    • &&run the next command only if this one worked
    • redis-cli incr visitsthe command to run inside it

    127.0.0.1: in -p keeps them reachable only from your own machine. These databases keep their data in the container's writable layer, so docker rm deletes it; lesson 6 fixes that with volumes.

    Tools that leave nothing behind

    Run a tool once with --rm, feed it a file through standard input with -i, and nothing is left afterwards:

    $ docker run --rm -i ghcr.io/jqlang/jq:1.8.1 '.orders[].item' < orders.json
    "book"
    "pen"
    $ docker run --rm curlimages/curl:8.16.0 -sI https://example.com | head -1
    HTTP/2 200
    $ docker run --rm --network host postgres:17-alpine \
        psql "postgresql://postgres:devpass@127.0.0.1:5432/postgres" -c "select count(*) from orders;"
    
    docker run --rm -i ghcr.io/jqlang/jq:1.8.1 '.orders[].item' < orders.json
    • dockerbuild and run containers
    • runstart a new container
    • --rmdelete the container when it exits
    • -ikeep standard input open
    • ghcr.io/jqlang/jq:1.8.1the image to start
    • '.orders[].item'the command run inside, instead of the image’s default
    • <read input from a file
    • orders.jsonthe command run inside, instead of the image’s default
    docker run --rm curlimages/curl:8.16.0 -sI https://example.com | head -1
    • curlimages/curl:8.16.0the image to start
    • https://example.comthe command run inside, instead of the image’s default
    • |pipe: send output to the next command
    • headshow the first lines
    docker run --rm --network host postgres:17-alpine \
    • --network hostwhich network to join
    • postgres:17-alpinethe image to start
    • \the command run inside, instead of the image’s default

    The last one borrows the Postgres image only for its psql client, so you get the exact client version that matches the server. The images stay cached; next time they start instantly.

    Any language version

    Mount your folder into a runtime image and run your code with an exact version, without installing it:

    $ docker run --rm -v "$PWD":/work -w /work python:3.13-slim python total.py
    total: 13.7
    $ docker run --rm python:3.13-slim python --version
    Python 3.13.16
    $ docker run --rm node:22-alpine node -e 'console.log(process.version)'
    v22.23.3
    
    docker run --rm -v "$PWD":/work -w /work python:3.13-slim python total.py
    • dockerbuild and run containers
    • runstart a new container
    • --rmdelete the container when it exits
    • -v "$PWD":/workmount a volume or folder, source:path-inside
    • -w /workworking directory inside the container
    • python:3.13-slimthe image to start
    • python total.pythe command run inside, instead of the image’s default
    docker run --rm python:3.13-slim python --version
    • python --versionthe command run inside, instead of the image’s default
    docker run --rm node:22-alpine node -e 'console.log(process.version)'
    • node:22-alpinethe image to start
    • node -e 'console.log(process.version)'the command run inside, instead of the image’s default

    -v "$PWD":/work makes the current folder appear at /work inside, and -w /work starts the command there. Need to check something against Python 3.12 as well? Change the tag. No conflicts with what's on your machine.

    Choosing an image you can trust

    Look forWhy
    Docker Official Image or Verified Publisher badgemaintained, rebuilt for security fixes, documented
    a pinned tag: postgres:17-alpine, not postgreslatest changes under you; a rebuild could jump a major version
    -slim or -alpine variantssmaller download and fewer packages with vulnerabilities
    the image page's environment variables and volumeshow to configure it and where it keeps data
    a linked source repositoryyou can see how it's built

    -alpine uses musl instead of glibc. That's fine for most images, but some compiled language packages behave differently, so -slim (Debian, smaller) is the safer choice for Python and Node apps.

    Dev passwords stay on dev

    POSTGRES_PASSWORD=devpass is fine for a database bound to 127.0.0.1 on your laptop. Anywhere else, read the password from a file or a secret (POSTGRES_PASSWORD_FILE is supported by the official image; lesson 11 covers secrets) and never commit it.

    ⌘
    Mini mission

    A disposable dev stack

    Without installing anything but Docker, run Postgres and Redis for local development, bound to your machine only; create a table and some rows; count visits in Redis; query the database with a psql client from an image; and summarise a JSON file with jq and Python run from images.

    Input: DockerOutput: A table with two rows, a Redis counter, and jq and Python output, then everything removed
    Reveal one safe solution
    $ docker run -d --name db -e POSTGRES_PASSWORD=devpass -p 127.0.0.1:5432:5432 postgres:17-alpine
    $ until docker exec db pg_isready -h 127.0.0.1 -U postgres >/dev/null; do sleep 1; done
    $ docker exec db psql -h 127.0.0.1 -U postgres -c "create table orders(id serial, item text); insert into orders(item) values ('book'),('pen');"
    $ docker run -d --name cache -p 127.0.0.1:6379:6379 redis:8-alpine
    $ docker exec cache redis-cli incr visits
    1
    $ printf '{"orders":[{"item":"book","price":12.5},{"item":"pen","price":1.2}]}\n' > orders.json
    $ printf 'import json\nd=json.load(open("orders.json"))\nprint("total:", sum(o["price"] for o in d["orders"]))\n' > total.py
    $ docker run --rm -i ghcr.io/jqlang/jq:1.8.1 '[.orders[].price] | add' < orders.json
    13.7
    $ docker run --rm -v "$PWD":/work -w /work python:3.13-slim python total.py
    total: 13.7
    $ docker rm -f db cache
    
    docker run -d --name db -e POSTGRES_PASSWORD=devpass -p 127.0.0.1:5432:5432 postgres:17-alpine
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name dbname the container
    • -e POSTGRES_PASSWORD=devpassset an environment variable inside
    • -p 127.0.0.1:5432:5432publish a port, host:container
    • postgres:17-alpinethe image to start
    until docker exec db pg_isready -h 127.0.0.1 -U postgres >/dev/null; do sleep 1; done
    • dostart of what the loop runs each time
    • doneend of the loop
    docker exec db psql -h 127.0.0.1 -U postgres -c "create table orders(id serial, item text); insert into orders(item) values ('book'),('pen');"
    • execrun a command in a running container
    • dbthe container
    • psql -h 127.0.0.1 -U postgres -c "create table orders(id serial, item text); insert into orders(item) values ('book'),('pen');"the command to run inside it
    docker run -d --name cache -p 127.0.0.1:6379:6379 redis:8-alpine
    • --name cachename the container
    • -p 127.0.0.1:6379:6379publish a port, host:container
    • redis:8-alpinethe image to start
    docker exec cache redis-cli incr visits
    • cachethe container
    • redis-cli incr visitsthe command to run inside it
    printf '{"orders":[{"item":"book","price":12.5},{"item":"pen","price":1.2}]}\n' > orders.json
    • >write output to a file, replacing it
    docker run --rm -i ghcr.io/jqlang/jq:1.8.1 '[.orders[].price] | add' < orders.json
    • --rmdelete the container when it exits
    • -ikeep standard input open
    • ghcr.io/jqlang/jq:1.8.1the image to start
    • '[.orders[].price] | add'the command run inside, instead of the image’s default
    • <read input from a file
    • orders.jsonthe command run inside, instead of the image’s default
    docker run --rm -v "$PWD":/work -w /work python:3.13-slim python total.py
    • -v "$PWD":/workmount a volume or folder, source:path-inside
    • -w /workworking directory inside the container
    • python:3.13-slimthe image to start
    • python total.pythe command run inside, instead of the image’s default
    docker rm -f db cache
    • rmdelete a container
    • -fstop it first if running

    until … pg_isready -h 127.0.0.1 waits for the real server, not the temporary first-start one, without guessing a sleep. When you're done, docker rm -f removes both containers, and with them their data: next lesson, you'll keep it.

    Try it yourself

    0 of 6 steps done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    You run docker run -d postgres:17-alpine and it exits immediately. Why?

    Go deeper

    Official reading for this lesson

    • ToolPostgres official imageDocker Hub ↗
    • ToolRedis official imageDocker Hub ↗
    • DocsDocker Docs: Docker Official ImagesDocker Docs ↗
    • Tooljq official imagejqlang ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousDocker basics: images, containers, ports, logs and execNext lesson →Keeping data: volumes, bind mounts and backups