{"id":51281537,"url":"https://github.com/deklol/octuna","last_synced_at":"2026-06-30T02:05:20.228Z","repository":{"id":355487646,"uuid":"1228282893","full_name":"deklol/octuna","owner":"deklol","description":"Self-hosted image host. Lightweight Node.js alternative to Imgur for your own domain. No accounts, no ads, no tracking. Drag and drop, paste, or pick. Short URLs, admin panel, scrypt-hashed auth, magic-byte upload validation, runs on a Raspberry Pi.","archived":false,"fork":false,"pushed_at":"2026-05-03T20:47:57.000Z","size":144,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-03T22:27:06.193Z","etag":null,"topics":["file-upload","image-host","image-hosting","image-server","image-upload","imgur-alternative","nodejs","raspberry-pi","screenshot-host","self-hosted","self-hosted-alternative","uploader","vps"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/deklol.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-03T20:40:43.000Z","updated_at":"2026-05-03T21:57:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/deklol/octuna","commit_stats":null,"previous_names":["deklol/octuna"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/deklol/octuna","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deklol%2Foctuna","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deklol%2Foctuna/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deklol%2Foctuna/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deklol%2Foctuna/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deklol","download_url":"https://codeload.github.com/deklol/octuna/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deklol%2Foctuna/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34949244,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["file-upload","image-host","image-hosting","image-server","image-upload","imgur-alternative","nodejs","raspberry-pi","screenshot-host","self-hosted","self-hosted-alternative","uploader","vps"],"created_at":"2026-06-30T02:05:19.739Z","updated_at":"2026-06-30T02:05:20.223Z","avatar_url":"https://github.com/deklol.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://i.dek.cx/b0bu.png\" alt=\"Octuna logo\" width=\"200\"\u003e\n\u003c/p\u003e\n\n# Octuna\n\nSelf-hosted image host. A small Node.js application for uploading images and short videos and serving them at short URLs from your own domain. No accounts, no ads, no tracking, no expiration. One runtime dependency, one JSON metadata file, one folder of uploads.\n\nOctuna is intended as a self-hosted alternative to public image hosts (Imgur, ImgBB, Postimage, ImageShack) for users who want to keep their files on their own server, control their own URLs, and not rely on a third-party service that may add ads, charge a fee, or delete inactive uploads.\n\n## Screenshots\n\nHomepage:\n\n![Homepage](https://i.dek.cx/e101.png)\n\nAdmin panel:\n\n![Admin](https://i.dek.cx/vnre.png)\n\nImage view page:\n\n![Image preview](https://i.dek.cx/0hcp.png)\n\n## Features\n\n- Drag and drop, paste from clipboard, or file picker upload\n- Multi-file upload with per-file progress and previews\n- Short, random, URL-safe IDs (configurable length)\n- Direct image links, view pages with Open Graph and Twitter Card metadata for link previews\n- Copy as plain URL, Markdown, HTML, BBCode, or view-page link\n- Public stats endpoint (file count, total size)\n- Password-protected admin panel: gallery, search, rename, delete\n- Single static config file, single JSON metadata file, single uploads directory\n- Hard heap cap of 64 MB, runs comfortably on a 256 MB VPS or a Raspberry Pi\n- Retro Web 1.0 interface skin (optional, all in CSS)\n- Mobile responsive layout\n- HTTPS-ready behind any reverse proxy (Nginx, Caddy, Apache, Traefik)\n\n## Security\n\n- Admin password hashed with scrypt (N=16384, r=8, p=1) and a 16-byte salt\n- Constant-time username and hash comparison (no timing oracle)\n- Server refuses to start until credentials are configured\n- Stateless HMAC-signed session cookies (no server-side session store)\n- Per-IP rate limits on login attempts, admin endpoints, and uploads\n- Magic-byte validation on every upload (rejects files whose contents do not match the claimed MIME type)\n- SVG uploads disabled by default (SVG can carry script payloads)\n- Strict Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and HSTS headers on every response\n- Uploaded files served with their true MIME type, `nosniff`, and a sandboxed CSP\n- Path traversal prevented by strict ID validation before any filesystem access\n- Configurable body size caps for JSON and uploads\n\n## Stack\n\n- Node.js 18 or newer\n- One runtime dependency: [busboy](https://www.npmjs.com/package/busboy) for multipart parsing\n- No framework, no database server, no native modules, no build step\n\nThe backend is approximately 500 lines of plain JavaScript using the Node built-in `http` module. Metadata is stored in a JSON file loaded once at startup. Uploads stream directly from the request to disk; downloads stream from disk to the response.\n\n## Quick start\n\n```\ngit clone https://github.com/deklol/octuna.git\ncd octuna\ncp config.example.json config.json\nnpm install\nnpm run setup\nnpm start\n```\n\nThe setup script prompts for an admin username and password, hashes the password with scrypt, and writes the hash and a session secret into `config.json`. The server refuses to start until this has been done.\n\nOpen http://localhost:3030 to upload files. The admin panel is at http://localhost:3030/admin.\n\n## Configuration\n\n`config.json` keys:\n\n| Key | Type | Notes |\n|---|---|---|\n| `port` | number | TCP port (default 3030) |\n| `publicUrl` | string | Base URL used in returned links, e.g. `https://images.example.com` |\n| `idLength` | number | Random ID length, 2 to 16 (default 4) |\n| `maxUploadMB` | number | Per-file upload limit |\n| `addressBarMode` | string | `real` shows the actual URL, `fixed` shows `addressBarFixed` |\n| `addressBarFixed` | string | Optional override displayed in the retro address bar |\n| `siteName` | string | Display name |\n| `allowedTypes` | array | Whitelist of MIME types accepted at upload |\n| `adminUser`, `adminSalt`, `adminHash`, `sessionSecret` | string | Set by `npm run setup`, do not edit by hand |\n\n## Production deployment\n\nOctuna is a long-running Node process that listens on a local port. Front it with any reverse proxy that handles TLS, then start it under any process supervisor.\n\n### systemd\n\n```\n[Unit]\nDescription=Octuna image host\nAfter=network.target\n\n[Service]\nType=simple\nUser=www-data\nWorkingDirectory=/srv/octuna\nExecStart=/usr/bin/node --max-old-space-size=64 server.js\nRestart=on-failure\nMemoryMax=128M\nNoNewPrivileges=true\nProtectSystem=full\nProtectHome=true\nPrivateTmp=true\nReadWritePaths=/srv/octuna/data\nEnvironment=NODE_ENV=production\n\n[Install]\nWantedBy=multi-user.target\n```\n\n### Nginx\n\n```\nserver {\n    listen 80;\n    server_name images.example.com;\n    client_max_body_size 30M;\n\n    location / {\n        proxy_pass http://127.0.0.1:3030;\n        proxy_http_version 1.1;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_request_buffering off;\n    }\n}\n```\n\nThen run `certbot --nginx -d images.example.com` to issue a Let's Encrypt certificate.\n\n### Caddy\n\n```\nimages.example.com {\n    reverse_proxy 127.0.0.1:3030\n    request_body {\n        max_size 30MB\n    }\n}\n```\n\nCaddy obtains and renews TLS certificates automatically.\n\n### pm2\n\n```\nnpm install -g pm2\npm2 start \"node --max-old-space-size=64 server.js\" --name octuna\npm2 save \u0026\u0026 pm2 startup\n```\n\n## File layout\n\n```\noctuna/\n  server.js            HTTP server and routes\n  setup.js             interactive admin credential setup\n  config.json          local configuration (gitignored)\n  config.example.json  template\n  package.json\n  public/\n    index.html         upload page\n    style.css          retro CSS shell\n    app.js             upload client (drag, drop, paste, progress)\n    logo.png\n  views/\n    image.html         single-image view page\n    admin.html         admin panel\n    about.html         about page\n  data/\n    uploads/           uploaded files\n    meta.json          id to metadata index\n```\n\n## Backups\n\nEverything that matters is in `data/`. To back up an Octuna instance:\n\n```\ntar czf octuna-backup-$(date +%F).tgz data/\n```\n\nTo restore on a new server, copy `data/` into place after `npm install`. Octuna will pick the metadata up at next start.\n\n## Updating\n\nPull or copy the new `server.js`, `public/`, and `views/`. The `data/` folder stays. Restart the service.\n\n## Rotating the admin password\n\n```\nnpm run setup\n```\n\nRe-run any time. The new salt and hash overwrite the old ones in `config.json`. Restart the server for the change to take effect.\n\n## ShareX integration\n\nOctuna works as a ShareX custom uploader without any extra plugins or server changes. The `/upload` endpoint accepts a standard multipart POST with the file under field name `file` and returns JSON containing the public URL, which is exactly what ShareX needs.\n\nA ready-to-use config is in [`octuna.sxcu.example`](octuna.sxcu.example). Copy it, replace `YOUR.DOMAIN.HERE` with your Octuna domain, save as `octuna.sxcu`, and double-click the file. ShareX will import it and prompt you to set Octuna as the active image and file uploader.\n\nThe config:\n\n```json\n{\n  \"Version\": \"16.1.0\",\n  \"Name\": \"Octuna\",\n  \"DestinationType\": \"ImageUploader, FileUploader\",\n  \"RequestMethod\": \"POST\",\n  \"RequestURL\": \"https://YOUR.DOMAIN.HERE/upload\",\n  \"Body\": \"MultipartFormData\",\n  \"FileFormName\": \"file\",\n  \"URL\": \"{json:url}\"\n}\n```\n\nTo configure manually in ShareX (Destinations, Custom uploader settings):\n\n- Method: POST\n- Request URL: `https://YOUR.DOMAIN.HERE/upload`\n- Body: Form data (multipart)\n- File form name: `file`\n- URL: `{json:url}`\n\nThe same uploader works for the screenshot, image, and file destinations. After ShareX uploads, the direct image URL is placed on your clipboard.\n\nIf you want a view-page URL instead of the direct file URL, set `URL` to `{json:viewUrl}`.\n\nOctuna's upload endpoint is open to anyone who can reach it. If you do not want public uploads, restrict access at the reverse proxy (allowlist your IP, basic auth, or a VPN) before exposing it.\n\n## API\n\n| Method | Path | Auth | Notes |\n|---|---|---|---|\n| GET | `/` | public | Upload page |\n| GET | `/about` | public | About page |\n| GET | `/api/stats` | public | Returns `{count, totalBytes}` |\n| POST | `/upload` | public | Multipart, field name `file`. Returns `{id, ext, url, viewUrl}` |\n| GET | `/:id` | public | View page for an upload |\n| GET | `/:id.:ext` | public | Direct file |\n| POST | `/admin/login` | public | JSON `{user, password}` |\n| POST | `/admin/logout` | session | |\n| GET | `/admin/me` | public | Returns session state |\n| GET | `/admin` | session | HTML admin panel |\n| GET | `/admin/list` | session | Returns all metadata records |\n| POST | `/admin/rename` | session | JSON `{id, newId}` |\n| POST | `/admin/delete` | session | JSON `{id}` |\n\n## Why\n\nPublic image hosts have a long history of starting free, adding ads, restricting hotlinking, deleting inactive uploads, requiring sign-up, or shutting down entirely. If your screenshots, memes, gifs, asset previews, or forum images matter to you, host them yourself. Octuna is small enough to read in one sitting, runs on hardware you already have, and produces URLs you control.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeklol%2Foctuna","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeklol%2Foctuna","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeklol%2Foctuna/lists"}