Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
.gitignore
.omc/
.vscode/
.github/
.railway/
node_modules/
package*.json
*.md
!README.md
LICENSE
Expand Down
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ s6-overlay/** text eol=lf
sshd_config text eol=lf
fail2ban/** text eol=lf
banner.txt text eol=lf
supervisord.conf text eol=lf
tmux.conf text eol=lf

# Dockerfile
Dockerfile text eol=lf
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Build image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: false
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint-dockerfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ jobs:
hadolint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.1.0
uses: hadolint/hadolint-action@v3.5.0
with:
dockerfile: Dockerfile
2 changes: 1 addition & 1 deletion .github/workflows/shellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/trivy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ jobs:
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Build image for scanning
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: false
Expand All @@ -29,15 +29,15 @@ jobs:
cache-from: type=gha

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.28.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: claude-code-ssh:scan
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH

- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: trivy-results.sarif
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Thumbs.db
.vscode/
.idea/

# Railway IaC SDK (npm install)
node_modules/

# OMC state
.omc/

Expand Down
4 changes: 3 additions & 1 deletion .hadolint.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
---
ignored:
# apt pin versions — impractical on rolling base image (node:24-slim)
# apt pin versions — impractical on rolling base image (node:26-slim)
- DL3008
# Last USER should not be root — s6-overlay requires root as PID 1
- DL3002
# Non-numeric USER — claude is created in this image and only switched to at build time
- DL3066
# Single quotes intentional — writes literal $HOME/$PATH to profile script
- SC2016
50 changes: 50 additions & 0 deletions .railway/railway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { defineRailway, github, project, service, volume } from "railway/iac";

// This repository manages only its own resources, so applying it into an
// existing project never touches the other services there.
// See https://docs.railway.com/infrastructure-as-code#multi-repo-projects
export const partial = "claude-code-ssh";

export default defineRailway(() => {
// EU West (Amsterdam). The service and its volume must share a region; pick
// one near you before the first apply, since moving a volume is destructive.
const region = "europe-west4-drams3a";

// Name matches the volume created by the template, so existing deployments
// keep their data instead of getting a new, empty volume.
const data = volume("claude-code-mobile-volume", { region, sizeMB: 5000 });

const claudeCodeSsh = service("claude-code-ssh", {
// Deploying from a fork? Point this at your fork.
source: github("Ntelikatos/claude-code-ssh", { branch: "master", checkSuites: true }),
// Railway always builds with the root Dockerfile when one exists.
build: {
buildEnvironment: "V3",
watchPatterns: [
"Dockerfile",
"sshd_config",
"banner.txt",
"tmux.conf",
"scripts/**",
"s6-overlay/**",
"fail2ban/**",
],
},
// Restart policy type defaults to ON_FAILURE; only the retry limit differs.
deploy: {
restartPolicyMaxRetries: 5,
},
replicas: { [region]: 1 },
tcp: [22],
volumeMounts: {
"/data": data,
},
});

return project("claude-code-ssh", {
// Secrets (SSH_PUBLIC_KEY, ANTHROPIC_API_KEY, GITHUB_TOKEN, ...) stay in
// Railway variables and are never written to this file.
variables: { managed: false },
resources: [claudeCodeSsh, data],
});
});
17 changes: 8 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# ==============================================================================
# claude-code-ssh — SSH-accessible Claude Code container
# Base: node:24-slim (Debian Bookworm)
# Base: node:26-slim (Node.js 26 LTS, Debian 13 Trixie)
# PID 1: s6-overlay v3
# ==============================================================================

FROM node:24-slim AS base
FROM node:26-slim AS base

# ---------- build args --------------------------------------------------------
ARG S6_OVERLAY_VERSION=3.2.0.2
ARG S6_OVERLAY_VERSION=3.2.3.2

# ---------- locale ------------------------------------------------------------
ENV LANG=en_US.UTF-8 \
Expand Down Expand Up @@ -47,7 +47,7 @@ RUN install -m 0755 -d /etc/apt/keyrings \

# ---------- Node version manager (n) + pnpm -----------------------------------
# hadolint ignore=DL3016
RUN npm install -g n@10.1.0 pnpm@10.4.1 \
RUN npm install -g n@11.0.0 pnpm@12.16.1 \
&& npm cache clean --force

# ---------- s6-overlay v3 -----------------------------------------------------
Expand Down Expand Up @@ -87,6 +87,7 @@ RUN printf '%s\n' \
RUN mkdir -p /run/sshd /var/log /data \
&& chown claude:claude /data \
&& sed -i 's/^module(load="imklog")/#module(load="imklog")/' /etc/rsyslog.conf \
&& echo 'auth,authpriv.* /dev/stdout' > /etc/rsyslog.d/50-auth-stdout.conf \
&& rm -f /etc/ssh/ssh_host_*

# ---------- config files ------------------------------------------------------
Expand All @@ -99,6 +100,7 @@ COPY tmux.conf /etc/tmux.conf

# ---------- s6-overlay service definitions ------------------------------------
COPY s6-overlay/s6-rc.d/ /etc/s6-overlay/s6-rc.d/
COPY s6-overlay/user-bundles.d/ /etc/s6-overlay/user-bundles.d/

# ---------- scripts -----------------------------------------------------------
COPY scripts/init-setup.sh /etc/s6-overlay/scripts/init-setup.sh
Expand All @@ -107,16 +109,13 @@ RUN chmod +x /etc/s6-overlay/scripts/init-setup.sh /usr/local/bin/stderr-to-json

# ---------- register s6 services in user bundle -------------------------------
RUN for svc in /etc/s6-overlay/s6-rc.d/*/type; do \
svc_name="$(basename "$(dirname "$svc")")"; \
if [ "$svc_name" != "user" ] && [ "$svc_name" != "user2" ]; then \
touch "/etc/s6-overlay/s6-rc.d/user/contents.d/${svc_name}"; \
fi; \
touch "/etc/s6-overlay/user-bundles.d/user/contents.d/$(basename "$(dirname "$svc")")"; \
done

# ---------- runtime -----------------------------------------------------------
EXPOSE 22

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD ssh-keyscan -T 5 localhost >/dev/null 2>&1 || exit 1
CMD ["/bin/sh", "-c", "ssh-keyscan -T 5 localhost >/dev/null 2>&1 || exit 1"]

ENTRYPOINT ["/bin/bash", "-c", "exec /init 2> >(stderr-to-json)"]
Loading
Loading