docker-bastion/examples/docker-mailserver-vps
Fabian @ Blax Software 964bb394db feat(examples): docker-mailserver "fake VPS" scoped to one directory
A disposable jail container that bind-mounts only one directory; the
bastion's FORCE_COMMAND drops every SSH session into an interactive shell
inside it. The jail's own root fs is throwaway image data, so the only host
data reachable over the session is the mounted directory. Documents the
docker-socket tradeoff and the read-only / no-socket hardened variants.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:35:47 +02:00
..
Dockerfile feat(examples): docker-mailserver "fake VPS" scoped to one directory 2026-06-03 13:35:47 +02:00
README.md feat(examples): docker-mailserver "fake VPS" scoped to one directory 2026-06-03 13:35:47 +02:00
docker-compose.yml feat(examples): docker-mailserver "fake VPS" scoped to one directory 2026-06-03 13:35:47 +02:00

README.md

docker-mailserver "fake VPS" (one directory, nothing else)

A copy-paste setup that gives an operator (or an agent) an interactive SSH shell that feels like a VPS — but the only real host data on it is your docker-mailserver directory. The rest of the server's filesystem isn't mounted, so there's nothing else to see.

Contrast with the sibling ../docker-mailserver/ example: that one is broker mode — a fixed menu of setup email … commands, no shell. This one is the opposite — a full shell, scoped to one directory, for when you want to edit configs and drive docker compose yourself.

What you get

ssh -p 2222 agent@your-host
  └─ bastion-vps      key auth; forces every session into the jail
       └─ docker exec -it dms-jail bash
            └─ dms-jail   disposable alpine + docker/compose/editors/git
                          ONLY real mount: your docker-mailserver directory
                          you land in it, read-write

Inside the shell you can vim mailserver.env, docker compose up -d, docker exec mailserver setup email add …, git pull, etc.

Setup

  1. Paths are preset to /srv/docker-mailserver (host and in-jail, identical). If your directory is elsewhere, edit the two paths in docker-compose.yml and keep both sides of the : identical — a different in-jail path still lets you read/edit files, but docker compose from the jail would resolve the stack's bind mounts to a host path that doesn't exist and bring the mailserver up with empty data.
  2. Add your key:
    mkdir -p docker-data/bastion/users.d
    cp ~/.ssh/id_ed25519.pub docker-data/bastion/users.d/me.pub
    
  3. Launch:
    docker compose up -d --build
    ssh -p 2222 agent@your-host
    

Security note — the docker-socket tradeoff

To let you run docker compose / restart the mailserver from the shell, the jail is given the host docker socket. A docker socket is host-root equivalent — from inside the shell, docker run -v /:/host alpine sh would expose the whole host. So this is a practical one-directory VPS for trusted operators, not a hard sandbox an adversary can't escape.

Want a real "only this directory exists" boundary instead? Remove the /var/run/docker.sock mount from both services in the compose file. You lose in-shell docker compose/restart (manage the stack from a separate broker-mode bastion — see ../docker-mailserver/), but the shell then genuinely cannot reach anything but the mounted directory.

The auth boundary is your SSH key (users.d/*.pub) plus the bastion's ForceCommand, which clients cannot bypass — a session can only ever become the jail shell, never anything else.