A devcontainer template that turns any devcontainer into a first-class, persistent node on your Tailscale tailnet. Once the container is running you can SSH into it from any device on your tailnet, keep long-running work in a tmux session, and build Docker images inside the container.
Tailscale is optional. If you leave
TAILSCALE_AUTHKEYempty (or skip creating a.envfile), the container starts normally without joining a tailnet. SSH, tmux, Node.js, Docker-in-Docker, and all other tooling still work.
The fastest way — one command that guides you through everything:
curl -fsSL https://raw.githubusercontent.com/ciberado/devcontainer-template/main/initproject.sh | bashThe script will:
- Ask for a project directory (default: current directory)
- Set up git, prompting for your name/email if not globally configured
- Run
git subtree addto pull in the template - Optionally configure Tailscale (leave the key empty to skip)
- Print the final
devcontainer upandsshcommands
If you prefer to do it yourself, add the template to your existing project
repository using git subtree:
git subtree add --prefix=.devcontainer \
https://github.com/ciberado/devcontainer-template.git \
main --squashThen build and start the container with the devcontainer CLI:
devcontainer up --workspace-folder .Once it's running, open a shell inside the container:
devcontainer exec --workspace-folder . bashThat's it — you're inside the container, in a tmux session.
To join your tailnet, create a .env file from the example:
cp .devcontainer/.env.example .devcontainer/.envEdit .devcontainer/.env with your Tailscale auth key and project name:
TAILSCALE_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxx
PROJECT_NAME=my-projectRebuild the container to pick up the env vars:
devcontainer up --workspace-folder .Once connected, SSH in from anywhere on your tailnet:
ssh dev@vs-my-projectWhen you SSH in, you're automatically dropped into a tmux session called dev.
Detach with Ctrl+B d, close the connection, and reattach later — your work
survives.
- Docker
- The
devcontainerCLI (npm install -g @devcontainers/cli) - Tailscale account (optional — only needed for tailnet access)
- Tailscale auth key (optional — reusable or ephemeral)
You ── devcontainer up ── Docker ── Ubuntu 24.04 ── (tailscale up) ── your tailnet
│ │
├─ Node 22 ├─ SSH enabled
├─ tmux ├─ hostname: vs-<PROJECT_NAME>
├─ Docker-in-Docker
└─ OpenSSH server
When TAILSCALE_AUTHKEY is set, the container joins your tailnet automatically
on every start using the pre-provisioned auth key from .env. The SSH server
lets you connect from anywhere on your tailnet:
ssh dev@vs-my-project| File | Role |
|---|---|
initproject.sh |
Bootstrap script. Run via curl … | bash to guide you through creating a new project. |
Dockerfile |
Builds the image: Ubuntu 24.04, Node 22 (via nvm), TypeScript tooling, tmux. Renames the default user to dev. |
devcontainer.json |
Devcontainer orchestrator. Sets runArgs for Docker (hostname, env file, container name), loads features (Tailscale, GitHub CLI, Docker-in-Docker, OpenSSH), wires lifecycle hooks. |
start-tailscale.sh |
Runs on every container start. If TAILSCALE_AUTHKEY is set: starts tailscaled, authenticates to your tailnet, enables SSH + MagicDNS, advertises tags. If empty: skipped gracefully. |
post-create.sh |
Runs once after the image is built. Installs oh-my-tmux and the Fresh editor. |
README.md |
This file. |
Two behaviors are wired into .bashrc and .zshrc during the Docker build:
- Auto-cd: if the shell lands in
$HOME, it automaticallycds into the workspace directory under/workspaces/. This matters when you SSH in. - Tmux auto-attach: on any interactive shell (terminal or SSH),
it attaches to a tmux session named
dev, creating it if it doesn't exist. Already inside tmux? It skips.
Pull the latest version from the template repo:
git subtree pull --prefix=.devcontainer \
https://github.com/ciberado/devcontainer-template.git \
main --squashIf you have local customizations inside .devcontainer/, commit them first.
The pull is a git merge — it handles conflicts normally. After the pull,
rebuild the container to pick up any image changes.
Even after pulling, each project must have:
| Item | Created by | Notes |
|---|---|---|
.devcontainer/.env |
Optional (auto-created if missing) | Per-project auth key + name. Gitignored. |
.devcontainer/.env.example |
git subtree add |
Template reference. Commit to git. |
.devcontainer/ |
git subtree add |
The whole template. |
Everything else — .gitignore entries (.env, .codewhale/), the Tailscale
auth key, Docker container naming — comes from the template or your .env.
| Variable | Required | Description |
|---|---|---|
TAILSCALE_AUTHKEY |
No | Tailscale auth key (reusable or ephemeral). Leave empty to skip Tailscale. Generate at admin console. |
PROJECT_NAME |
No | Used for the Tailscale hostname: vs-<PROJECT_NAME>. Only meaningful when TAILSCALE_AUTHKEY is set. |
Docker reads these from .env via --env-file at container start — they are
not baked into the image. If .env doesn't exist, it's created automatically
as an empty file and the container starts without Tailscale.
The container is created with --name vs-<workspace-basename> (e.g.,
vs-my-project). This makes docker ps output predictable and lets you run
commands like:
docker exec vs-my-project tailscale statusThe container advertises itself with tag: tag:vscode, tag:container. You
can use these in your Tailscale ACLs:
{
"tagOwners": {
"tag:vscode": ["autogroup:admin"],
"tag:container": ["autogroup:admin"]
},
"acls": [
{"action": "accept", "src": ["tag:vscode"], "dst": ["*:*"]}
]
}MIT