diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94b2f9a..185f87b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,6 @@ on: - 'Folderly/Frontend/**' - '.github/workflows/ci.yml' pull_request: - paths: - - 'Folderly/Backend/**' - - 'Folderly/Frontend/**' - - '.github/workflows/ci.yml' # A second push to the same branch makes the first one irrelevant. concurrency: diff --git a/Folderly/compose.yaml b/Folderly/compose.yaml new file mode 100644 index 0000000..ccedaa2 --- /dev/null +++ b/Folderly/compose.yaml @@ -0,0 +1,15 @@ +# The whole application locally, the way the host runs it. `include` resolves +# the API's paths and .env against Backend/, so nothing is repeated here. +include: + - Backend/docker-compose.yml + +services: + # No FOLDERLY_DOMAIN, so Caddy stays on plain HTTP. + web: + build: Frontend + ports: + - "127.0.0.1:8080:80" + volumes: + - ../deploy/Caddyfile:/etc/caddy/Caddyfile:ro + depends_on: + - app diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb6d3a9 --- /dev/null +++ b/README.md @@ -0,0 +1,309 @@ +
+ Self-hosted file storage with folder sharing - a small, honest Google Drive you run yourself. +
+ +
+
+
+
+
+
+
+
+
+
`, so an uploaded `.html` is shown as source rather than run. + + + +**The same folder as the person it was shared with sees it:** the name alone - never where it sits in +the owner's tree - a badge for their role, and a toolbar with nothing on it they are not allowed to +do. + + + +**The theme is chosen, not sniffed**, and the browser chrome follows it. + + + +## License + +MIT - see [LICENSE](LICENSE). Run it, change it, redistribute it, build on it commercially if +you like, keep the copyright notice, and note that it comes with no warranty of any kind. diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 2d5392c..c27c06d 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -1,4 +1,4 @@ -# Lives at /srv/folderly next to its .env, see the README. +# Lives at /srv/folderly next to its .env, see docs/deployment.md. services: app: diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..fe37939 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,188 @@ +# Deploying Folderly + +Everything needed to put Folderly on a host of your own. What the project is, and how to run it +locally, is in the [README](../README.md). + +## One host, one domain + +You need a 64-bit Linux host, a domain pointing at it, and ports 80 and 443 reachable from the +internet. A Raspberry Pi 4 is enough. Nothing has to be built and nothing has to be edited in the +repository - every value that differs between installations lives in one `.env` file. + +**1. Docker**, if the host does not have it. Distribution packages are usually too old: + +```bash +curl -fsSL https://get.docker.com | sudo sh +sudo usermod -aG docker $USER # log out and back in before the group applies +``` + +**2. Directories for the data.** `files/` is `DISK_PATH`, `db/` holds the SQLite database: + +```bash +sudo mkdir -p /srv/folderly/files /srv/folderly/db +sudo chown -R 10001:10001 /srv/folderly/files /srv/folderly/db +``` + +The `chown` is required, not tidiness. The API image runs as UID 10001 and these are bind mounts, +so ownership comes from the host, without it `db.create_all()` cannot create the database at +startup, gunicorn exits and the container restart-loops before serving a request. The UID is pinned +in the Dockerfile precisely so this command is the same on every machine. + +**3. The two files that describe the stack:** + +```bash +cd /srv/folderly +BASE=https://raw.githubusercontent.com/UltiPro/Folderly/main/deploy +sudo curl -fsSLO $BASE/docker-compose.prod.yml +sudo curl -fsSLO $BASE/Caddyfile +``` + +`/srv/folderly` is a convention rather than a requirement, and the only host path involved: the +container paths are fixed inside the images. Four files name it, though - the compose file's three +bind mounts, both service units and the backup script - so putting the data elsewhere means +changing it in all of them. + +**4. Configuration.** Copy the template and fill in five values: + +```bash +sudo curl -fsSL $BASE/.env.example -o .env +openssl rand -hex 32 # the JWT secret, generated on this host +sudo nano .env +sudo chmod 600 .env +``` + +| Variable | What to put there | +| -------------------------------------- | -------------------------------------------------------------------------------------------- | +| `FOLDERLY_IMAGE`, `FOLDERLY_WEB_IMAGE` | `ghcr.io//...` for your own fork's builds, or `ultipro` to run this repository's | +| `FOLDERLY_DOMAIN` | the domain Caddy answers on and obtains a certificate for | +| `CORS_ORIGINS` | `https:// ` | +| `JWT_SECRET_KEY` | the `openssl` output, never a key reused from development | + +The remaining five have working defaults. Leave `FOLDERLY_DOMAIN` empty to serve plain HTTP on +port 80 instead - then `CORS_ORIGINS` takes the `http://` form. + +**5. Start it:** + +```bash +sudo docker compose -f docker-compose.prod.yml up -d +sudo docker compose -f docker-compose.prod.yml logs -f web # "certificate obtained successfully" +sudo docker compose -f docker-compose.prod.yml ps # app healthy, web running +``` + +Caddy obtains the certificate itself, and port 80 has to be open for that as well - that is how +Let's Encrypt verifies the domain. Then, from anywhere: + +``` +https:// /api/hello -> "Hello World!" +https:// / -> the application +https:// /api/swagger-ui -> the API docs +``` + +**6. The first account.** Registration is open, but a new account is inactive and can do nothing +until it redeems a code, so an instance needs one before anyone can use it: + +```bash +sudo docker compose -f docker-compose.prod.yml exec app \ + flask --app "app:init()" codes new --label "me" +``` + +That command prints the code, and it is the only time you see it - the database keeps nothing but +its hash. Register in the browser, sign in, and enter the code on the activation screen. + +**7. Automatic updates**, which is what closes the CI/CD loop: a systemd timer pulls whatever CI +last published and restarts only if the digest changed. Every connection is outgoing, so the host +needs no inbound access for GitHub to reach it, and no credential of this network ever leaves it: + +```bash +sudo curl -fsSL $BASE/folderly-update.service -o /etc/systemd/system/folderly-update.service +sudo curl -fsSL $BASE/folderly-update.timer -o /etc/systemd/system/folderly-update.timer +sudo systemctl daemon-reload +sudo systemctl enable --now folderly-update.timer +systemctl list-timers folderly-update.timer +``` + +Five minutes is the default. To look less often on a host that does other things too, override the +interval rather than editing the unit, so re-installing the file later does not undo it: + +```bash +sudo mkdir -p /etc/systemd/system/folderly-update.timer.d +sudo tee /etc/systemd/system/folderly-update.timer.d/override.conf >/dev/null < ` from a build that worked, then +`up -d` again. One tag covers both images, so a tested pair stays together. Go back to `latest` +afterwards, or the timer holds the old version forever. + +**When it does not come up**, in order of likelihood: the `chown` in step 2 was skipped (the +container restart-loops on "unable to open database file"), port 80 does not reach the host from +the internet (no certificate, ACME errors in the web log), or `CORS_ORIGINS` has the wrong scheme +(the SPA loads but every request fails). There is no `Flask-Migrate`, so a model change means +deleting the database file and creating the accounts again, uploads on disk survive it. + +Two things worth knowing about the limits. The 85% fill threshold measures the whole filesystem, +system included, so uploads are refused before a full disk can take down the OS, Docker and SQLite +at once. And uploads are capped at 100 MB (`MAX_UPLOAD_BYTES`), which Caddy passes through untouched +because it sets no body limit of its own.