Remote Job Submission over SSH
ARC submits electronic structure jobs to a remote cluster by opening an SSH session with paramiko. This page describes how to authenticate that session, how host keys are verified, and how to do both from inside the Docker image.
If ARC and the electronic structure software run on the same machine, you do not
need any of this - define a server named local instead, as described in
Running ARC.
Server Settings
Remote servers are declared in the servers dictionary of your personal
~/.arc/settings.py:
servers = {
'cluster_a': {
'cluster_soft': 'Slurm',
'address': 'login.cluster.edu',
'path': '/home',
'un': 'my_user',
},
}
The keys that affect the SSH connection itself are:
address- the hostname ARC connects to;un- the username on the remote machine;path- the absolute path on the server holding the user directories, under which ARC runs (<path>/<un>/runs/ARC_Projects/). Without it the remote job directories are relative to the SSH login directory, which an input file that must name a path on the server, such as Orca NEB’s, cannot follow; ARC reports such a server at startup;key- optional. The path, on the machine running ARC, of the SSH private key to authenticate with;strict_host_key_checking- optional,Falseby default. See Host Key Verification.
Authentication
ARC hands key to paramiko as the identity to authenticate with. There are two
supported ways to authenticate, and the choice is made simply by whether you set
key.
Using an ssh-agent (preferred)
Omit key entirely. paramiko then looks for a running ssh-agent, and after that
for the default key paths ~/.ssh/id_rsa, ~/.ssh/id_ecdsa and
~/.ssh/id_ed25519. There is no id_dsa in that list; paramiko 4 dropped DSA.
servers = {
'cluster_a': {
'cluster_soft': 'Slurm',
'address': 'login.cluster.edu',
'path': '/home',
'un': 'my_user',
},
}
Add the key to your agent once per login session:
ssh-add ~/.ssh/id_ed25519
This is the preferred route. Passphrase-protected keys keep working, because the agent holds the decrypted key and ARC never has to prompt for the passphrase. In a container it also means the key material itself never has to enter the container.
Using a key file
Set key to the path of the private key:
servers = {
'cluster_a': {
'cluster_soft': 'Slurm',
'address': 'login.cluster.edu',
'path': '/home',
'un': 'my_user',
'key': '/home/my_user/.ssh/id_ed25519',
},
}
The path must exist and be readable on the machine running ARC, and it must
name the private key, not the public .pub half and not known_hosts.
paramiko raises if it cannot read the file, and ARC then retries the connection for
up to 24 hours, so a wrong path shows up as a run that appears to hang rather than
as an immediate error.
~/.ssh/config is not read
ARC connects with paramiko, and paramiko only applies ~/.ssh/config when the
application explicitly builds a paramiko.SSHConfig. ARC does not, so no
directive in that file has any effect: not IdentityFile, not User, not
HostName, not Port, and not ProxyJump or ProxyCommand.
The practical consequences are:
every connection detail must be spelled out in the server entry -
address,un, andkeyif you use one - even when yoursshcommand line works without them;bastion and jump hosts are not supported. A cluster that can only be reached through a jump host cannot be driven by ARC directly. Run ARC on a machine that has direct access to the login node instead, or establish the tunnel outside ARC and point
addressat the local end of it.
This is a property of ARC on any machine, not something introduced by running it in a container.
Host Key Verification
ARC loads the system host keys from paramiko’s default location, which is
~/.ssh/known_hosts on the machine running ARC, and nothing else. In particular
/etc/ssh/ssh_known_hosts is not read, even though the OpenSSH command line
client reads it; paramiko’s load_system_host_keys() consults the user file only,
and silently ignores it if it cannot be read. The path is not configurable from
settings.py.
What happens when a host is not in known_hosts depends on the per-server
strict_host_key_checking flag:
Value |
Behavior for an unknown host |
|---|---|
|
The connection proceeds and a warning is logged. Convenient, but a machine-in-the-middle is indistinguishable from a first-ever connection, so the warning is the only signal you get. |
|
The connection is refused. The host must be present in |
servers = {
'cluster_a': {
'cluster_soft': 'Slurm',
'address': 'login.cluster.edu',
'path': '/home',
'un': 'my_user',
'strict_host_key_checking': True,
},
}
Seed known_hosts before enabling the flag:
ssh-keyscan -H login.cluster.edu >> ~/.ssh/known_hosts
Verify the fingerprints against a trusted source before trusting the result -
ssh-keyscan trusts whatever answers on the network.
Why warning is the default
Refusing an unknown host is the safer policy in the abstract, and it is a poor default for ARC specifically, because ARC is a scheduler that runs unattended for days rather than a client that makes one connection.
A refused host key does not fail the run once and stop. Every submission, status
poll and download for that server fails while the ARC driver stays alive, so the run
continues, submits nothing, and looks like a stall. Each individual refusal is
reported at once rather than retried, but nothing takes the driver down, so whoever
is running it typically finds out hours later, and recovery means stopping ARC,
running ssh-keyscan, and starting again.
Weighed against a first-ever connection to a login node reached over a network the
user already trusts enough to submit jobs to, that failure mode costs more than the
risk it removes.
What makes the default defensible is that the risk is reported, not hidden. ARC
checks known_hosts at startup (see Startup Checks), so an unseeded host is
named before any calculation is submitted, and
'strict_host_key_checking': True remains available per server for anyone who
wants the connection refused instead.
Running in the Docker Image
Everything above applies unchanged inside the container; what changes is that
~/.arc, the SSH material, and possibly the agent socket have to be bind-mounted
in. See Docker image for the image’s general usage.
Mounting your ARC settings
The container user is mambauser with HOME=/home/mambauser, so your personal
settings must be mounted at /home/mambauser/.arc:
-v "$HOME/.arc:/home/mambauser/.arc:ro"
ARC reads settings.py, submit.py and inputs.py from that directory. All
three matter for remote submission: submit.py holds the cluster’s PBS/Slurm
submit script templates, and without it ARC will submit jobs with the repository’s
generic templates.
Two properties of that overlay are worth knowing:
It is a replacement of top-level names, not a deep merge. A
settings.pythat defines onlyserversreplaces the wholeserversdictionary and leaves every other setting at its repository default - which is what you usually want - but asettings.pythat defines, say,levels_essreplaces that whole dictionary too.Whenever a local
settings.pyexists, ARC forcesglobal_ess_settingstoNoneunless that file defines a truthy value of its own. This is deliberate: the repository defaults are dummies. If you route software to servers throughglobal_ess_settings, define it in your ownsettings.py.
Mount the directory read-only, and set PYTHONDONTWRITEBYTECODE=1 so Python does
not try to write __pycache__ into a read-only mount:
-e PYTHONDONTWRITEBYTECODE=1
With agent forwarding (preferred)
Forward the host’s ssh-agent socket and omit key from the server entry. No key
material enters the container:
docker run --rm \
-v "$PWD:/work" -w /work \
-e PUID=$(id -u) -e PGID=$(id -g) \
-e PYTHONDONTWRITEBYTECODE=1 \
-v "$HOME/.arc:/home/mambauser/.arc:ro" \
-v "$HOME/.ssh/known_hosts:/home/mambauser/.ssh/known_hosts:ro" \
-v "$SSH_AUTH_SOCK:/ssh-agent" -e SSH_AUTH_SOCK=/ssh-agent \
laxzal/arc:latest arc my_case/input.yml
On macOS, Docker Desktop exposes the host agent at a fixed path, so use
-v /run/host-services/ssh-auth.sock:/ssh-agent instead of $SSH_AUTH_SOCK.
PUID/PGID remap the mambauser account to your host UID/GID. This is not
optional for agent forwarding, it is the mechanism: an agent socket is mode 0600
and owned by you, so only a container user carrying your UID can open it.
The entrypoint deliberately does not relax the socket’s permissions to work
around a missing remap. A bind mount shares the inode with the host, so doing that
would make your real, live agent socket readable and writable by every other user on
the machine, for as long as the agent runs - and because the entrypoint hands off
with exec, nothing would ever restore it. If you genuinely need that behaviour,
set ARC_WIDEN_AGENT_SOCKET=1; the entrypoint will then relax the mode, say
exactly what it changed, and remind you to run chmod 600 "$SSH_AUTH_SOCK" on the
host afterwards. Passing PUID/PGID is almost always the better answer.
A note on stale sockets: if $SSH_AUTH_SOCK points at an agent that has since
exited, Docker creates the missing bind-mount source rather than failing, leaving a
root-owned directory at that path on your host. The entrypoint reports it as “not a
socket” and continues without agent forwarding. The same applies to $HOME/.arc
if that directory does not exist.
With a mounted key (fallback)
For headless runs where no agent is available, mount the key read-only and point
key at the path inside the container:
servers = {
'cluster_a': {
'cluster_soft': 'Slurm',
'address': 'login.cluster.edu',
'path': '/home',
'un': 'my_user',
'key': '/home/mambauser/.ssh/id_ed25519',
},
}
docker run --rm \
-v "$PWD:/work" -w /work \
-e PUID=$(id -u) -e PGID=$(id -g) \
-e PYTHONDONTWRITEBYTECODE=1 \
-v "$HOME/.arc:/home/mambauser/.arc:ro" \
-v "$HOME/.ssh/id_ed25519:/home/mambauser/.ssh/id_ed25519:ro" \
-v "$HOME/.ssh/known_hosts:/home/mambauser/.ssh/known_hosts:ro" \
laxzal/arc:latest arc my_case/input.yml
A read-only mount is fine: ARC uses paramiko, which unlike the OpenSSH command line client does not refuse key files with permissive modes. The key must be passphrase-free, since there is nothing to prompt for it in a batch run.
To mount the whole directory instead of individual files, use
-v "$HOME/.ssh:/home/mambauser/.ssh:ro". The entrypoint detects that
/home/mambauser/.ssh is a bind mount and leaves its ownership and modes alone,
so nothing leaks back to your host files.
Docker Compose
docker-compose.yml in the repository root wires all of this up already:
ARC_WORKDIR=$PWD ARC_INPUT=my_case/input.yml \
PUID=$(id -u) PGID=$(id -g) \
docker compose run --rm arc
It mounts $HOME/.arc read-only, forwards $SSH_AUTH_SOCK to /ssh-agent,
mounts $ARC_KNOWN_HOSTS read-only at /home/mambauser/.ssh/known_hosts, and
sets PYTHONDONTWRITEBYTECODE=1. The key-file mount is present but commented out;
uncomment it if you are not using an agent.
Point ARC_KNOWN_HOSTS at your host keys to share them with the container:
ARC_WORKDIR=$PWD ARC_INPUT=my_case/input.yml \
ARC_KNOWN_HOSTS=$HOME/.ssh/known_hosts \
PUID=$(id -u) PGID=$(id -g) \
docker compose run --rm arc
The variable defaults to /dev/null, which reads as an empty set of host keys,
rather than to $HOME/.ssh/known_hosts directly. Docker creates any bind-mount
source that does not exist, so naming that path unconditionally would leave a
root-owned directory at $HOME/.ssh/known_hosts on a host that has never
written the file - which then breaks ssh on the host itself.
known_hosts in a fresh container
A fresh container starts with an empty /home/mambauser/.ssh, so every host is
an unknown host. With the default strict_host_key_checking: False that only
produces a warning per connection, but with strict_host_key_checking: True it
refuses every connection until the file is seeded. paramiko reads it from its
default location, so the mount target has to be exactly
/home/mambauser/.ssh/known_hosts - mounting it anywhere else has no effect.
Either mount the host’s file, as in the examples above, or seed one inside the
container. The image ships openssh-client for exactly this:
docker run --rm -it \
-v "$HOME/.ssh:/home/mambauser/.ssh" \
-e PUID=$(id -u) -e PGID=$(id -g) \
laxzal/arc:latest \
ssh-keyscan login.cluster.edu
Append the verified output to the known_hosts file you then mount read-only.
Network reachability
A container on Docker’s default bridge network reaches an ordinary cluster login
node without any extra flags: the host acts as a NAT router, DNS resolves, and
paramiko speaks SSH itself rather than shelling out to ssh, so nothing but
outbound TCP on port 22 is required. --network host is not needed in the normal
case.
Two situations do differ from running on the host:
The cluster is reachable only over a VPN on the host. Split-tunnel routing and firewall rules frequently exclude the
docker0bridge, so the container’s traffic never enters the tunnel.--network hostis the escape hatch: the container then shares the host’s network namespace and its VPN routes.Internal hostnames resolved by a VPN-pushed resolver. The container uses Docker’s resolver, not the host’s, so a name that only the VPN’s DNS server knows will not resolve. Use the IP address in
address, or pass--dns <resolver-ip>.
To tell the two apart, check reachability from inside the container before blaming ARC:
docker run --rm laxzal/arc:latest \
bash -lc 'getent hosts login.cluster.edu && ssh-keyscan -T 5 login.cluster.edu'
A resolved address followed by a host key means the network path is fine and the problem is authentication.
Startup Checks
Every ARC run, in a container or not, reports the configured servers that have no
host key in ~/.ssh/known_hosts before it submits anything. The message names the
server, its address, and the ssh-keyscan command that fixes it. It is a warning,
never fatal, and the check is entirely local: the known_hosts file is read, and
no name is resolved and no connection opened.
Three kinds of server entry are skipped, because reporting them would be noise
rather than information: the local server, entries with no address, and
entries still carrying the repository’s placeholder *.host.edu address or
<username> user name.
Inside the container the entrypoint checks more, and earlier - before ARC itself
starts - because the failure modes there are otherwise invisible until much later in
the run. Most notably, ARC ignores a ~/.arc/settings.py that fails to import
and silently continues with the repository’s dummy servers
(server1.host.edu, <username>) - in a container, a mis-typed mount path
produces exactly that.
The entrypoint therefore:
refuses to start ARC, with exit code 78, if
/home/mambauser/.arc/settings.pyexists but cannot be imported, printing the import traceback;warns if no
settings.pyis mounted at all;warns about a server whose
keydoes not exist or is not readable inside the container;warns about a server with no
keywhen the container has neither a forwarded agent nor a default key under/home/mambauser/.ssh;warns about a server with
strict_host_key_checkingwhen noknown_hostsfile is present.
Only the unimportable settings.py is fatal. The rest are warnings, so a server
that is configured but not used in this particular run cannot abort it. Set
ARC_SKIP_PREFLIGHT=1 to skip the checks entirely.
Exit codes from the entrypoint follow sysexits.h: 64 for a usage error, 66 for
a missing input file, and 78 for a configuration error.
Troubleshooting
ARC connects to server1.host.edu , or reports unknown server names.
Your ~/.arc/settings.py was not picked up. Outside a container, check that the
file is at $HOME/.arc/settings.py and imports cleanly with
python -c "import sys; sys.path.insert(0, '$HOME/.arc'); import settings".
Inside a container, check the mount target is /home/mambauser/.arc.
The run appears to hang while connecting. ARC retries a failed connection every
60 seconds, up to 1440 times, so a full 24 hours – but only a transport-level one,
such as a login node that is down or a key pointing at a missing file. The
reason is reported on every attempt, but only every tenth attempt goes through the
logger, at info level; the others are written straight to standard output, so look
there as well as in the log if the run seems stuck.
Failures that retrying cannot resolve are not retried at all: a rejected identity
(a wrong username, or a key the server does not accept), a host key that contradicts
known_hosts, and a rejected host key under strict_host_key_checking raise a
ServerError naming the cause on the first attempt.
Permission denied, or the forwarded agent is not usable. Pass
-e PUID=$(id -u) -e PGID=$(id -g). Without it the container user’s UID does not
match the owner of your mounts or of the agent socket, and the entrypoint will say
so.
“cannot remap mambauser to uid/gid N”. The requested ID belongs to an account
the container cannot safely displace - the superuser, or a system account reserved
by the distribution. Ordinary, idle accounts are shared automatically, so this only
appears for IDs below 1000. Drop the flag named in the message and mount ownership
will fall back to the image’s own mambauser IDs.
Host key refused. The server sets strict_host_key_checking: True and the
host is not in known_hosts. Seed it with ssh-keyscan, verify the
fingerprint, and mount the file at /home/mambauser/.ssh/known_hosts.