Self-hosted cloud notes with a focused React editor, FastAPI backend, PostgreSQL storage, cookie authentication, image uploads, and English/Russian localization.
- Frontend: React 19, TypeScript, Vite, TipTap, and Lucide icons
- Backend: FastAPI, Pydantic, and async SQLAlchemy 2.0
- Database: PostgreSQL 16
- Authentication: JWT in HttpOnly cookies with bcrypt password hashing
- Web server: Caddy providing automatic HTTPS in front of the internal Nginx service
- Deployment: Docker Compose with health checks and persistent volumes
Docker or OrbStack is the only requirement. Python, Node.js, Nginx, and PostgreSQL do not need to be installed on the host.
-
Clone the repository:
git clone <repository-url> cd Cloud_Notes_api
-
Create the environment file and set a public domain:
cp .env.example .env
Set
DOMAINto a DNS name that points to the router's public IP. Forward TCP ports 80 and 443 from the router to the Docker host before starting the stack. -
Start the complete application with one command:
docker compose up -d --build
-
Open Cloud Notes:
- Application:
https://notes.example.com - API documentation:
https://notes.example.com/api/docs
- Application:
Caddy obtains and renews the public TLS certificate automatically. PostgreSQL, FastAPI, Nginx, and the Vite build are reachable only through the private Docker network.
Create .env before the first start:
cp .env.example .envRecommended settings:
DOMAIN=notes.example.com
POSTGRES_USER=cloud_notes
POSTGRES_PASSWORD=use-a-long-random-password
POSTGRES_DB=cloud_notes
JWT_SECRET_KEY=use-a-random-secret-at-least-32-bytes-long
JWT_EXPIRE_MINUTES=43200
SESSION_MAX_AGE_SECONDS=2592000
COOKIE_SECURE=true
REGISTRATION_ENABLED=true
SQL_ECHO=falseDOMAINmust resolve to the router's public IP. Dynamic addresses can be maintained with any compatible DDNS provider.JWT_SECRET_KEYkeeps login sessions valid after backend recreation. Without it, a secure temporary key is generated at every backend start and existing sessions are invalidated.JWT_EXPIRE_MINUTEScontrols token lifetime.43200is 30 days;0explicitly enables non-expiring tokens.SESSION_MAX_AGE_SECONDScontrols the browser cookie lifetime and should match the intended token lifetime.COOKIE_SECURE=trueprevents authentication cookies from being sent over plain HTTP.- Set
REGISTRATION_ENABLED=falseafter creating the required accounts if public registration is not needed. - Database credentials must be selected before the PostgreSQL volume is initialized. Changing them later does not modify an existing database volume.
Apply configuration changes with:
docker compose up -d --buildBefore exposing the application to the internet:
-
Create the local environment file:
cp .env.example .env
-
Replace every value marked
PRODUCTIONin.env:-
set a real public
DOMAIN; -
choose PostgreSQL credentials before the first database start;
-
generate and preserve a JWT signing key:
openssl rand -hex 32
-
-
Configure DNS: point the selected domain to the router's public IP. Configure a DDNS updater if that address changes periodically.
-
Give the Docker host a fixed LAN address: use a DHCP reservation or a static network configuration.
-
Configure router forwarding:
- TCP 80 to the Docker host port 80;
- TCP 443 to the Docker host port 443;
- UDP 443 to the Docker host port 443 only when HTTP/3 is wanted.
-
Do not expose internal services: ports 5432, 8000, and the former development port 8080 must remain closed externally.
-
Keep secure cookies enabled:
COOKIE_SECURE=trueis required for the HTTPS deployment. -
Restrict account creation: after creating the intended accounts, consider setting
REGISTRATION_ENABLED=falseand recreating the server container. -
Verify certificate issuance:
docker compose logs -f caddy
-
Test from an external network: open the configured HTTPS domain over cellular data instead of the server's local Wi-Fi.
-
Configure backups: regularly back up both the PostgreSQL data and uploaded attachment volume.
| Service | Container port | Host port | Exposure |
|---|---|---|---|
| Caddy HTTP | 80 | 80/tcp |
Public; redirects to HTTPS and handles ACME |
| Caddy HTTPS | 443 | 443/tcp and 443/udp |
Public HTTPS and HTTP/3 |
| Nginx frontend | 80 | Not published | Docker network only |
| FastAPI backend | 8000 | Not published | Docker network only |
| PostgreSQL | 5432 | Not published | Docker network only |
Do not forward ports 5432, 8000, or the former frontend port 8080 from the router.
Docker volumes preserve both database records and uploaded images:
pgdatastores PostgreSQL data.uploadsstores note attachments.
On upgrade, files from the legacy backend/uploads directory are copied into the attachment volume automatically. Existing attachment links that used localhost:8000 are normalized by the API.
Stop containers without deleting data:
docker compose downDo not use docker compose down -v unless all notes and uploaded images may be permanently deleted.
Create a database backup:
docker compose exec -T db pg_dump -U admin main_db > cloud-notes-backup.sqlWhen custom PostgreSQL values are configured, replace admin and main_db in the backup command.
Check container health and published ports:
docker compose psFollow application logs:
docker compose logs -f caddy frontend server dbRestart the stack:
docker compose restartUpdate after pulling new code:
git pull
docker compose up -d --buildCloud_Notes_api/
├── backend/
│ ├── app/
│ │ ├── api/ # Notes, users, attachments, and health routes
│ │ ├── database.py # Async SQLAlchemy engine and sessions
│ │ ├── dependencies.py # Authentication dependencies
│ │ ├── main.py # FastAPI application and startup lifecycle
│ │ ├── models.py # User, note, and attachment models
│ │ ├── schemas.py # Request and response validation
│ │ └── utils.py # Password hashing and JWT helpers
│ └── Dockerfile
├── frontend/
│ ├── src/ # React application, styles, API client, and localization
│ ├── Dockerfile # Vite build and Nginx runtime
│ └── nginx.conf # Static frontend and `/api` reverse proxy
├── Caddyfile # Public HTTPS and reverse proxy configuration
├── docker-compose.yml
├── .env.example
└── README.md
- Only Caddy ports 80 and 443 are published.
- Authentication cookies are HttpOnly and use
SameSite=Lax. - Token and cookie lifetimes are configurable. The production example uses 30 days; signing out invalidates the browser cookie immediately.
- Login attempts are limited per client address.
- Uploaded images are size-limited, streamed to disk, and checked by extension, MIME type, and file signature.
- SQL parameter logging is disabled by default.
- The backend container runs as an unprivileged user.
- Caddy redirects HTTP to HTTPS and manages certificate renewal automatically.