Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Ignore Sudo in a Shell Script If You're Running as Root

ignoring-sudo-in-a-shell-script-if-you-are-running-as-root.jpg

This can be used to run a command as root outside and inside of Docker where sudo might not be available.

Quick Jump:

Prefer video? Here it is on YouTube. It covers running some of the commands seen in this post and adds more context to the examples.

This is a pretty specific use case but let’s say you have a script that installs tools to /usr/local/bin as well as installs tools with something like pip where you may want to use --user so your tools end up in ~/.local/bin as a non-root user.

Not only that but you want this script to work on your main system as well as working inside of Docker where sudo won’t be available.

This came up recently where I wanted to install tools like kubectl, argocd and others to /usr/local/bin but also pip install a few things as the local non-root user. It was an install script to help onboard developers with macOS and various distros of Linux but it also needed to work inside of a Docker base image to run in CI.

So you kind of have to solve the problem of wanting to run certain commands with sudo but you only want to invoke sudo if you’re not already escalated as the root user.

# This Could Have Been More Straight Forward

If the local pip install use case wasn’t there this would be no problem at all.

Your script could install and copy files to /usr/local/bin and it would be expected that you run it with sudo ./install-tools on your personal machine and then inside of Docker you’d run ./install-tools since you’re already the root user.

The advantage here is your script doesn’t need to use sudo for a bunch of commands and I like this because it feels less rude. Either the script fails because you don’t have root privileges and can’t write to /usr/local/bin or you’ve explicitly given the script permission to write to /usr/local/bin by using sudo as the end user.

# Overriding sudo in Your Script

Sometimes real life happens and you can’t use the ideal solution because of a specific use case, in which case you can do something like this.

You can create this file as demo, chmod +x demo and then run it with ./demo:

#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset

# Source: https://askubuntu.com/a/937596
sudo() {
  [[ "${EUID}" == 0 ]] || set -- command sudo "${@}"
  # If you're the root user then short circuit out of this OR condition. This
  # is the Docker use case.
  #
  # $EUID is the effective user. Unlike $USER, $EUID should be defined in most
  # Docker images. I've seen it available in every Debian Slim based image.
  #
  # Otherwise, take whatever arguments you passed into this function and then
  # run it through the sudo command. This is the personal machine use case and
  # is the other side of the OR condition.
  #
  # `command` is a bash command to execute or display info about a command and
  # we're using `set` to modify the args of ${@}.

  "${@}"
  # Run whatever arguments you passed into this function. This would be whatever
  # the command was but without `sudo`.
}

sudo cp demo /usr/local/bin

echo "Hello world"

You can also confirm it works in Docker by running:

docker container run --rm -v "${PWD}:/app" -w "/app" debian:bookworm-slim ./demo

That will copy this demo script to /usr/local/bin and now you can run demo to run it. This is meant to be used for demonstration purposes, installing “real” tools may involve curling them down, unzipping them and then moving their binary to /usr/local/bin.

What’s neat about this approach is:

  • On your personal system it will work and it’ll prompt you to either enter your user password or passively work if you have passwordless sudo enabled
  • You don’t need to litter your script with a bunch of if conditions to check if the sudo command exists, this is a problem if you’re installing 10 tools
  • On systems without sudo such as within Docker, it will run the command without sudo and “just work” if it’s being run as the root user

That gives us the best of both worlds in the sense that it runs the same in both cases. The downside is you need to internally use sudo within the script where ever you need it which feels a little anti-user in behavior.

By the way, if you’re interested in learning more about how set -- works I made a separate post and video about that here.

# Maybe There’s Another Way With SUDO_USER

If you invoke a command with sudo then the $SUDO_USER environment variable will be set to the user who invoked the command such as nick.

This could let you avoid needing to override sudo like we did above and also avoid using sudo in your script. Instead you’d run the script itself with sudo like the first option.

The basic idea here is if $SUDO_USER is undefined then we know the script was run without sudo so we can expect it’s the root user. This would be the case where things are running in Docker, but of course it’s not limited to Docker.

If that env var is set then on your personal box things will install to /usr/local/bin no problem because you’re acting as the root user.

Then for the ~/.local/bin pip install commands with --user or whatever else you need to do as the local non-root user, you can use su to switch to the $SUDO_USER beforehand.

# Which Method Should You Use?

Like most things, it’ll depend on your use case. The first option is ideal in my opinion if all you have is system wide tools to install.

If you have a lot of locally installed tools mixed in then I think the 2nd option is more maintainable since you only need to define that sudo function once and the rest of the script can use sudo as normal. You don’t need to think about it much beyond that.

If you have a small handful of locally installed tools then the 3rd option feels more “correct”.

It also depends on where this script is being used. If it’s internally within an organization where everyone is being onboarded in the same way that’s a bit different than a script that’s open source.

Personally on a moral / ethical level I like option 3 the best because this shows the most clear intent that your script needs root privileges before you run it. You may also want to consider splitting out your logic into separate scripts to differentiate what needs and doesn’t need root access.

I rolled out option 2 for a client because it made the most sense for their use case. The good news is if our use case changes then we can change the implementation. In the grand scheme of things this isn’t a big deal.

# Demo Video

Timestamps

  • 0:43 – A use case
  • 3:16 – This could have been more straight forward
  • 4:12 – Overriding sudo in your script
  • 5:53 – UID vs EUID and USER
  • 9:00 – Using command and set to dynamically run commands as sudo
  • 11:26 – Confirming it works with Docker
  • 12:34 – The override approach is pretty neat
  • 13:47 – There’s another way using SUDO_USER
  • 16:00 – Which method should you use?

Which option would you pick? Let me know below.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per year (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments