셀프호스팅 앱 플랫폼Self-hosted app platform

서버 한 대에 앱을 얹어두면 계속 돌아갑니다 Put an app on the shelf. It stays running.

Git 저장소나 Docker 이미지를 컨테이너로 배포하고, 도메인·SSL·CI/CD를 대신 처리합니다. nginx 설정 파일도, 인증서 갱신 크론도 직접 만들지 않습니다. Deploy a Git repository or a Docker image as a container. Shelf handles the domain, the certificate, and the deploy pipeline — no nginx config to write, no renewal cron to babysit.

MIT · Docker 한 개 · 서버 한 대 MIT · one container · one server

shelf.example.com/admin
Shelf 대시보드: 호스트 CPU·메모리·디스크와 앱별 사용량
배포Deploy

저장소를 걸어두면 push가 배포입니다Register a repo once, then just push

앱의 계약은 하나입니다 — 저장소 루트에 Dockerfile이 있고, 컨테이너가 포트 하나로 HTTP를 서빙하면 됩니다. There is one contract: a Dockerfile at the repo root, and a container that serves HTTP on a single port.

  • 웹훅 주소와 시크릿을 발급Webhook URL and secret, generatedGitHub 설정에 붙여넣기만 하면 됩니다.paste them into GitHub and you're done.
  • 배포 이력과 롤백History and rollback커밋·시각·결과가 남고, 예전 커밋으로 다시 빌드합니다.every commit, time and result is kept; rebuild from an earlier one.
  • 이미지도 그대로Images tooDocker Hub·GHCR 이미지를 pull해서 바로 올립니다.pull from Docker Hub or GHCR and run it as is.
/admin/deploy/projects/4
앱 상세 화면: 설정, 웹훅 주소와 시크릿, 컨테이너 로그, 배포 이력
도메인과 인증서Domains and certificates

리버스 프록시가 들어 있습니다The reverse proxy is already inside

앱에 도메인을 적으면 프록시에 등록됩니다. 앱끼리는 컨테이너 이름으로 직접 연결되므로 호스트 포트를 열 필요가 없습니다. Type a domain into an app and it is routed. Apps reach each other by container name, so no host ports need opening.

  • Let's EncryptHTTP-01 또는 Cloudflare DNS-01. 와일드카드도 되고 갱신은 매일 확인합니다. HTTP-01 or Cloudflare DNS-01, wildcards included, renewal checked daily.
  • 인증서가 생기면 HTTPS 강제HTTPS enforced on arrival80은 301로 넘기고 HSTS를 켭니다. 인증서를 지우면 되돌아가 잠기지 않습니다. port 80 redirects and HSTS turns on; remove the certificate and it reverts, so you never lock yourself out.
  • 외부에 여는 포트는 80·443뿐Only 80 and 443 face the world관리 화면도 프록시 뒤에 있습니다.the admin UI sits behind the proxy too.
/admin/proxy
프록시 호스트 목록: 도메인, 컨테이너 타깃, online과 SSL 상태
감시와 알림Watching and alerts

앱이 죽으면 대시보드가 아니라 당신에게 알립니다When an app dies, you hear about it

상태가 바뀌는 순간에만 알립니다. 죽으면 한 번, 돌아오면 한 번. 직접 세워둔 앱은 장애로 보지 않습니다. Alerts fire on change, not on a timer: one when an app goes down, one when it returns. An app you stopped on purpose is never an incident.

  • 호스트와 앱의 지표Host and app metricsCPU·로드·메모리·디스크와 앱별 사용량을 5초마다 갱신합니다. CPU, load, memory, free disk and per-app usage, refreshed every five seconds.
  • 받는 곳은 내가 정함Delivered where you want웹훅으로 JSON POST. Discord·Slack 주소도 그대로 씁니다. JSON over a webhook — a Discord or Slack URL works as is.
  • 서명과 발송 이력Signed, and logged시크릿을 넣으면 HMAC 서명이 붙고, 보낸 결과가 남습니다. add a secret for an HMAC signature; every delivery is recorded.
/admin/notifications
알림 화면: 채널 목록과 다운·복구 발송 이력
1서버 한 대면 충분server is enough
4런타임 의존성runtime dependencies
96자동 테스트automated tests
2외부 노출 포트ports exposed

설치Install

Docker가 있는 리눅스 서버에서 두 줄이면 됩니다. 관리 화면은 첫 접속에서 계정을 만듭니다. Two commands on any Linux box with Docker. The admin UI asks you to create an account on first visit.

you@your-server
$ git clone https://github.com/KangminNa/shelf && cd shelf
$ docker compose up -d --build

# 도메인을 .env 에 적으면 부팅할 때 프록시에 자동 등록됩니다put your domain in .env and it is routed at boot
#   ADMIN_DOMAIN=shelf.example.com
#   ACME_EMAIL=you@example.com

아직 못 하는 것What it can't do yet

쓰기 전에 아는 편이 낫습니다. 여기 없는 문제는 대체로 해결돼 있습니다. Better to know before you commit. Problems not listed here are generally handled.

무중단 배포Zero-downtime deploys

컨테이너를 교체하므로 몇 초 끊깁니다. 헬스체크 후 전환하는 방식은 아직입니다. The container is replaced, so there are a few seconds of downtime. Health-checked cutover is not built yet.

시크릿 저장Secret storage

Git 토큰과 웹훅 시크릿은 data/에 평문으로 있습니다. Git tokens and webhook secrets sit in data/ in plain text.

여러 대의 서버More than one server

한 대를 잘 쓰는 도구입니다. 클러스터링은 목표가 아닙니다. A tool for running one machine well. Clustering is an explicit non-goal.

빌드 로그 실시간Live build logs

배포가 끝난 뒤 전체 로그를 봅니다. You read the full log once the deploy finishes.

DNS 제공자DNS providers

DNS-01은 Cloudflare만 지원합니다. 다른 곳은 HTTP-01을 쓰면 됩니다. DNS-01 works with Cloudflare only; elsewhere, use HTTP-01.

관리자 권한Admin scope

관리자 계정은 Docker 소켓을 쥐고 있어 설계상 root와 동급입니다. The admin account holds the Docker socket, so it is root-equivalent by design.