When you run multiple services on a shared Linux server, one of the most common DevOps challenges is giving a teammate or a client access to exactly one running service: nothing more, nothing less. You don't want to hand them root access, you don't want them browsing other application directories, and you don't want them accidentally taking down unrelated containers.
This article walks through a practical, real-world approach to scoped Docker container access on a shared Linux server. By the end, you will have a second Linux user who logs in and lands directly inside a specific Docker container, with visibility into the app's configuration files and real-time logs: and no access to anything else on the server.
The Setup
We have a Linux server running several Docker Compose stacks. One of those stacks is a payment service. A developer needs access to that payment service: they need to inspect the running container, read the environment configuration, and tail logs in real time. They should not have access to other stacks or host-level system files.
What we will do:
- Verify the user exists on the server
- Add them to the Docker group
- Create a restricted login shell that drops them into the target container
- Set the correct directory permissions so they can read the app config files
- Verify everything works end to end
Step 1: Check If the User Already Exists
Before creating a new user, check whether one has already been provisioned:
grep -iE 'myapp-dev' /etc/passwd
Enter fullscreen mode Exit fullscreen mode
What this does:
-
grepsearches for a pattern in a file -
-imakes the search case-insensitive -
-Eenables extended regular expressions (useful if you want to match patterns likemyapp|otherapp) -
/etc/passwdis the file Linux uses to store user account information: every user on the system has a line here
Example output:
myapp-dev:x:1002:1002:,,,:/home/myapp-dev:/bin/bash
Enter fullscreen mode Exit fullscreen mode
Each field separated by : means:
-
myapp-dev: username -
x: password is stored in/etc/shadow(hashed) -
1002: user ID (UID) -
1002: primary group ID (GID) -
,,,: optional comment/GECOS field (name, phone, etc.) -
/home/myapp-dev: home directory -
/bin/bash: login shell
If the user does not exist, create them:
useradd -m -s /bin/bash myapp-dev
passwd myapp-dev
Enter fullscreen mode Exit fullscreen mode
-
useradd: creates a new user account -
-m: creates a home directory at/home/myapp-dev -
-s /bin/bash: sets bash as the default shell -
passwd myapp-dev: sets a password for the user interactively
Step 2: Check the User's Current Groups
id myapp-dev
Enter fullscreen mode Exit fullscreen mode
What this does:
id prints the user ID, primary group ID, and all supplementary groups a user belongs to.
Example output:
uid=1002(myapp-dev) gid=1002(myapp-dev) groups=1002(myapp-dev),1003(myapp)
Enter fullscreen mode Exit fullscreen mode
This tells you the user exists but is not yet in the docker group, meaning they cannot interact with Docker at all yet.
Step 3: Add the User to the Docker Group
usermod -aG docker myapp-dev
Enter fullscreen mode Exit fullscreen mode
What this does:
-
usermod: modifies an existing user account -
-a: append (do not replace existing groups) -
-G docker: add the user to thedockersupplementary group
Why this matters: Docker's socket (
/var/run/docker.sock) is only accessible to root and members of thedockergroup. Without this step, the user cannot run anydockercommands at all.Security note: Adding a user to the
dockergroup effectively gives them root-equivalent access to the host via Docker (e.g. they could mount the host filesystem). For a trusted developer in a dev environment this is acceptable. In production, consider rootless Docker or more granular controls.
Verify the group was added:
id myapp-dev
Enter fullscreen mode Exit fullscreen mode
You should now see docker in their groups:
uid=1002(myapp-dev) gid=1002(myapp-dev) groups=1002(myapp-dev),998(docker),1003(myapp)
Enter fullscreen mode Exit fullscreen mode
Step 4: Create a Restricted Login Shell
Instead of giving the user a full bash session on the host, we create a small shell script that immediately drops them into the target container when they log in.
cat > /usr/local/bin/myapp-shell << 'EOF'
#!/bin/bash
exec docker exec -it my-payment-container /bin/sh
EOF
Enter fullscreen mode Exit fullscreen mode
What this does:
-
cat >: writes the following content into a file -
<< 'EOF' ... EOF: a heredoc block; everything between the twoEOFmarkers is written as-is to the file -
#!/bin/bash: shebang line; tells the OS to run this script with bash -
exec: replaces the current shell process with the docker exec command (no parent shell remains) -
docker exec: runs a command inside an already-running container -
-i: keeps STDIN open (interactive mode) -
-t: allocates a pseudo-TTY (gives you a proper terminal experience) -
my-payment-container: the name of your target container (get this fromdocker ps) -
/bin/sh: the shell to open inside the container (use/bin/bashif the container image has bash installed)
Make the script executable:
chmod +x /usr/local/bin/myapp-shell
Enter fullscreen mode Exit fullscreen mode
-
chmod: changes file permissions -
+x: adds the execute bit, making it runnable as a program
Alternative shells you could use: If the container has bash, replace
/bin/shwith/bin/bashfor a better experience. Some minimal images (Alpine-based) only have/bin/sh.
Step 5: Set the Restricted Shell as the User's Login Shell
usermod -s /usr/local/bin/myapp-shell myapp-dev
Enter fullscreen mode Exit fullscreen mode
-
usermod: modifies the user account -
-s: sets the login shell -
/usr/local/bin/myapp-shell: the script we just created
Now whenever myapp-dev logs in via SSH or su, instead of getting a bash prompt on the host, they are immediately placed inside the container.
Verify the change:
grep myapp-dev /etc/passwd
Enter fullscreen mode Exit fullscreen mode
The last field should now show:
myapp-dev:x:1002:1002:,,,:/home/myapp-dev:/usr/local/bin/myapp-shell
Enter fullscreen mode Exit fullscreen mode
Step 6: Give the User Visibility Into the App Directory
By default the user's home directory is /home/myapp-dev. The actual application files (.env, docker-compose.yml, logs) live in a different directory, for example /home/apps/my-payment-service. We need to:
- Give the user's group read access to that directory
- Change their home directory so they land there on login
Set group ownership and permissions:
chown root:myapp /home/apps/my-payment-service
chmod 750 /home/apps/my-payment-service
chmod g+r /home/apps/my-payment-service/.env
Enter fullscreen mode Exit fullscreen mode
-
chown: changes file/directory ownership -
root:myapp: sets owner to root, group tomyapp(the groupmyapp-devalready belongs to) -
chmod 750: owner gets read/write/execute, group gets read/execute, others get nothing -
g+r: adds read permission for the group on the.envfile specifically
Change the user's home directory:
If the user has an active session, kill it first:
pkill -u myapp-dev
Enter fullscreen mode Exit fullscreen mode
-
pkill: sends a signal to processes matching a criterion -
-u myapp-dev: matches all processes owned bymyapp-dev - This sends
SIGTERMby default, gracefully terminating their session
Then update the home directory:
usermod -d /home/apps/my-payment-service myapp-dev
Enter fullscreen mode Exit fullscreen mode
-
-d: sets the user's home directory
Now when myapp-dev logs in they land directly at /home/apps/my-payment-service and can see docker-compose.yml, .env, and any log directories.
Step 7: Verify End to End
Switch to the user and test:
su - myapp-dev
Enter fullscreen mode Exit fullscreen mode
-
su: substitute user (switch to another user) -
-: loads the full login environment for that user (home directory, shell, environment variables)
If the login shell is set to the container script, you will land directly inside the container:
/ $
Enter fullscreen mode Exit fullscreen mode
If the login shell is set to bash, you will land in the app directory:
myapp-dev@server:/home/apps/my-payment-service$ ls -a
. .. .env docker-compose.yml live-logs
Enter fullscreen mode Exit fullscreen mode
From there, the user can also interact with Docker normally:
# See running containers in this stack
docker compose -f docker-compose.yml ps
# Tail real-time logs
docker logs -f my-payment-container
# Shell into the container manually
docker exec -it my-payment-container /bin/sh
Enter fullscreen mode Exit fullscreen mode
What Both Users Share
Once this is set up, both root and myapp-dev are operating on the same running containers. This means:
-
docker compose ps: both see identical container states -
docker logs -f my-payment-container: both stream the same real-time log output simultaneously -
docker exec -it my-payment-container /bin/sh: both can open shells inside the container at the same time - Any restart or config change made by either user affects the same running service
This is the intended behaviour for a shared dev environment: one running stack, multiple people with visibility into it.
Understanding the Two Access Modes
Depending on how you configure the login shell, you get two different access patterns:
| Mode | Login Shell | Where They Land | What They See |
|---|---|---|---|
| Container-only | /usr/local/bin/myapp-shell |
Inside the container filesystem | Container files only, no host files |
| Host directory scoped | /bin/bash |
App directory on the host |
.env, docker-compose.yml, logs, and Docker CLI |
For a developer who needs to read config, tail logs, and inspect the compose setup: the host directory scoped mode is more practical. For a user who only needs to run commands inside the application process: the container-only mode is more restrictive and appropriate.
Quick Reference: All Commands
# Check if user exists
grep -iE 'myapp-dev' /etc/passwd
# Create user if needed
useradd -m -s /bin/bash myapp-dev
passwd myapp-dev
# Check current groups
id myapp-dev
# Add to docker group
usermod -aG docker myapp-dev
# Create restricted container shell
cat > /usr/local/bin/myapp-shell << 'EOF'
#!/bin/bash
exec docker exec -it my-payment-container /bin/sh
EOF
chmod +x /usr/local/bin/myapp-shell
# Set as login shell
usermod -s /usr/local/bin/myapp-shell myapp-dev
# Or set bash as login shell (host access mode)
usermod -s /bin/bash myapp-dev
# Set app directory permissions
chown root:myapp /home/apps/my-payment-service
chmod 750 /home/apps/my-payment-service
# Kill active session before modifying home dir
pkill -u myapp-dev
# Change home directory
usermod -d /home/apps/my-payment-service myapp-dev
# Test login
su - myapp-dev
Enter fullscreen mode Exit fullscreen mode
Conclusion
With a few targeted usermod commands, a wrapper shell script, and careful directory permissions, you can give a developer or client scoped access to a specific Docker service running on a shared server: without handing them the keys to everything else.
This pattern is particularly useful in multi-tenant dev environments where multiple application stacks share a single droplet or VM and you need to delegate access per service without spinning up separate servers or managing complex ACLs.
For production environments, consider extending this pattern with:
- Rootless Docker: each user runs their own Docker daemon with no shared socket
-
Sudoers rules: restrict which Docker commands a user can run via
sudo -
Audit logging: use
auditdto log all Docker exec sessions for the scoped user
Thanks for reading...
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.